2680 lines
168 KiB
HTML
2680 lines
168 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN" class="h-full">
|
||
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Gemini Go - Google账号管理工具</title>
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||
<link rel="stylesheet" href="/style.css">
|
||
</head>
|
||
|
||
<body class="bg-gray-50 h-full overflow-hidden flex flex-col" x-data="geminiApp()" x-init="init()">
|
||
|
||
<!-- 顶部栏 -->
|
||
<header class="app-header bg-white shadow-sm border-b border-gray-200 sticky top-0 z-50">
|
||
<div class="app-header__inner app-page-wide flex items-stretch justify-between">
|
||
<!-- 左侧:账号管理 + 客户筛选 -->
|
||
<div class="app-header__left flex items-stretch gap-1">
|
||
<button @click="activeTab='accounts'" :class="activeTab==='accounts' ? 'active' : ''" class="tab-btn">
|
||
账号管理 <span class="text-xs text-gray-400" x-text="'(' + accounts.length + ')'"></span>
|
||
</button>
|
||
<div class="flex items-center" x-show="activeTab==='accounts'">
|
||
<select x-model="customerNameFilter" @change="onCustomerFilterChange()"
|
||
style="height:28px;padding:2px 8px;font-size:12px;min-width:90px;cursor:pointer;border:1px solid #d1d5db;border-radius:4px;background:#fff;color:#374151">
|
||
<option value="">全部客户</option>
|
||
<option value="__empty__">未分配</option>
|
||
<template x-for="cn in customerNames" :key="cn">
|
||
<option :value="cn" x-text="cn"></option>
|
||
</template>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<!-- 右侧:其他选项卡 + 状态信息 -->
|
||
<div class="app-header__right flex items-stretch">
|
||
<button @click="activeTab='settings'" :class="activeTab==='settings' ? 'active' : ''" class="tab-btn">
|
||
设置
|
||
</button>
|
||
<button @click="activeTab='logs'" :class="activeTab==='logs' ? 'active' : ''" class="tab-btn">
|
||
诊断日志
|
||
</button>
|
||
<div class="header-top-controls flex items-center gap-4 ml-4">
|
||
<!-- 比特浏览器连接状态 -->
|
||
<div class="flex items-center text-sm">
|
||
<span class="status-dot" :class="bitStatus"></span>
|
||
<span x-text="bitStatus === 'online' ? '比特浏览器已连接' : (bitStatus === 'checking' ? '检测中...' : '比特浏览器未连接')"
|
||
:class="bitStatus === 'online' ? 'text-green-600' : 'text-red-500'"></span>
|
||
<button @click="checkBitStatus()"
|
||
class="ml-2 text-gray-400 hover:text-gray-600 text-xs cursor-pointer">↻</button>
|
||
</div>
|
||
<!-- 并发设置 -->
|
||
<div class="flex items-center gap-2 text-sm">
|
||
<label class="text-gray-500">并发:</label>
|
||
<input type="number" x-model="threadCount" @change="saveSetting('default_thread_count', threadCount)"
|
||
min="1" max="100" class="w-20 text-center" style="width:70px">
|
||
<button @click="toggleLogDrawer()" class="btn btn-gray text-xs" style="padding:4px 10px">日志</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="app-page-wide pt-3 pb-3 flex-1 overflow-hidden flex flex-col relative">
|
||
|
||
<!-- ===== 账号管理 ===== -->
|
||
<div x-show="activeTab === 'accounts'" class="flex-1 min-h-0 flex flex-col gap-3 overflow-hidden relative">
|
||
|
||
<!-- 第一行:顶级状态 -->
|
||
<div class="stats-grid stats-grid--six" style="margin-bottom:4px">
|
||
<div class="stat-card" :class="tierFilter.level1==='全部' ? 'active' : ''" @click="setTierFilter(1,'全部')">
|
||
<div class="stat-card__title">全部</div>
|
||
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level1['全部'] || 0"></div>
|
||
</div>
|
||
<div class="stat-card" :class="tierFilter.level1==='未登录' ? 'active' : ''" @click="setTierFilter(1,'未登录')">
|
||
<div class="stat-card__title">未登录</div>
|
||
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level1['未登录'] || 0"></div>
|
||
</div>
|
||
<div class="stat-card" :class="tierFilter.level1==='已登录' ? 'active' : ''" @click="setTierFilter(1,'已登录')">
|
||
<div class="stat-card__title">已登录</div>
|
||
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level1['已登录'] || 0"></div>
|
||
</div>
|
||
<div class="stat-card" :class="tierFilter.level1==='登录失败' ? 'active' : ''" @click="setTierFilter(1,'登录失败')">
|
||
<div class="stat-card__title">登录失败</div>
|
||
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level1['登录失败'] || 0"></div>
|
||
</div>
|
||
<div class="stat-card" :class="statusFilter==='任务中' ? 'active' : ''" @click="setStatusFilter('任务中')">
|
||
<div class="stat-card__title">任务中</div>
|
||
<div class="stat-card__value" x-text="dashboardStats.statusCounts['任务中'] || 0"></div>
|
||
</div>
|
||
<div class="stat-card" :class="statusFilter==='刚刚运行' ? 'active' : ''" @click="setStatusFilter('刚刚运行')"
|
||
style="border-left:2px solid #3b82f6" :title="recentTaskTooltip">
|
||
<div class="stat-card__title" style="color:#2563eb">🕘 刚刚运行</div>
|
||
<div class="stat-card__value" x-text="recentTaskCount"></div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- 第二行:业务状态(受第一行过滤) -->
|
||
<div class="stats-grid stats-grid--six" style="margin-bottom:4px">
|
||
<div class="stat-card" :class="tierFilter.level2==='未违反规则' ? 'active' : ''" @click="setTierFilter(2,'未违反规则')"
|
||
style="border-left:2px solid #10b981">
|
||
<div class="stat-card__title" style="color:#059669">✅ 未违反规则</div>
|
||
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level2['未违反规则'] || 0"></div>
|
||
</div>
|
||
<div class="stat-card" :class="tierFilter.level2==='违反规则' ? 'active' : ''" @click="setTierFilter(2,'违反规则')"
|
||
style="border-left:2px solid #ef4444">
|
||
<div class="stat-card__title" style="color:#dc2626">❌ 违反规则</div>
|
||
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level2['违反规则'] || 0"></div>
|
||
</div>
|
||
<div class="stat-card" :class="tierFilter.level2==='有凭证' ? 'active' : ''" @click="setTierFilter(2,'极速凭证') || setTierFilter(2,'有凭证')"
|
||
style="border-left:2px solid #7c3aed">
|
||
<div class="stat-card__title" style="color:#7c3aed">🔑 有凭证</div>
|
||
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level2['有凭证'] || 0"></div>
|
||
</div>
|
||
<div class="stat-card" :class="tierFilter.level2==='GCP被限制' ? 'active' : ''" @click="setTierFilter(2,'GCP被限制')"
|
||
style="border-left:2px solid #ef4444">
|
||
<div class="stat-card__title" style="color:#dc2626">🚫 GCP被限制</div>
|
||
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level2['GCP被限制'] || 0"></div>
|
||
</div>
|
||
<div class="stat-card" :class="tierFilter.level2==='OAuth等待7天' ? 'active' : ''" @click="setTierFilter(2,'OAuth等待7天')"
|
||
style="border-left:2px solid #f59e0b" title="点击筛选并全选要等7天的账号">
|
||
<div class="stat-card__title" style="color:#d97706">⏳ 要等7天</div>
|
||
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level2['OAuth等待7天'] || 0"></div>
|
||
</div>
|
||
<div class="stat-card" :class="tierFilter.level2==='未检测凭证' ? 'active' : ''" @click="setTierFilter(2,'未检测凭证')"
|
||
style="border-left:2px solid #9ca3af">
|
||
<div class="stat-card__title" style="color:#6b7280">未检测凭证</div>
|
||
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level2['未检测凭证'] || 0"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 第三行:操作状态(受第二行过滤) -->
|
||
<div class="stats-grid stats-grid--seven" style="margin-bottom:4px">
|
||
<div class="stat-card" :class="tierFilter.level3==='未改密' ? 'active' : ''" @click="setTierFilter(3,'未改密')">
|
||
<div class="stat-card__title">未改密</div>
|
||
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level3['未改密'] || 0"></div>
|
||
</div>
|
||
<div class="stat-card" :class="tierFilter.level3==='未改FA' ? 'active' : ''" @click="setTierFilter(3,'未改FA')">
|
||
<div class="stat-card__title">未改FA</div>
|
||
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level3['未改FA'] || 0"></div>
|
||
</div>
|
||
<div class="stat-card" :class="tierFilter.level3==='改密<7天' ? 'active' : ''" @click="setTierFilter(3,'改密<7天')"
|
||
style="border-left:2px solid #10b981">
|
||
<div class="stat-card__title" style="color:#059669">改密<7天</div>
|
||
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level3['改密<7天'] || 0"></div>
|
||
</div>
|
||
<div class="stat-card" :class="tierFilter.level3==='改密≥7天' ? 'active' : ''" @click="setTierFilter(3,'改密≥7天')"
|
||
style="border-left:2px solid #f59e0b">
|
||
<div class="stat-card__title" style="color:#d97706">改密≥7天</div>
|
||
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level3['改密≥7天'] || 0"></div>
|
||
</div>
|
||
<div class="stat-card" :class="tierFilter.level3==='可重新提取' ? 'active' : ''" @click="setTierFilter(3,'可重新提取')"
|
||
style="border-left:2px solid #0ea5e9">
|
||
<div class="stat-card__title" style="color:#0284c7">可重新提取</div>
|
||
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level3['可重新提取'] || 0"></div>
|
||
</div>
|
||
<div class="stat-card" :class="tierFilter.level3==='ChatGPT成功' ? 'active' : ''" @click="setTierFilter(3,'ChatGPT成功')"
|
||
style="border-left:2px solid #10b981">
|
||
<div class="stat-card__title" style="color:#047857">ChatGPT成功</div>
|
||
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level3['ChatGPT成功'] || 0"></div>
|
||
</div>
|
||
<div class="stat-card" :class="tierFilter.level3==='ChatGPT失败' ? 'active' : ''" @click="setTierFilter(3,'ChatGPT失败')"
|
||
style="border-left:2px solid #ef4444">
|
||
<div class="stat-card__title" style="color:#b91c1c">ChatGPT失败</div>
|
||
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level3['ChatGPT失败'] || 0"></div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
|
||
<!-- 工具栏:账号操作 + 搜索 合并一行 -->
|
||
<div class="toolbar-row">
|
||
<div class="toolbar-stack">
|
||
<div class="toolbar-line toolbar-line--accounts">
|
||
<div class="btn-group">
|
||
<span class="btn-group-label">账号</span>
|
||
<button @click="showImportAccountModal=true" class="btn btn-primary">导入</button>
|
||
<button @click="exportSelectedAccounts()" class="btn btn-gray">导出</button>
|
||
<button @click="openProblemExportModal()" class="btn btn-orange">导出问题号</button>
|
||
<button @click="openStagingModal()" class="btn btn-gray">暂存箱</button>
|
||
<button @click="loadAccounts()" class="btn btn-gray">刷新</button>
|
||
<button @click="deleteSelectedAccounts()" :disabled="isDeletingAccounts || selectedCount===0"
|
||
class="btn btn-red" title="删除所选账号、浏览器编号和对应的比特浏览器窗口"
|
||
x-text="isDeletingAccounts ? '删账号中...' : '删除账号'"></button>
|
||
<button @click="copySelectedEmails()" :disabled="selectedCount===0" class="btn btn-gray">复制邮箱</button>
|
||
</div>
|
||
<div class="btn-group">
|
||
<span class="btn-group-label">搜索</span>
|
||
<div class="search-box">
|
||
<input type="text" x-model="searchText" @input.debounce.150ms="resetAccountPage()"
|
||
placeholder="搜索邮箱/处理结论/状态/客户名..." class="search-input">
|
||
</div>
|
||
<button @click="openBatchEmailSearch()" class="btn btn-gray">批量邮箱查询</button>
|
||
<div style="display:flex;align-items:center;gap:6px;flex-shrink:0;flex-wrap:nowrap">
|
||
<div class="toolbar-info">
|
||
<span>共 <strong x-text="accounts.length"></strong> 个</span>
|
||
<span x-show="selectedCount > 0"> · 已选 <strong x-text="selectedCount"></strong></span>
|
||
<span x-show="isRunning && taskProgress.total > 0"> · 进度 <strong x-text="taskProgress.current"></strong>/<strong x-text="taskProgress.total"></strong></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="toolbar-line toolbar-line--operations">
|
||
<div class="btn-group">
|
||
<span class="btn-group-label">操作</span>
|
||
<button @click="openBatchStatusModal()" :disabled="selectedCount===0" class="btn btn-orange">更改状态</button>
|
||
<button @click="runDeleteBrowsers()" :disabled="isRunning || selectedCount===0"
|
||
class="btn btn-red" title="删除所选账号的比特浏览器窗口并清空浏览器编号,账号保留">删除浏览器</button>
|
||
<button @click="runChangeProxy()" :disabled="isRunning" class="btn btn-primary">换代理</button>
|
||
<button @click="openBatchCustomerNameModal()"
|
||
:disabled="selectedCount===0 || batchCustomerNameSaving" class="btn btn-gray">改写客户名</button>
|
||
<button @click="openRefundDeadlineModal()" :disabled="selectedCount===0 || refundDeadlineSaving"
|
||
class="btn btn-purple">退款截止时间</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div x-show="batchEmailSearchActive" x-cloak
|
||
class="flex items-center gap-2 flex-wrap rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 mb-2 text-xs text-blue-800">
|
||
<strong>批量邮箱查询中</strong>
|
||
<span>输入 <b x-text="batchEmailSearchEmails.length"></b> 个</span>
|
||
<span>· 找到 <b class="text-green-700" x-text="batchEmailSearchAccounts.length"></b> 个</span>
|
||
<span>· 未找到 <b class="text-red-600" x-text="batchEmailSearchMissingEmails.length"></b> 个</span>
|
||
<span class="text-blue-500">(可继续叠加普通搜索和组合筛选)</span>
|
||
<button @click="openBatchEmailSearch()" class="btn btn-sm btn-outline ml-auto">查看/修改</button>
|
||
<button x-show="batchEmailSearchMissingEmails.length > 0" @click="copyBatchEmailSearchMissing()"
|
||
class="btn btn-sm btn-outline">复制未找到</button>
|
||
<button @click="clearBatchEmailSearch()" class="btn btn-sm btn-red">退出查询</button>
|
||
</div>
|
||
|
||
<div class="account-filter-bar">
|
||
<div class="account-filter-bar__header">
|
||
<span class="account-filter-bar__title">组合筛选</span>
|
||
<button @click="addAccountFilterCondition()" class="btn btn-sm btn-outline">添加条件</button>
|
||
<button x-show="accountFilterConditions.length > 0" x-cloak @click="clearAccountFieldFilters()"
|
||
class="btn btn-sm btn-outline account-filter-clear">清空</button>
|
||
<span class="account-filter-bar__result">结果 <strong x-text="filteredAccounts.length"></strong></span>
|
||
</div>
|
||
<div class="account-filter-list" x-show="accountFilterConditions.length > 0" x-cloak>
|
||
<template x-for="(condition, index) in accountFilterConditions" :key="condition.id">
|
||
<div class="account-filter-condition">
|
||
<span class="account-filter-condition__index" x-text="index + 1"></span>
|
||
<select x-model="condition.field" @change="onAccountFilterConditionChange(condition)" aria-label="筛选字段">
|
||
<template x-for="field in accountFilterFields" :key="field.value">
|
||
<option :value="field.value" x-text="field.label"></option>
|
||
</template>
|
||
</select>
|
||
<select x-model="condition.operator" @change="onAccountFilterConditionChange(condition)" aria-label="匹配方式">
|
||
<option value="contains">包含</option>
|
||
<option value="not_contains">不包含</option>
|
||
<option value="equals">等于</option>
|
||
<option value="not_equals">不等于</option>
|
||
<option value="empty">为空</option>
|
||
<option value="filled">不为空</option>
|
||
</select>
|
||
<template x-if="accountFilterNeedsValue(condition.operator) && condition.field === 'status'">
|
||
<select x-model="condition.value" @change="onAccountFilterConditionChange(condition)" aria-label="状态值">
|
||
<option value="">选择状态</option>
|
||
<template x-for="status in accountStatusOptions" :key="status">
|
||
<option :value="status" x-text="status"></option>
|
||
</template>
|
||
</select>
|
||
</template>
|
||
<template x-if="accountFilterNeedsValue(condition.operator) && condition.field === 'recharge_status'">
|
||
<select x-model="condition.value" @change="onAccountFilterConditionChange(condition)" aria-label="是否已充值状态值">
|
||
<option value="">选择充值状态</option>
|
||
<template x-for="status in rechargeStatusOptions" :key="status">
|
||
<option :value="status" x-text="status"></option>
|
||
</template>
|
||
</select>
|
||
</template>
|
||
<template x-if="accountFilterNeedsValue(condition.operator) && condition.field === 'account_refund_status'">
|
||
<select x-model="condition.value" @change="onAccountFilterConditionChange(condition)" aria-label="账号退款状态值">
|
||
<option value="">选择账号退款状态</option>
|
||
<template x-for="status in accountRefundStatusOptions" :key="status">
|
||
<option :value="status" x-text="status"></option>
|
||
</template>
|
||
</select>
|
||
</template>
|
||
<template x-if="accountFilterNeedsValue(condition.operator) && condition.field === 'refund_process_status'">
|
||
<select x-model="condition.value" @change="onAccountFilterConditionChange(condition)" aria-label="是否已发起退款流程状态值">
|
||
<option value="">选择退款流程状态</option>
|
||
<template x-for="status in refundProcessStatusOptions" :key="status">
|
||
<option :value="status" x-text="status"></option>
|
||
</template>
|
||
</select>
|
||
</template>
|
||
<template x-if="accountFilterNeedsValue(condition.operator) && condition.field === 'refund_deadline_at'">
|
||
<select x-model="condition.value" @change="onAccountFilterConditionChange(condition)" aria-label="退款截止时间状态值">
|
||
<option value="">选择时间状态</option>
|
||
<option value="未设置">未设置</option>
|
||
<option value="未到期">未到期</option>
|
||
<option value="已到期">已到期</option>
|
||
</select>
|
||
</template>
|
||
<template x-if="accountFilterNeedsValue(condition.operator) && condition.field === 'is_pro'">
|
||
<select x-model="condition.value" @change="onAccountFilterConditionChange(condition)" aria-label="Pro 状态值">
|
||
<option value="">选择 Pro 状态</option>
|
||
<option value="Pro">Pro</option>
|
||
<option value="非Pro">非Pro</option>
|
||
<option value="未检测">未检测</option>
|
||
</select>
|
||
</template>
|
||
<template x-if="accountFilterNeedsValue(condition.operator) && condition.field !== 'status' && condition.field !== 'recharge_status' && condition.field !== 'account_refund_status' && condition.field !== 'refund_process_status' && condition.field !== 'refund_deadline_at' && condition.field !== 'is_pro'">
|
||
<input type="text" x-model="condition.value" @input.debounce.150ms="onAccountFilterConditionChange(condition)"
|
||
placeholder="输入筛选值" aria-label="筛选值">
|
||
</template>
|
||
<span x-show="!accountFilterNeedsValue(condition.operator)" class="account-filter-condition__no-value">无需填写</span>
|
||
<button @click="removeAccountFilterCondition(condition.id)" class="account-filter-remove"
|
||
title="删除此条件" aria-label="删除此条件">×</button>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 批量操作:横向按钮组 -->
|
||
<div class="action-row">
|
||
<div class="action-line action-line--tasks">
|
||
<!-- 账号任务:普通模式 -->
|
||
<template x-if="!customMode">
|
||
<div class="btn-group">
|
||
<span class="btn-group-label">任务</span>
|
||
<button @click="runLogin()" :disabled="isRunning" class="btn btn-primary">登录</button>
|
||
<button @click="runChatgptRegister()" :disabled="isRunning" class="btn btn-green">ChatGPT登录/注册</button>
|
||
<button @click="runOpenAIGetAuth()" :disabled="isRunning" class="btn btn-green">提取授权JSON</button>
|
||
<button @click="runOpenAIQueryPlan()" :disabled="isRunning" class="btn btn-green">查询套餐</button>
|
||
<button @click="runRefundQuery()" :disabled="isRunning" class="btn btn-purple">查询退款</button>
|
||
<button @click="openRechargeModal()" :disabled="isRunning || selectedCount===0" class="btn btn-orange">充值</button>
|
||
<button @click="runWorkspaceRefund()" :disabled="isRunning || selectedCount===0" class="btn btn-red">退款</button>
|
||
<button @click="runChange2fa()" :disabled="isRunning" class="btn btn-primary">更改2FA</button>
|
||
<button @click="runChangePassword()" :disabled="isRunning" class="btn btn-primary">改密码</button>
|
||
<button @click="runSecurityChatgpt()" :disabled="isRunning" class="btn btn-purple">改密+2FA</button>
|
||
<button @click="runAgGetCredential()" :disabled="isRunning" class="btn btn-purple">获取凭证</button>
|
||
<button @click="runAistudioCombo()" :disabled="isRunning" class="btn btn-purple">AI Studio 一键组合</button>
|
||
<button @click="runGeminiCliGetCredential()" :disabled="isRunning" class="btn btn-purple">获取Cli凭证</button>
|
||
<button @click="openGoogleAccountAgModal()" :disabled="isRunning" class="btn btn-blue">链接提取+获取凭证</button>
|
||
<button @click="runAgRevokeAuthorization()" :disabled="isRunning || selectedCount===0" class="btn btn-red">取消授权反重力</button>
|
||
<button @click="runCheckRestrictions()" :disabled="isRunning" class="btn btn-green">检查限制</button>
|
||
<button @click="runCheckSecurityDates()" :disabled="isRunning" class="btn btn-green">查安全日期</button>
|
||
</div>
|
||
</template>
|
||
<!-- 自定义模式:模板列表 -->
|
||
<template x-if="customMode">
|
||
<div style="flex:1;min-width:0;display:flex;flex-direction:column;gap:4px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:8px 10px;min-height:60px;max-height:50vh;overflow-y:auto;resize:vertical">
|
||
<!-- 已保存的模板列表(每个一行) -->
|
||
<template x-for="tpl in savedTemplates" :key="tpl.id">
|
||
<div style="display:flex;align-items:center;gap:6px;padding:4px 8px;border-radius:6px;cursor:pointer;transition:all .15s"
|
||
:style="selectedTemplateId === tpl.id ? 'background:#dbeafe;border:1px solid #93c5fd' : 'background:#fff;border:1px solid #e5e7eb'"
|
||
@click="selectTemplate(tpl)">
|
||
<!-- 单选 -->
|
||
<input type="radio" name="tpl_select" :checked="selectedTemplateId === tpl.id"
|
||
@click.stop="selectTemplate(tpl)"
|
||
style="accent-color:#3b82f6;width:14px;height:14px;flex-shrink:0">
|
||
<!-- 步骤流 -->
|
||
<div style="display:flex;align-items:center;gap:3px;flex:1;min-width:0;overflow-x:auto">
|
||
<template x-for="(s, si) in tpl.steps" :key="si">
|
||
<span style="display:inline-flex;align-items:center;gap:2px;white-space:nowrap">
|
||
<span style="padding:1px 6px;border-radius:4px;font-size:11px;background:#e2e8f0;color:#475569"
|
||
x-text="getCustomTaskLabel(s.key || s)"></span>
|
||
<span x-show="si < tpl.steps.length - 1" style="color:#cbd5e1;font-size:10px">→</span>
|
||
</span>
|
||
</template>
|
||
</div>
|
||
<!-- 模板名 -->
|
||
<span style="font-size:12px;font-weight:600;color:#6d28d9;white-space:nowrap;flex-shrink:0;padding:0 4px"
|
||
x-text="tpl.name"></span>
|
||
<!-- 删除 -->
|
||
<button @click.stop="deleteTemplateById(tpl.id)"
|
||
style="color:#f87171;font-size:11px;background:none;border:none;cursor:pointer;padding:0 2px;flex-shrink:0"
|
||
title="删除模板">✕</button>
|
||
</div>
|
||
</template>
|
||
<!-- 无模板提示 -->
|
||
<template x-if="savedTemplates.length === 0">
|
||
<div style="text-align:center;padding:8px;color:#94a3b8;font-size:12px">暂无模板,点击下方「新建」创建</div>
|
||
</template>
|
||
<!-- 新建模板区(折叠) -->
|
||
<div style="border-top:1px solid #e2e8f0;margin-top:2px;padding-top:6px">
|
||
<div style="display:flex;align-items:center;gap:4px;cursor:pointer" @click="showTemplateCreator = !showTemplateCreator">
|
||
<span style="font-size:11px;color:#3b82f6;font-weight:600">+ 新建模板</span>
|
||
<span style="font-size:10px;color:#94a3b8" x-text="showTemplateCreator ? '▲' : '▼'"></span>
|
||
</div>
|
||
<template x-if="showTemplateCreator">
|
||
<div style="margin-top:6px;display:flex;flex-direction:column;gap:4px">
|
||
<!-- 步骤勾选 -->
|
||
<div style="display:flex;align-items:center;gap:3px;flex-wrap:wrap">
|
||
<template x-for="td in _customTaskDefs" :key="td.key">
|
||
<div style="display:inline-flex;align-items:center;gap:2px">
|
||
<label style="display:inline-flex;align-items:center;gap:2px;cursor:pointer;padding:2px 7px;border-radius:5px;font-size:11px;user-select:none;white-space:nowrap"
|
||
:style="isCustomTaskChecked(td.key) ? 'background:#dbeafe;color:#1d4ed8;font-weight:600' : 'background:#f1f5f9;color:#64748b'">
|
||
<input type="checkbox" :checked="isCustomTaskChecked(td.key)" @change="toggleCustomTask(td.key)"
|
||
style="accent-color:#3b82f6;width:12px;height:12px">
|
||
<span x-text="td.label"></span>
|
||
</label>
|
||
<template x-if="td.params && isCustomTaskChecked(td.key)">
|
||
<template x-for="p in td.params" :key="p.key">
|
||
<label style="display:inline-flex;align-items:center;gap:2px;cursor:pointer;font-size:10px;padding:2px 5px;border-radius:3px;background:#fff7ed;color:#ea580c;user-select:none;white-space:nowrap">
|
||
<input type="checkbox" :checked="getStepParamValue(td.key, p.key)"
|
||
@change="setStepParamValue(td.key, p.key, $event.target.checked)"
|
||
style="accent-color:#f97316;width:10px;height:10px">
|
||
<span x-text="p.label"></span>
|
||
</label>
|
||
</template>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
<!-- 保存行:名称+按钮 -->
|
||
<div style="display:flex;align-items:center;gap:4px" x-show="customCheckedTasks.length > 0">
|
||
<div style="display:flex;align-items:center;gap:2px;flex:1;min-width:0;overflow-x:auto">
|
||
<template x-for="(tk, ti) in customCheckedTasks" :key="tk">
|
||
<span style="display:inline-flex;align-items:center;gap:2px;white-space:nowrap">
|
||
<span style="padding:1px 5px;border-radius:3px;font-size:10px;background:#dbeafe;color:#1d4ed8"
|
||
x-text="getCustomTaskLabel(tk)"></span>
|
||
<span x-show="ti < customCheckedTasks.length - 1" style="color:#93c5fd;font-size:9px">→</span>
|
||
</span>
|
||
</template>
|
||
</div>
|
||
<input type="text" x-model="templateName" placeholder="模板名称"
|
||
style="width:80px;padding:3px 6px;font-size:11px;border-radius:4px;border:1px solid #d1d5db;outline:none;flex-shrink:0">
|
||
<button @click="saveAsTemplate()" class="btn btn-orange" style="padding:2px 10px;font-size:11px;flex-shrink:0">保存</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
<div class="action-line action-line--open">
|
||
<!-- 打开网页 -->
|
||
<template x-if="!customMode">
|
||
<div class="btn-group">
|
||
<span class="btn-group-label">打开</span>
|
||
<button @click="runOpenBrowsers()" :disabled="isRunning" class="btn btn-green"
|
||
title="打开比特浏览器;人工创建 AI Studio Key 成功后会自动保存到当前邮箱">浏览器</button>
|
||
<button class="btn btn-primary" @click="runOpenCustomUrl()">打开网址</button>
|
||
<button class="btn btn-green" @click="runFeishuSync()"
|
||
:disabled="feishuSyncRunning"
|
||
x-text="feishuSyncRunning ? ('同步中 ' + feishuSyncProgress.current + '/' + feishuSyncProgress.total) : '同步飞书'"
|
||
title="只同步当前勾选的账号"></button>
|
||
<span x-show="feishuSyncStatusText" x-cloak class="text-xs max-w-64 truncate"
|
||
:class="feishuSyncStatusType === 'error' ? 'text-red-600' : (feishuSyncStatusType === 'success' ? 'text-green-600' : (feishuSyncStatusType === 'warn' ? 'text-orange-600' : 'text-blue-600'))"
|
||
:title="feishuSyncStatusText" x-text="feishuSyncStatusText"></span>
|
||
</div>
|
||
</template>
|
||
<!-- 控制 -->
|
||
<div class="btn-group" style="margin-left:auto;flex-shrink:0">
|
||
<button @click="toggleCustomMode()" class="btn" :class="customMode ? 'btn-blue' : 'btn-gray'"
|
||
:disabled="isRunning || customRunning">
|
||
<span x-text="customMode ? '退出' : '自定义'"></span>
|
||
</button>
|
||
<template x-if="customMode">
|
||
<button @click="runSelectedTemplate()"
|
||
:disabled="isRunning || customRunning || !selectedTemplateId"
|
||
class="btn btn-green">▶ 运行</button>
|
||
</template>
|
||
|
||
<button @click="togglePauseTask()" :disabled="!isRunning || !currentTaskId"
|
||
class="btn" :class="isPaused ? 'btn-blue' : 'btn-orange'"
|
||
x-text="isPaused ? '继续' : '暂停'"></button>
|
||
<button @click="customRunning ? stopCustomPipeline() : stopTask()" :disabled="!isRunning && !customRunning"
|
||
class="btn btn-red">停止</button>
|
||
<div class="text-xs"
|
||
:class="isPaused ? 'text-orange-600' : (isRunning ? 'text-blue-500' : (customRunning ? 'text-purple-500' : 'text-gray-400'))">
|
||
<span x-text="isPaused ? '已暂停' : (customRunning ? '运行中...' : (isRunning ? '运行中...' : '空闲'))"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="task-progress-strip" x-show="isRunning && taskProgress.total > 0" x-cloak>
|
||
<div class="task-progress-strip__meta">
|
||
<span class="task-progress-strip__task" x-text="activeTaskDisplayLabel"></span>
|
||
<span class="task-progress-strip__count">
|
||
<strong x-text="taskProgress.current"></strong>/<span x-text="taskProgress.total"></span>
|
||
· <span x-text="taskProgressPercent + '%'"></span>
|
||
</span>
|
||
<span class="task-progress-strip__eta" x-text="taskEtaText"></span>
|
||
</div>
|
||
<div class="task-progress-strip__track" role="progressbar"
|
||
:aria-valuenow="taskProgress.current" :aria-valuemax="taskProgress.total">
|
||
<span class="task-progress-strip__fill" :style="'width:' + taskProgressPercent + '%'"></span>
|
||
</div>
|
||
</div>
|
||
<div class="task-progress-strip" x-show="feishuSyncRunning && feishuSyncProgress.total > 0" x-cloak>
|
||
<div class="task-progress-strip__meta">
|
||
<span class="task-progress-strip__task">同步飞书</span>
|
||
<span class="task-progress-strip__count">
|
||
<span x-text="feishuSyncProgress.current"></span>/<span x-text="feishuSyncProgress.total"></span>
|
||
· <span x-text="Math.round((feishuSyncProgress.current / Math.max(1, feishuSyncProgress.total)) * 100) + '%' "></span>
|
||
</span>
|
||
<span class="task-progress-strip__eta">实时同步结果请查看处理日志</span>
|
||
</div>
|
||
<div class="task-progress-strip__track" role="progressbar"
|
||
:aria-valuenow="feishuSyncProgress.current" aria-valuemin="0" :aria-valuemax="feishuSyncProgress.total">
|
||
<span class="task-progress-strip__fill"
|
||
:style="'width:' + Math.round((feishuSyncProgress.current / Math.max(1, feishuSyncProgress.total)) * 100) + '%' "></span>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<!-- 账号表格 -->
|
||
<div class="table-container bg-white flex-1 min-h-0">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th style="width:32px">
|
||
<input type="checkbox" :checked="allSelected" @change="toggleSelectAll()">
|
||
</th>
|
||
<th>邮箱</th>
|
||
<th>客户名</th>
|
||
<th>账号退款</th>
|
||
<th>是否已发起退款流程</th>
|
||
<th>退款截止时间</th>
|
||
<th>状态</th>
|
||
<th>ChatGPT</th>
|
||
<th>是否已充值</th>
|
||
<th>授权JSON</th>
|
||
<th>AC凭证</th>
|
||
<th>Cli_AC凭证</th>
|
||
<th>账号套餐</th>
|
||
<th>Pro</th>
|
||
<th class="hide-mobile">密码</th>
|
||
<th class="hide-mobile">2FA验证码</th>
|
||
<th class="hide-mobile">16位应用密码</th>
|
||
<th class="hide-mobile">AI Studio Key</th>
|
||
<th class="hide-mobile">AI Studio 日志</th>
|
||
<th class="hide-mobile">辅助邮箱</th>
|
||
<th class="hide-mobile">安全日期</th>
|
||
<th class="hide-mobile">国家</th>
|
||
<th class="hide-mobile">浏览器编号</th>
|
||
<th class="hide-mobile">静态IP</th>
|
||
<th class="hide-mobile">处理结论</th>
|
||
<th class="hide-mobile">截图</th>
|
||
<th class="hide-mobile">更新时间</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<template x-for="account in pagedAccounts" :key="account.email">
|
||
<tr :class="isSelected(account.email) ? 'selected' : ''" @mousedown="startAccountDrag($event, account)"
|
||
@mouseenter="onAccountDragOver($event, account)">
|
||
<td @click.stop>
|
||
<input type="checkbox" :checked="isSelected(account.email)" @change="toggleSelect(account.email)">
|
||
</td>
|
||
<td class="copy-cell" :title="account.email" style="max-width:180px"
|
||
@click.stop="copyCell(account.email)" @dblclick.stop="openEditAccount(account)">
|
||
<span x-show="account.tag==='家庭组母号'" style="color:#f59e0b;margin-right:2px" title="家庭组母号">🏠</span>
|
||
<span x-show="account.restriction_status==='ok'" style="color:#059669;margin-right:2px;font-size:11px" title="未违反规则">✅</span>
|
||
<span x-show="account.restriction_status==='restricted'" style="color:#dc2626;margin-right:2px;font-size:11px" title="违反规则">⛔</span>
|
||
<span x-show="account.ag_refresh_token" style="color:#7c3aed;margin-right:2px;font-size:11px" title="AG凭证已获取">🔑</span>
|
||
<span x-show="_isGcpRestricted(account)" style="color:#dc2626;margin-right:2px;font-size:11px" title="GCP被限制">🚫</span>
|
||
<span x-show="_isOAuthWait7Account(account)" style="color:#d97706;margin-right:2px;font-size:11px" title="OAuth等待7天后重试">⏳</span>
|
||
<span x-text="account.email"></span>
|
||
</td>
|
||
<td>
|
||
<span x-show="account.customer_name" class="inline-block px-2 py-0.5 rounded-full text-xs font-medium"
|
||
:class="customerTagClass(account.customer_name)" x-text="account.customer_name"></span>
|
||
<span x-show="!account.customer_name" class="text-gray-300 text-xs">-</span>
|
||
</td>
|
||
<td class="cursor-pointer" @mousedown.stop @click.stop
|
||
title="双击可人工修改账号退款状态"
|
||
@dblclick.stop="startInlineEditAccountRefund(account.email)">
|
||
<template x-if="inlineEditAccountRefundEmail !== account.email">
|
||
<span class="status-badge" :class="statusClass(account.account_refund_status || '无状态')"
|
||
x-text="account.account_refund_status || '无状态'"></span>
|
||
</template>
|
||
<template x-if="inlineEditAccountRefundEmail === account.email">
|
||
<select class="status-select" style="min-width:132px;height:26px;padding:2px 4px;font-size:12px"
|
||
:value="account.account_refund_status || '无状态'"
|
||
@change="quickSetAccountRefundStatus(account.email, $event.target.value)"
|
||
@keydown.escape.stop="inlineEditAccountRefundEmail=null"
|
||
x-init="$nextTick(() => { $el.focus(); try { $el.showPicker() } catch(e) {} })">
|
||
<option value="无状态">无状态</option>
|
||
<option value="已退款">已退款</option>
|
||
<option value="未退款">未退款</option>
|
||
<option value="退款失败">退款失败</option>
|
||
<option value="已申请退款">已申请退款</option>
|
||
<option value="google封号无法查询">google封号无法查询</option>
|
||
<option value="gpt封号退款失败">gpt封号退款失败</option>
|
||
<option value="gpt警告">gpt警告</option>
|
||
<option value="已被改密无法查询">已被改密无法查询</option>
|
||
<option value="已被改2FA无法查询">已被改2FA无法查询</option>
|
||
<option value="需要手机号查询">需要手机号查询</option>
|
||
<option value="无订阅">无订阅</option>
|
||
</select>
|
||
</template>
|
||
</td>
|
||
<td class="cursor-pointer" @mousedown.stop @click.stop
|
||
title="双击可人工修改退款流程状态"
|
||
@dblclick.stop="startInlineEditRefundProcess(account.email)">
|
||
<template x-if="inlineEditRefundProcessEmail !== account.email">
|
||
<span class="status-badge" :class="statusClass(account.refund_process_status || '未发起')"
|
||
x-text="account.refund_process_status || '未发起'"></span>
|
||
</template>
|
||
<template x-if="inlineEditRefundProcessEmail === account.email">
|
||
<select class="status-select" style="min-width:92px;height:26px;padding:2px 4px;font-size:12px"
|
||
:value="account.refund_process_status || '未发起'"
|
||
@change="quickSetRefundProcessStatus(account.email, $event.target.value)"
|
||
@keydown.escape.stop="inlineEditRefundProcessEmail=null"
|
||
x-init="$nextTick(() => { $el.focus(); try { $el.showPicker() } catch(e) {} })">
|
||
<option value="未发起">未发起</option>
|
||
<option value="已发起">已发起</option>
|
||
<option value="发起失败">发起失败</option>
|
||
</select>
|
||
</template>
|
||
</td>
|
||
<td @mousedown.stop @click.stop>
|
||
<button type="button" class="chatgpt-session-trigger"
|
||
:class="account.refund_deadline_at && new Date(account.refund_deadline_at).getTime() <= Date.now() ? 'text-red-600' : ''"
|
||
:title="account.refund_deadline_at ? '点击修改退款截止时间' : '点击设置退款截止时间'"
|
||
@click.stop="openRefundDeadlineModal(account)"
|
||
x-text="account.refund_deadline_at ? formatDate(account.refund_deadline_at) : '未设置'"></button>
|
||
</td>
|
||
<td @dblclick.stop="startInlineEditStatus(account.email)" @mousedown.stop @click.stop>
|
||
<template x-if="inlineEditStatusEmail !== account.email">
|
||
<span class="status-badge" :class="statusClass(account.status)"
|
||
x-text="account.status || '未登录'"></span>
|
||
</template>
|
||
<template x-if="inlineEditStatusEmail === account.email">
|
||
<select class="status-select" style="width:100%;height:26px;padding:2px 4px;font-size:12px"
|
||
:value="account.status || '未登录'" @change="quickSetStatus(account.email, $event.target.value)"
|
||
@blur="inlineEditStatusEmail=null"
|
||
x-init="$nextTick(() => { $el.focus(); try { $el.showPicker() } catch(e) {} })">
|
||
<option value="未登录">未登录</option>
|
||
<option value="登录成功">登录成功</option>
|
||
<option value="登录失败">登录失败</option>
|
||
<option value="改密成功">改密成功</option>
|
||
<option value="改密失败">改密失败</option>
|
||
<option value="2FA修改成功">2FA修改成功</option>
|
||
<option value="2FA修改失败">2FA修改失败</option>
|
||
<option value="未验证已改密和2FA">未验证已改密和2FA</option>
|
||
<option value="已验证改密和2FA">已验证改密和2FA</option>
|
||
<option value="未注册成功已改密和2FA">未注册成功已改密和2FA</option>
|
||
<option value="ChatGPT成功">ChatGPT成功</option>
|
||
<option value="ChatGPT失败">ChatGPT失败</option>
|
||
<option value="有资格">有资格</option>
|
||
<option value="无资格">无资格</option>
|
||
<option value="认证成功">认证成功</option>
|
||
<option value="认证失败">认证失败</option>
|
||
<option value="绑卡成功">绑卡成功</option>
|
||
<option value="绑卡失败">绑卡失败</option>
|
||
<option value="订阅成功">订阅成功</option>
|
||
<option value="已充值提取">已充值提取</option>
|
||
<option value="超时">超时</option>
|
||
|
||
</select>
|
||
</template>
|
||
</td>
|
||
<td>
|
||
<div class="chatgpt-status-cell">
|
||
<span x-show="account.chatgpt_status" class="status-badge"
|
||
:class="statusClass(account.chatgpt_status)" x-text="account.chatgpt_status"></span>
|
||
<button x-show="account.chatgpt_status === '已登录' || account.chatgpt_status === '已注册'"
|
||
type="button" class="chatgpt-session-trigger" title="查看 ChatGPT Session"
|
||
@click.stop="openChatgptSession(account)">查看</button>
|
||
</div>
|
||
<span x-show="!account.chatgpt_status" class="text-gray-300 text-xs">-</span>
|
||
</td>
|
||
<td class="cursor-pointer" @mousedown.stop @click.stop
|
||
:title="(account.recharge_message ? account.recharge_message + ';' : '') + '双击可人工修改充值状态'"
|
||
@dblclick.stop="startInlineEditRechargeStatus(account.email)">
|
||
<template x-if="inlineEditRechargeStatusEmail !== account.email">
|
||
<div class="chatgpt-status-cell">
|
||
<span class="status-badge" :class="statusClass(account.recharge_status || '未充值')"
|
||
x-text="account.recharge_status || '未充值'"></span>
|
||
<button x-show="account.has_recharge_card" type="button"
|
||
class="chatgpt-session-trigger" title="查看本次充值使用的卡密"
|
||
@click.stop="openRechargeCard(account)" @dblclick.stop>查看卡密</button>
|
||
</div>
|
||
</template>
|
||
<template x-if="inlineEditRechargeStatusEmail === account.email">
|
||
<div class="chatgpt-status-cell">
|
||
<select class="status-select" style="min-width:104px;height:26px;padding:2px 4px;font-size:12px"
|
||
:value="account.recharge_status || '未充值'"
|
||
@change="quickSetRechargeStatus(account.email, $event.target.value)"
|
||
@blur="inlineEditRechargeStatusEmail=null"
|
||
x-init="$nextTick(() => { $el.focus(); try { $el.showPicker() } catch(e) {} })">
|
||
<option value="未充值">未充值</option>
|
||
<option value="充值中">充值中</option>
|
||
<option value="等待已确认">等待已确认</option>
|
||
<option value="充值成功">充值成功</option>
|
||
<option value="充值失败">充值失败</option>
|
||
<option value="账号本身已有会员">账号本身已有会员</option>
|
||
</select>
|
||
<button x-show="account.has_recharge_card" type="button"
|
||
class="chatgpt-session-trigger" title="查看本次充值使用的卡密"
|
||
@click.stop="openRechargeCard(account)" @dblclick.stop>查看卡密</button>
|
||
</div>
|
||
</template>
|
||
</td>
|
||
<td>
|
||
<div class="chatgpt-status-cell">
|
||
<span x-show="account.openai_auth_status || account.has_openai_auth" class="status-badge"
|
||
:class="statusClass(account.openai_auth_status || '已提取 JSON')"
|
||
x-text="account.openai_auth_status || '已提取 JSON'"></span>
|
||
<button x-show="account.has_openai_auth" type="button" class="chatgpt-session-trigger"
|
||
title="查看授权 JSON" @click.stop="openOpenAIAuth(account)">查看</button>
|
||
</div>
|
||
<span x-show="!account.openai_auth_status && !account.has_openai_auth"
|
||
class="text-gray-300 text-xs">-</span>
|
||
</td>
|
||
<td>
|
||
<div class="chatgpt-status-cell">
|
||
<span x-show="account.has_ag_credential || account.ag_refresh_token"
|
||
class="status-badge" :class="statusClass('凭证已获取')">已获取</span>
|
||
<button x-show="account.has_ag_credential || account.ag_refresh_token" type="button"
|
||
class="chatgpt-session-trigger" title="查看 AC 凭证"
|
||
@click.stop="openAgCredential(account)">查看</button>
|
||
</div>
|
||
<span x-show="!account.has_ag_credential && !account.ag_refresh_token"
|
||
class="text-gray-300 text-xs">-</span>
|
||
</td>
|
||
<td>
|
||
<div class="chatgpt-status-cell">
|
||
<span x-show="account.has_cli_ac_credential"
|
||
class="status-badge" :class="statusClass('凭证已获取')">已获取</span>
|
||
<button x-show="account.has_cli_ac_credential" type="button"
|
||
class="chatgpt-session-trigger" title="查看 Gemini CLI AC 凭证"
|
||
@click.stop="openCliAcCredential(account)">查看</button>
|
||
</div>
|
||
<span x-show="!account.has_cli_ac_credential" class="text-gray-300 text-xs">-</span>
|
||
</td>
|
||
<td style="min-width:132px;white-space:nowrap" :title="formatOpenAIPlanTitle(account)">
|
||
<div x-show="account.openai_plan_type || account.openai_plan_status" style="font-size:10px;line-height:1.45">
|
||
<span class="status-badge"
|
||
:class="statusClass(account.openai_plan_status || '查询成功')"
|
||
x-text="formatOpenAIPlanCell(account)"></span>
|
||
<div x-show="formatOpenAIUsagePercent(account.openai_usage_percent)">
|
||
用量:<span x-text="formatOpenAIUsagePercent(account.openai_usage_percent)"></span>
|
||
</div>
|
||
<div x-show="account.openai_subscription_expires_at">
|
||
到期:<span x-text="formatDate(account.openai_subscription_expires_at)"></span>
|
||
</div>
|
||
<div x-show="account.openai_plan_type
|
||
&& account.openai_plan_status
|
||
&& !['查询成功', '部分成功'].includes(account.openai_plan_status)"
|
||
:class="account.openai_plan_status === '查询失败' ? 'text-red-500' : 'text-gray-500'"
|
||
x-text="account.openai_plan_status"></div>
|
||
</div>
|
||
<span x-show="!account.openai_plan_type && !account.openai_plan_status"
|
||
class="text-gray-300 text-xs">-</span>
|
||
</td>
|
||
<td style="white-space:nowrap">
|
||
<template x-if="account.is_pro === true || Number(account.is_pro) === 1">
|
||
<span class="status-badge" style="background:#dcfce7;color:#166534">Pro</span>
|
||
</template>
|
||
<template x-if="account.is_pro === 0 || account.is_pro === false">
|
||
<span class="text-gray-400 text-xs">非Pro</span>
|
||
</template>
|
||
<template x-if="account.is_pro === null || account.is_pro === undefined">
|
||
<span class="text-gray-300 text-xs">-</span>
|
||
</template>
|
||
</td>
|
||
<td class="hide-mobile copy-cell" :title="account.password"
|
||
x-text="account.password ? account.password.slice(0,5) + '...' : '-'"
|
||
@click.stop="copyCell(account.password)" @dblclick.stop="openEditAccount(account)"></td>
|
||
<td class="hide-mobile" style="white-space:nowrap" @dblclick.stop="openEditAccount(account)">
|
||
<template x-if="account.secret_key">
|
||
<span>
|
||
<span class="copy-cell" :title="'剩余 ' + totpRemaining + 's'"
|
||
x-text="totpCodes[account.email] || '------'"
|
||
@click.stop="totpCodes[account.email] && copyCell(totpCodes[account.email])"></span>
|
||
<span> </span>
|
||
<span class="copy-cell" :title="account.secret_key" x-text="account.secret_key.slice(0,6) + '...'"
|
||
@click.stop="copyCell(account.secret_key)"></span>
|
||
</span>
|
||
</template>
|
||
<template x-if="!account.secret_key"><span>-</span></template>
|
||
</td>
|
||
<td class="hide-mobile copy-cell font-mono" style="white-space:nowrap"
|
||
:title="account.gmail_app_password ? '单击复制16位应用专用密码' : ''"
|
||
x-text="account.gmail_app_password || '-'"
|
||
@click.stop="account.gmail_app_password && copyCell(account.gmail_app_password)"></td>
|
||
<td class="hide-mobile" style="white-space:nowrap" @mousedown.stop @click.stop>
|
||
<div class="chatgpt-status-cell">
|
||
<span x-show="account.aistudio_api_key" class="status-badge"
|
||
:class="statusClass('成功')">已保存</span>
|
||
<span x-show="!account.aistudio_api_key" class="text-gray-300 text-xs">未保存</span>
|
||
<button x-show="account.aistudio_api_key" type="button"
|
||
class="chatgpt-session-trigger" title="查看该邮箱的 AI Studio API Key JSON"
|
||
@click.stop="openAistudioKeyModal(account)">查看</button>
|
||
</div>
|
||
</td>
|
||
<td class="hide-mobile" style="font-size:11px;line-height:1.4;white-space:nowrap;max-width:180px"
|
||
:title="formatAistudioFlowTitle(account)" @dblclick.stop="openEditAccount(account)">
|
||
<template x-if="account.aistudio_flow_status">
|
||
<div>
|
||
<div :style="account.aistudio_flow_status === 'success' ? 'color:#16a34a;font-weight:600' : 'color:#dc2626;font-weight:600'"
|
||
x-text="account.aistudio_flow_status === 'success' ? '✅ 成功' : ('❌ 失败:' + (account.aistudio_flow_failed_step || '未知步骤'))"></div>
|
||
<div class="text-gray-400" style="overflow:hidden;text-overflow:ellipsis"
|
||
x-text="account.aistudio_flow_status === 'success' ? formatDate(account.aistudio_flow_at) : (account.aistudio_flow_detail || '')"></div>
|
||
</div>
|
||
</template>
|
||
<template x-if="!account.aistudio_flow_status"><span class="text-gray-300">-</span></template>
|
||
</td>
|
||
<td class="hide-mobile copy-cell" :title="account.recovery_email"
|
||
x-text="account.recovery_email ? account.recovery_email.slice(0,5) + '...' : '-'"
|
||
@click.stop="copyCell(account.recovery_email)" @dblclick.stop="openEditAccount(account)"></td>
|
||
<td class="hide-mobile" style="font-size:11px;line-height:1.5;white-space:nowrap;min-width:110px"
|
||
:title="formatSecurityDatesTitle(account.security_dates)">
|
||
<template x-if="account.security_dates">
|
||
<div>
|
||
<div>2步:<span x-text="parseSecurityDate(account.security_dates, 'twoStep')"></span></div>
|
||
<div>改密:<span x-text="parseSecurityDate(account.security_dates, 'password')"></span></div>
|
||
<div>验证器:<span x-text="parseSecurityDate(account.security_dates, 'authenticator')"></span></div>
|
||
</div>
|
||
</template>
|
||
<template x-if="!account.security_dates"><span>-</span></template>
|
||
</td>
|
||
<td class="hide-mobile" x-text="account.country || '-'" @dblclick.stop="openEditAccount(account)"></td>
|
||
<td class="hide-mobile" :class="getBrowserSeq(account.browser_id, account.browser_seq) ? 'copy-cell' : ''"
|
||
:title="getBrowserSeq(account.browser_id, account.browser_seq) ? 'ID: ' + account.browser_id : ''"
|
||
x-text="getBrowserSeq(account.browser_id, account.browser_seq) || '-'"
|
||
@click.stop="getBrowserSeq(account.browser_id, account.browser_seq) && copyCell(getBrowserSeq(account.browser_id, account.browser_seq))"
|
||
@dblclick.stop="openEditAccount(account)"></td>
|
||
<td class="hide-mobile font-mono" style="font-size:10px;max-width:120px;overflow:hidden;text-overflow:ellipsis"
|
||
:class="account.proxy_ip ? 'copy-cell' : ''"
|
||
:title="account.proxy_ip || ''"
|
||
x-text="account.proxy_ip ? account.proxy_ip.split(':').slice(0,2).join(':') : '-'"
|
||
@click.stop="account.proxy_ip && copyCell(account.proxy_ip)"></td>
|
||
|
||
<td class="hide-mobile" :title="getLiveMessage(account)" :class="getLiveMessageClass(account)"
|
||
x-text="getLiveMessage(account).slice(0,35)" @dblclick.stop="openEditAccount(account)"></td>
|
||
<td class="hide-mobile" style="position:relative">
|
||
<span x-show="account.screenshot_path"
|
||
class="text-blue-500 hover:text-blue-700 text-xs whitespace-nowrap cursor-pointer"
|
||
@click.stop="viewScreenshot(account)"
|
||
@mouseenter="screenshotHover = '/' + account.screenshot_path + '?t=' + Date.now(); $nextTick(() => { const r=$event.target.getBoundingClientRect(); const el=document.getElementById('screenshotPreview'); if(el){el.style.top=(r.top-320)+'px'; el.style.left=Math.min(r.left, window.innerWidth-420)+'px';} })"
|
||
@mouseleave="screenshotHover = null"
|
||
>查看截图</span>
|
||
<span x-show="!account.screenshot_path" class="text-gray-300 text-xs">-</span>
|
||
</td>
|
||
<td class="hide-mobile" x-text="formatDate(account.updated_at)"></td>
|
||
</tr>
|
||
</template>
|
||
<tr x-show="filteredAccounts.length === 0">
|
||
<td colspan="27" class="text-center text-gray-400 py-8"
|
||
x-text="accounts.length === 0 ? '暂无账号,点击导入账号开始' : '没有符合当前筛选条件的账号'"></td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="account-pagination" x-show="filteredAccounts.length > 0">
|
||
<div style="display:flex;align-items:center;gap:6px;min-width:0">
|
||
<span class="account-pagination__summary"
|
||
x-text="'本页 ' + pagedAccounts.length + ' 个 · 筛选结果 ' + filteredAccounts.length + ' 个'"></span>
|
||
<button class="btn btn-gray text-xs" style="padding:3px 9px" @click="selectCurrentPage()"
|
||
:disabled="pagedAccounts.length===0">选中当前页</button>
|
||
<button class="btn btn-gray text-xs" style="padding:3px 9px" @click="deselectCurrentPage()"
|
||
:disabled="pagedAccounts.length===0 || !pagedAccounts.some(account => isSelected(account.email))">取消当前页选择</button>
|
||
</div>
|
||
<div class="account-pagination__controls">
|
||
<button class="page-icon-btn" title="第一页" @click="goToAccountPage(1)"
|
||
:disabled="accountCurrentPage <= 1">«</button>
|
||
<button class="page-icon-btn" title="上一页" @click="goToAccountPage(accountCurrentPage - 1)"
|
||
:disabled="accountCurrentPage <= 1">‹</button>
|
||
<span class="account-pagination__page" x-text="accountCurrentPage + ' / ' + accountTotalPages"></span>
|
||
<button class="page-icon-btn" title="下一页" @click="goToAccountPage(accountCurrentPage + 1)"
|
||
:disabled="accountCurrentPage >= accountTotalPages">›</button>
|
||
<button class="page-icon-btn" title="最后一页" @click="goToAccountPage(accountTotalPages)"
|
||
:disabled="accountCurrentPage >= accountTotalPages">»</button>
|
||
<select class="account-page-size" :value="accountPageSize" @change="setAccountPageSize($event.target.value)"
|
||
title="每页账号数">
|
||
<option value="50">50 / 页</option>
|
||
<option value="100">100 / 页</option>
|
||
<option value="200">200 / 页</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<!-- 右侧日志抽屉 -->
|
||
<div class="log-drawer" :class="{ 'open': logDrawerOpen }">
|
||
<div class="log-drawer-header">
|
||
<div class="flex items-center gap-2">
|
||
<span class="font-semibold text-gray-700 text-sm">实时日志</span>
|
||
<span class="text-xs text-gray-400" x-text="logs.length + ' 条'"></span>
|
||
</div>
|
||
<div class="flex gap-2">
|
||
<button @click="clearLogs()" class="btn btn-gray btn-sm">清空</button>
|
||
<button @click="toggleLogDrawer()" class="btn btn-gray btn-sm">收起</button>
|
||
</div>
|
||
</div>
|
||
<div class="log-drawer-body">
|
||
<template x-if="logDrawerOpen">
|
||
<div id="log-container">
|
||
<template x-for="(line, index) in logs" :key="index">
|
||
<div class="log-line" :class="logClass(line.type)" x-text="line.message"></div>
|
||
</template>
|
||
<div x-show="logs.length === 0" class="text-gray-500 text-center py-4"
|
||
style="font-family:inherit;font-size:12px">日志将在这里显示...</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
|
||
<!-- ===== 设置 ===== -->
|
||
<div x-show="activeTab === 'settings'" class="flex-1 overflow-y-auto">
|
||
<!-- 基础设置 + Cliproxy 并排 -->
|
||
<div class="settings-2col">
|
||
|
||
<!-- 基础设置 -->
|
||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||
<h3 class="font-semibold text-gray-700 mb-4">基础设置</h3>
|
||
|
||
<div class="space-y-4">
|
||
<!-- 比特浏览器设置 -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">比特浏览器端口</label>
|
||
<div class="flex gap-2">
|
||
<input type="number" :value="settings.bit_browser_port || '54345'"
|
||
@change="saveSetting('bit_browser_port', $event.target.value)" placeholder="54345" class="flex-1">
|
||
<button @click="checkBitStatus()" class="btn btn-gray">测试连接</button>
|
||
</div>
|
||
<div class="mt-1 flex items-center gap-2 text-xs">
|
||
<span class="status-dot" :class="bitStatus"></span>
|
||
<span x-text="bitStatus === 'online' ? '已连接' : (bitStatus === 'checking' ? '检测中...' : '未连接')"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 任务完成后关闭浏览器 -->
|
||
<div>
|
||
<label class="flex items-center gap-2 cursor-pointer">
|
||
<input type="checkbox" :checked="settings.close_browser_after_task !== '0'"
|
||
@change="saveSetting('close_browser_after_task', $event.target.checked ? '1' : '0')">
|
||
<span class="text-sm text-gray-700">任务完成后关闭浏览器窗口</span>
|
||
</label>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">ChatGPT 注册年龄</label>
|
||
<input type="number" min="18" max="99" :value="settings.chatgpt_age || '23'"
|
||
@change="saveSetting('chatgpt_age', Math.max(18, Math.min(99, parseInt($event.target.value || '23', 10) || 23)))">
|
||
<p class="text-xs text-gray-400 mt-1">Age 页面默认填写 23;Birthday 页面固定将年份改为 2000。</p>
|
||
</div>
|
||
|
||
<!-- 业务旁监听(开发调试) -->
|
||
<div>
|
||
<label class="flex items-center gap-2 cursor-pointer" title="勾选后,登录/绑卡等业务任务会自动写入旁监听日志">
|
||
<input type="checkbox" x-model="monitorEnabled" @change="onMonitorToggle()">
|
||
<span class="text-sm text-gray-700">业务旁监听</span>
|
||
</label>
|
||
<p class="text-xs text-gray-400 mt-1">开发调试用,勾选后业务任务会自动写入旁监听日志</p>
|
||
</div>
|
||
|
||
|
||
<!-- 本地 VPN 代理地址(测速中转) -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">本地VPN代理地址(测速中转,可选)</label>
|
||
<input type="text" :value="settings.upstream_proxy || ''"
|
||
@change="saveSetting('upstream_proxy', $event.target.value)" placeholder="http://127.0.0.1:7890">
|
||
<p class="text-xs text-gray-400 mt-1">填写后测速通过本地 VPN 全局代理中转,无需开启 TUN 模式。留空则直连。</p>
|
||
</div>
|
||
|
||
<!-- IP 欺诈度检查 -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">IP 欺诈度检查</label>
|
||
<div class="flex items-center gap-4">
|
||
<label class="inline-flex items-center gap-2">
|
||
<input type="checkbox" :checked="settings.fraud_check_enabled === '1'"
|
||
@change="saveSetting('fraud_check_enabled', $event.target.checked ? '1' : '0')">
|
||
<span class="text-sm">启用欺诈度检查</span>
|
||
</label>
|
||
<div class="flex items-center gap-1">
|
||
<span class="text-sm text-gray-500">阈值:</span>
|
||
<input type="number" :value="settings.fraud_score_threshold || '15'"
|
||
@change="saveSetting('fraud_score_threshold', $event.target.value)" min="1" max="100" class="w-20">
|
||
</div>
|
||
</div>
|
||
<p class="text-xs text-gray-400 mt-1">启用后初始化时通过 scamalytics.com 查询出口 IP 欺诈度分数,低于阈值才合格。需要 Python + cloudscraper。</p>
|
||
</div>
|
||
|
||
<!-- 英国资料卡片池 -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">英国资料卡片池</label>
|
||
<textarea :value="settings.uk_profile_cards || ''"
|
||
@change="saveSetting('uk_profile_cards', $event.target.value)"
|
||
placeholder="每行一张卡,格式: 卡号 月/年 CVV 5481087158441166 02/32 973 5481087188681666 02/32 435"
|
||
rows="4" class="w-full text-xs font-mono"></textarea>
|
||
<p class="text-xs text-gray-400 mt-1">「卡英国资料」专用卡片池,每行一张,格式: 卡号 月/年 CVV。运行时随机取一张。</p>
|
||
</div>
|
||
|
||
<!-- 打开限速 -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">打开限速(每秒最大打开/创建窗口数)</label>
|
||
<input type="number" :value="settings.ramp_up_rate || '9'"
|
||
@change="saveSetting('ramp_up_rate', $event.target.value)" min="1" max="50" class="w-32">
|
||
<p class="text-xs text-gray-400 mt-1">控制创建窗口、打开窗口、换代理的 API 调用频率。设100线程时建议9/s,12秒可安全启动全部。</p>
|
||
</div>
|
||
|
||
<!-- 人脸验证视频路径 -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">人脸验证视频路径</label>
|
||
<div class="flex gap-2">
|
||
<input type="text" :value="settings.face_video_path || ''"
|
||
@change="saveSetting('face_video_path', $event.target.value)"
|
||
placeholder="E:\videos\face.mp4" class="flex-1">
|
||
<label class="btn btn-gray cursor-pointer" style="display:inline-flex;align-items:center;gap:4px;">
|
||
选择
|
||
<input type="file" accept="video/*" style="display:none"
|
||
@change="if($event.target.files[0]){const p=$event.target.files[0].name; saveSetting('face_video_path', $event.target.files[0].path || p); $event.target.value=''}">
|
||
</label>
|
||
</div>
|
||
<p class="text-xs text-gray-400 mt-1">人脸验证使用的预录制视频文件绝对路径(.mp4/.webm/.mov),留空则不启用。</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mt-6 pt-4 border-t border-gray-100">
|
||
<h3 class="font-semibold text-gray-700 mb-2">数据库</h3>
|
||
<p class="text-sm text-gray-500">
|
||
数据库路径: <code class="bg-gray-100 px-1 rounded text-xs">data/accounts.db</code>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||
<h3 class="font-semibold text-gray-700 mb-4">代理配置</h3>
|
||
<div class="space-y-3">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">代理系统</label>
|
||
<select x-model="settings.proxy_provider" @change="saveSetting('proxy_provider', settings.proxy_provider)"
|
||
class="w-full">
|
||
<option value="cliproxy">Cliproxy</option>
|
||
<option value="nexip">Nexip</option>
|
||
<option value="cliapi">Cliapi (API取IP)</option>
|
||
<option value="nodemaven">nodemaven</option>
|
||
<option value="dataimpulse">dataimpulse</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- 代理预设 -->
|
||
<div class="border border-dashed border-gray-300 rounded-lg p-3 bg-gray-50">
|
||
<div class="flex items-center justify-between mb-2">
|
||
<span class="text-sm font-medium text-gray-600">⚡ 代理预设</span>
|
||
</div>
|
||
<!-- 保存当前配置 -->
|
||
<div class="flex gap-2 mb-2">
|
||
<input type="text" x-model="presetName" placeholder="输入预设名称..."
|
||
class="flex-1 text-sm" @keydown.enter="saveProxyPreset()">
|
||
<button @click="saveProxyPreset()" class="btn btn-gray flex-shrink-0 text-sm px-3 py-1">
|
||
💾 保存当前
|
||
</button>
|
||
</div>
|
||
<!-- 预设列表 -->
|
||
<template x-if="proxyPresets.length > 0">
|
||
<div class="space-y-1 max-h-40 overflow-y-auto">
|
||
<template x-for="(preset, idx) in proxyPresets" :key="idx">
|
||
<div class="flex items-center gap-2 bg-white rounded px-2 py-1.5 border border-gray-200 text-sm">
|
||
<span class="flex-1 truncate cursor-pointer hover:text-blue-600" @click="loadProxyPreset(preset)"
|
||
:title="'点击加载: ' + preset.name">
|
||
<span class="font-medium" x-text="preset.name"></span>
|
||
<span class="text-gray-400 text-xs ml-1" x-text="'(' + _getPresetProviderLabel(preset.config) + ' · ' + _getPresetSummary(preset.config) + ')'"></span>
|
||
</span>
|
||
<button @click="loadProxyPreset(preset)" class="text-blue-500 hover:text-blue-700 text-xs flex-shrink-0">加载</button>
|
||
<button @click="deleteProxyPreset(idx)" class="text-red-400 hover:text-red-600 text-xs flex-shrink-0">删除</button>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
<template x-if="proxyPresets.length === 0">
|
||
<p class="text-xs text-gray-400 text-center py-1">暂无预设,填好配置后点「保存当前」</p>
|
||
</template>
|
||
</div>
|
||
|
||
<div x-show="settings.proxy_provider === 'nodemaven'">
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">一键解析</label>
|
||
<div class="flex gap-2">
|
||
<input type="text" x-model="nodemavenFullStr" placeholder="粘贴完整代理字符串..." class="flex-1">
|
||
<button @click="parseNodemavenFull()" class="btn btn-gray flex-shrink-0">解析</button>
|
||
</div>
|
||
|
||
<label class="block text-sm font-medium text-gray-700 mb-1 mt-3">nodemaven 代理</label>
|
||
<input type="text" x-model="settings.nodemaven_proxy"
|
||
@change="saveSetting('nodemaven_proxy', settings.nodemaven_proxy)"
|
||
placeholder="host:port:username:password">
|
||
<p class="text-xs text-gray-400 mt-1">格式:host:port:username:password</p>
|
||
</div>
|
||
|
||
<div x-show="settings.proxy_provider === 'cliapi'">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">API 地址</label>
|
||
<input type="text" x-model="settings.cliapi_url"
|
||
@change="saveSetting('cliapi_url', settings.cliapi_url)"
|
||
placeholder="https://api.cliproxy.io/white/api">
|
||
<p class="text-xs text-gray-400 mt-1">请求后返回 IP:PORT 格式</p>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1 mt-3">地区</label>
|
||
<input type="text" x-model="settings.cliapi_region"
|
||
@change="saveSetting('cliapi_region', settings.cliapi_region)" placeholder="US">
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1 mt-3">数量 (num)</label>
|
||
<input type="number" x-model="settings.cliapi_num"
|
||
@change="saveSetting('cliapi_num', settings.cliapi_num)" min="1" max="100"
|
||
class="w-32" placeholder="1">
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1 mt-3">时长 (分钟)</label>
|
||
<input type="number" x-model="settings.cliapi_time"
|
||
@change="saveSetting('cliapi_time', settings.cliapi_time)" min="1" max="1440"
|
||
class="w-32" placeholder="30">
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1 mt-3">请求预览</label>
|
||
<div
|
||
class="bg-gray-50 border border-gray-200 rounded p-2 text-xs font-mono break-all text-gray-700 select-all"
|
||
x-text="cliApiPreview || '(填写上方字段后自动生成)'">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div x-show="settings.proxy_provider === 'dataimpulse'">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">主机</label>
|
||
<input type="text" value="gw.dataimpulse.com" disabled
|
||
class="w-full bg-gray-100 text-gray-500 cursor-not-allowed">
|
||
<p class="text-xs text-gray-400 mt-1">端口 10000-19999 随机分配</p>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1 mt-3">用户名</label>
|
||
<input type="text" x-model="settings.dataimpulse_username"
|
||
@change="saveSetting('dataimpulse_username', settings.dataimpulse_username)" placeholder="dataimpulse 用户名">
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1 mt-3">密码</label>
|
||
<input type="text" x-model="settings.dataimpulse_password"
|
||
@change="saveSetting('dataimpulse_password', settings.dataimpulse_password)" placeholder="dataimpulse 密码">
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1 mt-3">代理预览</label>
|
||
<div
|
||
class="bg-gray-50 border border-gray-200 rounded p-2 text-xs font-mono break-all text-gray-700 select-all"
|
||
x-text="dataimpulsePreview || '(填写上方字段后自动生成)'">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div x-show="['cliproxy', 'nexip'].includes(settings.proxy_provider || 'cliproxy')">
|
||
<div>
|
||
<div class="flex items-center justify-between gap-3 mb-1">
|
||
<label class="block text-sm font-medium text-gray-700"
|
||
x-text="settings.proxy_provider === 'nexip' ? 'Nexip 代理模板' : 'Cliproxy 代理模板'"></label>
|
||
<span x-show="settings.proxy_provider === 'nexip'" class="text-xs text-gray-500">每个账号最多测速 10 次</span>
|
||
</div>
|
||
<textarea x-model="settings.proxy_template"
|
||
@change="saveSetting('proxy_template', settings.proxy_template)"
|
||
:placeholder="settings.proxy_provider === 'nexip' ? 'us.nexip.cc:3010@USER-us-sid-{sid}-ttl-60:password' : 'hk.rotgb.711proxy.com:10000:USER-session-{sid}-sessTime-5:password'"
|
||
rows="2" class="w-full text-sm font-mono"></textarea>
|
||
<p class="text-xs text-gray-400 mt-1">
|
||
用 <code class="bg-gray-100 px-1 rounded">{sid}</code> 标记随机会话ID的位置,每次生成代理时自动替换;Nexip 支持 <code class="bg-gray-100 px-1 rounded">host:port@username:password</code>。
|
||
</p>
|
||
</div>
|
||
|
||
<div class="mt-3">
|
||
<label class="block text-sm font-medium text-gray-700 mb-2">SID 随机格式</label>
|
||
<div class="flex items-center gap-4 flex-wrap">
|
||
<label class="flex items-center gap-1.5 text-sm cursor-pointer">
|
||
<input type="checkbox"
|
||
:checked="settings.sid_use_letters !== '0' && settings.sid_use_letters !== false"
|
||
@change="settings.sid_use_letters = $event.target.checked ? '1' : '0'; saveSetting('sid_use_letters', settings.sid_use_letters)"
|
||
x-init="if (settings.sid_use_letters === undefined || settings.sid_use_letters === '') settings.sid_use_letters = true">
|
||
<span>字母 (a-z)</span>
|
||
</label>
|
||
<label class="flex items-center gap-1.5 text-sm cursor-pointer">
|
||
<input type="checkbox"
|
||
:checked="settings.sid_use_numbers !== '0' && settings.sid_use_numbers !== false"
|
||
@change="settings.sid_use_numbers = $event.target.checked ? '1' : '0'; saveSetting('sid_use_numbers', settings.sid_use_numbers)"
|
||
x-init="if (settings.sid_use_numbers === undefined || settings.sid_use_numbers === '') settings.sid_use_numbers = true">
|
||
<span>数字 (0-9)</span>
|
||
</label>
|
||
<div class="flex items-center gap-1.5">
|
||
<span class="text-sm">位数</span>
|
||
<input type="number" x-model="settings.sid_length"
|
||
@change="saveSetting('sid_length', settings.sid_length)"
|
||
min="4" max="32" class="w-16 text-sm text-center"
|
||
x-init="if (!settings.sid_length) settings.sid_length = '8'">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div x-show="settings.proxy_provider === 'nexip'"
|
||
class="mt-3 border-l-2 border-blue-400 bg-blue-50 px-3 py-2.5">
|
||
<div class="flex items-center gap-4 flex-wrap">
|
||
<label class="flex items-center gap-2 text-sm cursor-pointer">
|
||
<input type="checkbox" :checked="settings.nexip_sid_reuse_enabled === '1'"
|
||
@change="settings.nexip_sid_reuse_enabled = $event.target.checked ? '1' : '0'; saveSetting('nexip_sid_reuse_enabled', settings.nexip_sid_reuse_enabled)">
|
||
<span class="font-medium text-gray-700">复用已验证 SID</span>
|
||
</label>
|
||
<label class="flex items-center gap-2 text-sm text-gray-600">
|
||
<span class="whitespace-nowrap">分配账号数</span>
|
||
<input type="number" :value="settings.nexip_sid_reuse_count || '10'"
|
||
:disabled="settings.nexip_sid_reuse_enabled !== '1'"
|
||
@change="settings.nexip_sid_reuse_count = String(Math.max(1, Math.min(100, parseInt($event.target.value || '10', 10) || 10))); $event.target.value = settings.nexip_sid_reuse_count; saveSetting('nexip_sid_reuse_count', settings.nexip_sid_reuse_count)"
|
||
min="1" max="100" class="w-20 text-sm text-center disabled:bg-gray-100 disabled:text-gray-400">
|
||
</label>
|
||
</div>
|
||
<p class="text-xs text-blue-700 mt-1.5">一个 SID 测速合格后,在本次批量任务内最多分配给指定数量的账号;首次测速账号包含在内。</p>
|
||
</div>
|
||
|
||
<div class="mt-3">
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">代理预览</label>
|
||
<div
|
||
class="bg-gray-50 border border-gray-200 rounded p-2 text-xs font-mono break-all text-gray-700 select-all"
|
||
x-text="templateProxyPreview || '(填写模板后自动生成预览)'">
|
||
</div>
|
||
<button @click="refreshTemplatePreview()" class="text-xs text-blue-500 hover:text-blue-700 mt-1">🔄 刷新预览</button>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- 外部充值 API(独立加密存储,不进入通用 settings) -->
|
||
<div class="bg-white rounded-lg border border-gray-200 p-6 mt-4">
|
||
<div class="flex items-center justify-between gap-3 mb-4">
|
||
<div>
|
||
<h3 class="font-semibold text-gray-700">外部充值 API</h3>
|
||
<p class="text-xs text-gray-400 mt-1">用于卡付充值;凭据只保存在本机并由 Windows 当前用户加密保护。</p>
|
||
</div>
|
||
<span class="status-badge"
|
||
:class="rechargeCredentialMeta.configured ? 'status-登录成功' : 'status-未登录'"
|
||
x-text="rechargeCredentialMeta.configured ? '已配置' : '未配置'"></span>
|
||
</div>
|
||
<div x-show="rechargeCredentialMeta.configured" class="text-xs text-gray-500 mb-3">
|
||
当前 Key ID:<code class="bg-gray-100 px-1.5 py-0.5 rounded" x-text="rechargeCredentialMeta.keyIdMasked"></code>
|
||
<span x-show="rechargeCredentialMeta.updatedAt" class="ml-2"
|
||
x-text="'更新于 ' + formatDate(rechargeCredentialMeta.updatedAt)"></span>
|
||
</div>
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">API Key ID</label>
|
||
<input type="text" x-model="rechargeCredentialForm.keyId" autocomplete="off"
|
||
placeholder="填写 API Key ID" class="w-full">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">API Secret</label>
|
||
<input type="password" x-model="rechargeCredentialForm.secret" autocomplete="new-password"
|
||
placeholder="填写 API Secret" class="w-full" @keydown.enter.prevent="saveRechargeCredentials()">
|
||
</div>
|
||
</div>
|
||
<p class="text-xs text-gray-400 mt-2">保存或更新时需要同时填写两项;Secret 保存后不会再次显示或返回浏览器。</p>
|
||
<div class="flex gap-2 mt-3">
|
||
<button @click="saveRechargeCredentials()" :disabled="rechargeCredentialSaving"
|
||
class="btn btn-primary"
|
||
x-text="rechargeCredentialSaving ? '保存中...' : (rechargeCredentialMeta.configured ? '更新凭据' : '保存凭据')"></button>
|
||
<button x-show="rechargeCredentialMeta.configured" @click="deleteRechargeCredentials()"
|
||
:disabled="rechargeCredentialDeleting" class="btn btn-red"
|
||
x-text="rechargeCredentialDeleting ? '清除中...' : '清除凭据'"></button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Google 账号取号 API(独立加密存储,不进入通用 settings) -->
|
||
<div class="bg-white rounded-lg border border-gray-200 p-6 mt-4">
|
||
<div class="flex items-center justify-between gap-3 mb-4">
|
||
<div>
|
||
<h3 class="font-semibold text-gray-700">Google 账号取号 API</h3>
|
||
<p class="text-xs text-gray-400 mt-1">用于“链接提取+获取凭证”;API Key 只保存在本机,并由 Windows 当前用户加密保护。</p>
|
||
</div>
|
||
<span class="status-badge"
|
||
:class="googleAccountCredentialMeta.configured ? 'status-登录成功' : 'status-未登录'"
|
||
x-text="googleAccountCredentialMeta.configured ? '已配置' : '未配置'"></span>
|
||
</div>
|
||
<div x-show="googleAccountCredentialMeta.configured" class="text-xs text-gray-500 mb-3">
|
||
当前 Key:<code class="bg-gray-100 px-1.5 py-0.5 rounded" x-text="googleAccountCredentialMeta.keyMasked"></code>
|
||
<span class="ml-2" x-text="'默认存活 ' + googleAccountCredentialMeta.timelive + ' 分钟'"></span>
|
||
<span class="block mt-1">当前接口:<code class="bg-gray-100 px-1.5 py-0.5 rounded" x-text="googleAccountCredentialMeta.apiUrl"></code></span>
|
||
<span x-show="googleAccountCredentialMeta.updatedAt" class="ml-2"
|
||
x-text="'更新于 ' + formatDate(googleAccountCredentialMeta.updatedAt)"></span>
|
||
</div>
|
||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||
<div class="md:col-span-2">
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">API Key 或完整取号链接</label>
|
||
<input type="password" x-model="googleAccountCredentialForm.value" autocomplete="new-password"
|
||
placeholder="填写 key_value,或粘贴 api.php / apiv2.php / l0z/api.php 链接;已保存 Key 时可只填新接口地址"
|
||
class="w-full" @keydown.enter.prevent="saveGoogleAccountCredentials()">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">默认账号存活时间(分钟)</label>
|
||
<input type="number" min="1" max="43200" step="1"
|
||
x-model.number="googleAccountCredentialForm.timelive" class="w-full">
|
||
</div>
|
||
</div>
|
||
<p class="text-xs text-gray-400 mt-2">支持供应商 V1、V2 和 l0z 接口;系统会加密保存 Key 和所选接口。已保存 Key 时可只粘贴接口地址进行切换;留空可单独修改存活分钟。</p>
|
||
<div class="flex gap-2 mt-3">
|
||
<button @click="saveGoogleAccountCredentials()" :disabled="googleAccountCredentialSaving"
|
||
class="btn btn-primary"
|
||
x-text="googleAccountCredentialSaving ? '保存中...' : (googleAccountCredentialMeta.configured ? '更新配置' : '保存配置')"></button>
|
||
<button x-show="googleAccountCredentialMeta.configured" @click="deleteGoogleAccountCredentials()"
|
||
:disabled="googleAccountCredentialDeleting" class="btn btn-red"
|
||
x-text="googleAccountCredentialDeleting ? '清除中...' : '清除配置'"></button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 退款母号(独立加密存储,不进入通用 settings) -->
|
||
<div class="bg-white rounded-lg border border-gray-200 p-6 mt-4">
|
||
<div class="flex items-center justify-between gap-3 mb-4">
|
||
<div>
|
||
<h3 class="font-semibold text-gray-700">退款母号</h3>
|
||
<p class="text-xs text-gray-400 mt-1">填写母号登录内容和退款专用代理;两项都只保存在本机,并由 Windows 当前用户加密保护。</p>
|
||
</div>
|
||
<span class="status-badge"
|
||
:class="workspaceRefundCredentialMeta.configured ? 'status-登录成功' : 'status-未登录'"
|
||
x-text="workspaceRefundCredentialMeta.configured ? '已配置' : '未配置'"></span>
|
||
</div>
|
||
<div x-show="workspaceRefundCredentialMeta.configured" class="text-xs text-gray-500 mb-3">
|
||
当前母号:<code class="bg-gray-100 px-1.5 py-0.5 rounded" x-text="workspaceRefundCredentialMeta.ownerMasked"></code>
|
||
<span class="ml-2">退款代理:</span>
|
||
<code class="bg-gray-100 px-1.5 py-0.5 rounded"
|
||
x-text="workspaceRefundCredentialMeta.proxyConfigured ? workspaceRefundCredentialMeta.proxyMasked : '未配置'"></code>
|
||
<span x-show="workspaceRefundCredentialMeta.updatedAt" class="ml-2"
|
||
x-text="'更新于 ' + formatDate(workspaceRefundCredentialMeta.updatedAt)"></span>
|
||
</div>
|
||
<div class="grid grid-cols-1 gap-3">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">母号 ChatGPT Session / Access Token</label>
|
||
<input type="password" x-model="workspaceRefundCredentialForm.value" autocomplete="new-password"
|
||
placeholder="粘贴完整 Session JSON 或 eyJ... Access Token;已保存时可留空" class="w-full"
|
||
@keydown.enter.prevent="saveWorkspaceRefundCredentials()">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">退款专用代理池(每行一个)</label>
|
||
<textarea x-model="workspaceRefundCredentialForm.proxy" autocomplete="new-password" rows="5"
|
||
placeholder="socks5://用户名:密码@主机:端口 socks5://用户名:密码@主机:端口"
|
||
class="w-full font-mono text-xs"></textarea>
|
||
</div>
|
||
</div>
|
||
<p class="text-xs text-gray-400 mt-2">支持 SOCKS5、HTTP 及 host:port:user:pass,最多20条;连接失败、认证失败或 Cloudflare 403时自动切换。保存后令牌、代理用户名和密码都不会再次返回浏览器。</p>
|
||
<p class="text-xs text-amber-600 mt-1">执行退款会先移除母号团队中的全部非 Owner 成员,随后逐个邀请、迁移个人空间并移出团队;任务固定串行。</p>
|
||
<div class="flex gap-2 mt-3">
|
||
<button @click="saveWorkspaceRefundCredentials()" :disabled="workspaceRefundCredentialSaving"
|
||
class="btn btn-primary"
|
||
x-text="workspaceRefundCredentialSaving ? '保存中...' : (workspaceRefundCredentialMeta.configured ? '更新退款配置' : '保存退款配置')"></button>
|
||
<button x-show="workspaceRefundCredentialMeta.configured" @click="deleteWorkspaceRefundCredentials()"
|
||
:disabled="workspaceRefundCredentialDeleting" class="btn btn-red"
|
||
x-text="workspaceRefundCredentialDeleting ? '清除中...' : '清除母号'"></button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 飞书电子表格同步 -->
|
||
<div class="bg-white rounded-lg border border-gray-200 p-6 mt-4">
|
||
<div class="flex items-center justify-between gap-3 mb-4">
|
||
<div>
|
||
<h3 class="font-semibold text-gray-700">飞书电子表格同步</h3>
|
||
<p class="text-xs text-gray-400 mt-1">同步当前勾选账号;App Secret 只在本机加密保存,不会返回浏览器。</p>
|
||
</div>
|
||
<span class="status-badge"
|
||
:class="feishuCredentialMeta.configured ? 'status-登录成功' : 'status-未登录'"
|
||
x-text="feishuCredentialMeta.configured ? '凭据已配置' : '凭据未配置'"></span>
|
||
</div>
|
||
|
||
<div x-show="feishuCredentialMeta.configured" class="text-xs text-gray-500 mb-3">
|
||
当前 App ID:<code class="bg-gray-100 px-1.5 py-0.5 rounded" x-text="feishuCredentialMeta.appIdMasked"></code>
|
||
<span x-show="feishuCredentialMeta.updatedAt" class="ml-2"
|
||
x-text="'更新于 ' + formatDate(feishuCredentialMeta.updatedAt)"></span>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">FEISHU_APP_ID</label>
|
||
<input type="text" x-model="feishuCredentialForm.appId" autocomplete="off"
|
||
placeholder="cli_xxxxxxxxx" class="w-full">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">FEISHU_APP_SECRET</label>
|
||
<input type="password" x-model="feishuCredentialForm.appSecret" autocomplete="new-password"
|
||
placeholder="填写 App Secret" class="w-full" @keydown.enter.prevent="saveFeishuCredentials()">
|
||
</div>
|
||
</div>
|
||
<div class="flex gap-2 mt-3">
|
||
<button @click="saveFeishuCredentials()" :disabled="feishuCredentialSaving"
|
||
class="btn btn-primary"
|
||
x-text="feishuCredentialSaving ? '保存中...' : (feishuCredentialMeta.configured ? '更新凭据' : '保存凭据')"></button>
|
||
<button x-show="feishuCredentialMeta.configured" @click="deleteFeishuCredentials()"
|
||
:disabled="feishuCredentialDeleting" class="btn btn-red"
|
||
x-text="feishuCredentialDeleting ? '清除中...' : '清除凭据'"></button>
|
||
</div>
|
||
<p class="text-xs text-gray-400 mt-2">更新凭据时需要同时重新填写 App ID 和 App Secret。</p>
|
||
|
||
<div class="border-t border-gray-100 mt-5 pt-5 space-y-3">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">飞书电子表格地址(必填)</label>
|
||
<input type="url" x-model="settings.feishu_document_url"
|
||
@change="saveSetting('feishu_document_url', settings.feishu_document_url)"
|
||
placeholder="支持 /sheets/ 直链或知识库 /wiki/ 链接" class="w-full">
|
||
<p class="text-xs text-gray-400 mt-1">支持电子表格直链和知识库里的电子表格。已有邮箱按 Gmail 忽略点和大小写匹配;未找到会新增并继承上一数据行样式。</p>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">FEISHU_FOLDER_TOKEN(可选)</label>
|
||
<input type="text" x-model="settings.feishu_folder_token"
|
||
@change="saveSetting('feishu_folder_token', settings.feishu_folder_token)"
|
||
placeholder="fldcnxxxxxxxx" class="w-full">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">FEISHU_TENANT_DOMAIN(可选)</label>
|
||
<input type="url" x-model="settings.feishu_tenant_domain"
|
||
@change="saveSetting('feishu_tenant_domain', settings.feishu_tenant_domain)"
|
||
placeholder="https://你的企业域名.feishu.cn" class="w-full">
|
||
</div>
|
||
</div>
|
||
<p class="text-xs text-gray-400">同步已有电子表格时使用上面的表格地址;文件夹 Token 和企业域名先保留为可选配置,不参与本次写入。</p>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">同步完成通知</label>
|
||
<select x-model="settings.feishu_notify_mode"
|
||
@change="saveSetting('feishu_notify_mode', settings.feishu_notify_mode)" class="w-full settings-select">
|
||
<option value="none">不发送通知</option>
|
||
<option value="private">机器人私发</option>
|
||
<option value="group">发送到群</option>
|
||
</select>
|
||
</div>
|
||
<div x-show="settings.feishu_notify_mode === 'private'">
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">私发目标类型</label>
|
||
<select x-model="settings.feishu_notify_receive_id_type"
|
||
@change="saveSetting('feishu_notify_receive_id_type', settings.feishu_notify_receive_id_type)" class="w-full">
|
||
<option value="email">飞书账号邮箱</option>
|
||
<option value="open_id">Open ID</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div x-show="settings.feishu_notify_mode === 'private'">
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">私发目标</label>
|
||
<input type="text" x-model="settings.feishu_notify_receive_id"
|
||
@change="saveSetting('feishu_notify_receive_id', settings.feishu_notify_receive_id)"
|
||
placeholder="填写飞书账号邮箱或 Open ID" class="w-full">
|
||
</div>
|
||
<div x-show="settings.feishu_notify_mode === 'group'">
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">FEISHU_CHAT_ID</label>
|
||
<input type="text" x-model="settings.feishu_notify_receive_id"
|
||
@change="saveSetting('feishu_notify_receive_id', settings.feishu_notify_receive_id)"
|
||
placeholder="oc_xxxxxxxxx" class="w-full">
|
||
</div>
|
||
|
||
<div class="rounded border border-blue-100 bg-blue-50 p-3 text-xs text-blue-700 leading-5">
|
||
字段固定映射:邮件、密码、辅助邮件、2FA密钥、是否已申请退款(是否已发起退款流程)、是否已退款(账号退款)、AC(完整 ChatGPT Session)。
|
||
本地没有 ChatGPT Session 也可以同步:本地 AC 为空时保留飞书原值,不会用空值覆盖;本地 AC 有值时会覆盖飞书原值。账号退款为空或“无状态”时,在飞书文档中写为“未操作”;飞书“是否已退款”已有“已退款”时会保留,不被本地状态覆盖。充值时间仅在本地有值且飞书对应单元格为空时写入;已有值绝不覆盖。“最后同步”不会读取或写入,去处、套餐、限流等其他列也全部保持不变。
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 已充值提取账号额度监控与自动退款 -->
|
||
<div class="bg-white rounded-lg border border-gray-200 p-6 mt-4">
|
||
<div class="flex items-center justify-between gap-3 mb-4">
|
||
<div>
|
||
<h3 class="font-semibold text-gray-700">额度监控与自动退款</h3>
|
||
<p class="text-xs text-gray-400 mt-1">额度任务按设置间隔查询;退款截止时间由独立任务每 15 秒本地检查。额度达到阈值或截止时间到达,任一条件满足都会进入同一个单路退款队列。</p>
|
||
</div>
|
||
<span class="status-badge"
|
||
:class="quotaMonitorStatus.running ? 'status-充值中' : (settings.quota_monitor_enabled === '1' ? 'status-登录成功' : 'status-未登录')"
|
||
x-text="quotaMonitorStatus.running ? '执行中' : (settings.quota_monitor_enabled === '1' ? '已开启' : '未开启')"></span>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-3 xl:grid-cols-8 gap-3 items-end">
|
||
<label class="flex items-center gap-2 h-10 text-sm text-gray-700">
|
||
<input type="checkbox" :checked="settings.quota_monitor_enabled === '1'"
|
||
@change="saveQuotaMonitorSetting('quota_monitor_enabled', $event.target.checked ? '1' : '0')">
|
||
开启定时任务
|
||
</label>
|
||
<label class="flex items-center gap-2 h-10 text-sm text-gray-700" title="跳过上次套餐状态已经明确为 Token失效(401) 的账号;关闭后可重新查询">
|
||
<input type="checkbox" :checked="settings.quota_monitor_skip_401_enabled === '1'"
|
||
@change="saveQuotaMonitorSetting('quota_monitor_skip_401_enabled', $event.target.checked ? '1' : '0')">
|
||
跳过明确的 401
|
||
</label>
|
||
<label class="flex items-center gap-2 h-10 text-sm text-gray-700">
|
||
<input type="checkbox" :checked="settings.quota_monitor_auto_refund_enabled === '1'"
|
||
@change="saveQuotaMonitorSetting('quota_monitor_auto_refund_enabled', $event.target.checked ? '1' : '0')">
|
||
开启自动退款(阈值或截止时间)
|
||
</label>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">执行间隔</label>
|
||
<input type="number" :min="settings.quota_monitor_interval_unit === 'minutes' ? 10 : 1" max="10000"
|
||
:value="settings.quota_monitor_interval_value || '24'"
|
||
@change="saveQuotaMonitorSetting('quota_monitor_interval_value', String(Math.max(settings.quota_monitor_interval_unit === 'minutes' ? 10 : 1, parseInt($event.target.value || '24', 10) || 24)))"
|
||
class="w-full">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">间隔单位</label>
|
||
<select x-model="settings.quota_monitor_interval_unit"
|
||
@change="saveQuotaMonitorSetting('quota_monitor_interval_unit', settings.quota_monitor_interval_unit)" class="w-full settings-select">
|
||
<option value="minutes">分钟</option>
|
||
<option value="hours">小时</option>
|
||
<option value="days">天</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">查询并发(1~10)</label>
|
||
<input type="number" min="1" max="10" :value="settings.quota_monitor_concurrency || '2'"
|
||
@change="saveQuotaMonitorSetting('quota_monitor_concurrency', String(Math.max(1, Math.min(10, parseInt($event.target.value || '2', 10) || 2))))"
|
||
class="w-full">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">用量阈值(%)</label>
|
||
<input type="number" min="0" max="100" step="1" :value="settings.quota_monitor_threshold || '100'"
|
||
@change="saveQuotaMonitorSetting('quota_monitor_threshold', String(Math.max(0, Math.min(100, Number($event.target.value || '100')))))"
|
||
class="w-full">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">截止检查间隔(秒)</label>
|
||
<input type="number" min="5" max="3600" step="1"
|
||
:value="settings.refund_deadline_check_interval_seconds || '15'"
|
||
@change="saveQuotaMonitorSetting('refund_deadline_check_interval_seconds', String(Math.max(5, Math.min(3600, parseInt($event.target.value || '15', 10) || 15))))"
|
||
class="w-full">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex flex-wrap items-center gap-3 mt-4">
|
||
<button class="btn btn-green" @click="runQuotaMonitorNow()" :disabled="quotaMonitorStatus.running"
|
||
x-text="quotaMonitorStatus.running
|
||
? (quotaMonitorProgress.phase === 'refund'
|
||
? ('退款中 ' + quotaMonitorProgress.refundCurrent + '/' + quotaMonitorProgress.refundTotal)
|
||
: ('查询中 ' + quotaMonitorProgress.current + '/' + quotaMonitorProgress.total))
|
||
: (settings.quota_monitor_auto_refund_enabled === '1' ? '立即查询并退款' : '立即查询一次')"></button>
|
||
<span x-show="quotaMonitorStatusText" x-cloak class="text-xs"
|
||
:class="quotaMonitorStatusType === 'error' ? 'text-red-600' : (quotaMonitorStatusType === 'success' ? 'text-green-600' : (quotaMonitorStatusType === 'warn' ? 'text-orange-600' : 'text-blue-600'))"
|
||
x-text="quotaMonitorStatusText"></span>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mt-4 text-xs text-gray-600">
|
||
<div class="rounded border border-gray-100 bg-gray-50 p-3 leading-6">
|
||
<div>下次执行:<span x-text="quotaMonitorStatus.nextRunAt ? formatDate(quotaMonitorStatus.nextRunAt) : (settings.quota_monitor_enabled === '1' ? '计算中' : '未安排')"></span></div>
|
||
<div x-show="quotaMonitorStatus.running && quotaMonitorProgress.phase !== 'refund'">额度查询:<span x-text="quotaMonitorProgress.current + '/' + quotaMonitorProgress.total"></span></div>
|
||
<div x-show="quotaMonitorStatus.running && quotaMonitorProgress.phase === 'refund'">自动退款:<span x-text="quotaMonitorProgress.refundCurrent + '/' + quotaMonitorProgress.refundTotal"></span>,成功 <span x-text="quotaMonitorProgress.refundSuccessCount"></span>,失败 <span x-text="quotaMonitorProgress.refundFailureCount"></span></div>
|
||
<div>用量阈值:主窗口已用比例 ≥ <span x-text="settings.quota_monitor_threshold || '100'"></span>%</div>
|
||
<div>明确 401:<span x-text="settings.quota_monitor_skip_401_enabled === '1' ? '跳过查询' : '继续查询'"></span></div>
|
||
<div>退款并发:固定 1 路</div>
|
||
<div>截止时间检查:<span x-text="settings.quota_monitor_auto_refund_enabled === '1' ? ('每 ' + (settings.refund_deadline_check_interval_seconds || '15') + ' 秒检查') : '未开启'"></span></div>
|
||
</div>
|
||
<div class="rounded border border-gray-100 bg-gray-50 p-3 leading-6" x-show="quotaMonitorStatus.lastRun">
|
||
<div>上次完成:<span x-text="quotaMonitorStatus.lastRun && quotaMonitorStatus.lastRun.finished_at ? formatDate(quotaMonitorStatus.lastRun.finished_at) : '未完成'"></span></div>
|
||
<div>
|
||
检查 <span x-text="quotaMonitorStatus.lastRun ? quotaMonitorStatus.lastRun.eligible_count : 0"></span>,
|
||
达到阈值 <span x-text="quotaMonitorStatus.lastRun ? quotaMonitorStatus.lastRun.threshold_count : 0"></span>
|
||
(新增 <span x-text="quotaMonitorStatus.lastRun ? quotaMonitorStatus.lastRun.threshold_new_count : 0"></span>),
|
||
失败 <span x-text="quotaMonitorStatus.lastRun ? quotaMonitorStatus.lastRun.failure_count : 0"></span>
|
||
(新增 <span x-text="quotaMonitorStatus.lastRun ? quotaMonitorStatus.lastRun.failure_new_count : 0"></span>)
|
||
</div>
|
||
<div>明确 401 跳过:<span x-text="quotaMonitorStatus.lastRun ? (quotaMonitorStatus.lastRun.skipped_401_count || 0) : 0"></span></div>
|
||
<div x-show="quotaMonitorStatus.lastRun && Number(quotaMonitorStatus.lastRun.auto_refund_enabled) === 1">
|
||
退款候选 <span x-text="quotaMonitorStatus.lastRun ? quotaMonitorStatus.lastRun.refund_candidate_count : 0"></span>,
|
||
成功 <span x-text="quotaMonitorStatus.lastRun ? quotaMonitorStatus.lastRun.refund_success_count : 0"></span>,
|
||
失败 <span x-text="quotaMonitorStatus.lastRun ? quotaMonitorStatus.lastRun.refund_failure_count : 0"></span>,
|
||
跳过 <span x-text="quotaMonitorStatus.lastRun ? quotaMonitorStatus.lastRun.refund_skipped_count : 0"></span>
|
||
</div>
|
||
<div>飞书发送:<span x-text="quotaMonitorStatus.lastRun && quotaMonitorStatus.lastRun.delivery_status === 'sent' ? '成功' : (quotaMonitorStatus.lastRun && quotaMonitorStatus.lastRun.delivery_status === 'failed' ? '失败' : '未发送')"></span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="rounded border border-orange-100 bg-orange-50 p-3 text-xs text-orange-700 leading-5 mt-4">
|
||
“账号退款”为“已退款”的账号会直接跳过,不查询用量、不自动退款,也不会修改“账号退款”字段。自动退款只更新独立的“是否已发起退款流程”;成功改为“已发起”,失败改为“发起失败”。退款截止时间和额度阈值是“或”关系,未来截止时间不会阻止额度达标退款;共享退款通道忙时会排队等待,不再丢弃本轮任务。同一邮箱提交前会重新校验状态,避免重复退款。飞书通知已精简为结果总览,详细账号和异常原因统一放在 CSV。
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 静态代理池 -->
|
||
<div class="bg-white rounded-lg border border-gray-200 p-6 mt-4">
|
||
<div class="flex items-center justify-between mb-4">
|
||
<h3 class="font-semibold text-gray-700">静态代理池</h3>
|
||
<div class="flex items-center gap-2 text-xs text-gray-500">
|
||
<span>总数: <b x-text="staticProxies.length"></b></span>
|
||
<span>空闲: <b class="text-green-600" x-text="staticProxies.filter(p=>p.usage_count<p.max_usage).length"></b></span>
|
||
<span>已满: <b class="text-orange-500" x-text="staticProxies.filter(p=>p.usage_count>=p.max_usage).length"></b></span>
|
||
</div>
|
||
</div>
|
||
<div class="space-y-3">
|
||
<!-- 导入 -->
|
||
<div>
|
||
<label class="block text-xs font-medium text-gray-600 mb-1">导入代理(每行一个 host:port:user:pass)</label>
|
||
<textarea x-model="staticProxyImportText" rows="3"
|
||
placeholder="host:port:user:pass host:port:user:pass"
|
||
class="w-full text-xs border rounded p-2 font-mono"></textarea>
|
||
<div class="flex items-center gap-2 mt-2">
|
||
<label class="text-xs text-gray-500">可用次数:</label>
|
||
<input type="number" x-model.number="staticProxyMaxUsage" min="1" max="100"
|
||
class="w-16 text-xs border rounded px-2 py-1">
|
||
<button @click="importStaticProxies()" class="btn btn-primary text-xs">导入</button>
|
||
<button @click="clearAllStaticProxies()" class="btn btn-red text-xs"
|
||
x-show="staticProxies.length > 0">清空</button>
|
||
</div>
|
||
</div>
|
||
<!-- 列表 -->
|
||
<div x-show="staticProxies.length > 0" style="max-height:240px;overflow:auto">
|
||
<table class="data-table" style="font-size:11px">
|
||
<thead><tr>
|
||
<th>ID</th><th>代理地址</th><th>状态</th><th>使用/上限</th><th>绑定账号</th><th>操作</th>
|
||
</tr></thead>
|
||
<tbody>
|
||
<template x-for="p in staticProxies" :key="p.id">
|
||
<tr :class="!p.is_active ? 'opacity-40' : ''">
|
||
<td x-text="p.id"></td>
|
||
<td class="font-mono" style="max-width:220px;overflow:hidden;text-overflow:ellipsis"
|
||
x-text="p.proxy_str" :title="p.proxy_str"></td>
|
||
<td>
|
||
<span :class="p.is_active ? 'text-green-600' : 'text-gray-400'"
|
||
x-text="p.is_active ? '启用' : '禁用'"></span>
|
||
</td>
|
||
<td>
|
||
<span class="cursor-pointer hover:underline"
|
||
:class="p.usage_count >= p.max_usage ? 'text-orange-500' : 'text-green-600'"
|
||
@click="editStaticProxyMaxUsage(p)"
|
||
:title="'点击修改上限'"
|
||
x-text="p.usage_count + '/' + p.max_usage"></span>
|
||
</td>
|
||
<td style="max-width:180px;overflow:hidden;text-overflow:ellipsis;font-size:10px"
|
||
x-text="p.bound_emails || '-'" :title="p.bound_emails"></td>
|
||
<td class="whitespace-nowrap">
|
||
<button @click="toggleStaticProxy(p.id)"
|
||
class="text-xs ml-1"
|
||
:class="p.is_active ? 'text-orange-500 hover:text-orange-700' : 'text-green-500 hover:text-green-700'"
|
||
x-text="p.is_active ? '禁用' : '启用'"></button>
|
||
<button x-show="p.usage_count > 0" @click="releaseStaticProxy(p.id)"
|
||
class="text-blue-500 hover:text-blue-700 text-xs ml-1">释放</button>
|
||
<button @click="deleteStaticProxy(p.id)"
|
||
class="text-red-400 hover:text-red-600 text-xs ml-1">删除</button>
|
||
</td>
|
||
</tr>
|
||
</template>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- ===== 日志 ===== -->
|
||
<div x-show="activeTab === 'logs'" class="flex-1 overflow-y-auto">
|
||
<div class="bg-white rounded-lg border border-gray-200 p-4">
|
||
<div class="flex items-center justify-between gap-3 flex-wrap">
|
||
<div>
|
||
<div class="text-base font-semibold text-gray-800">诊断日志</div>
|
||
<div class="text-xs text-gray-500 mt-0.5">用于排查登录/更改2FA等任务失败原因(网络、超时、DOM异常、页面报错等)</div>
|
||
</div>
|
||
<div class="text-xs text-gray-400">
|
||
<span x-text="diagEmail ? ('当前账号:' + diagEmail) : '请选择账号'"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mt-3 flex flex-wrap items-center gap-3">
|
||
<div class="flex items-center gap-2">
|
||
<span class="text-sm text-gray-600">账号</span>
|
||
<input type="text" x-model="diagEmailSearch" placeholder="搜索账号..." class="text-sm" style="min-width:180px">
|
||
<select x-model="diagEmail" @change="onDiagEmailChange()" class="text-sm" style="min-width:260px">
|
||
<option value="">请选择账号</option>
|
||
<template x-for="e in filteredDiagAccounts()" :key="e">
|
||
<option :value="e" x-text="e"></option>
|
||
</template>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="flex items-center gap-2">
|
||
<span class="text-sm text-gray-600">日期</span>
|
||
<select x-model="diagDate" @change="diagOffset=0; loadDiagEvents()" class="text-sm" style="min-width:140px">
|
||
<template x-for="d in diagDates" :key="d">
|
||
<option :value="d" x-text="d"></option>
|
||
</template>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="flex items-center gap-2">
|
||
<span class="text-sm text-gray-600">类型</span>
|
||
<select x-model="diagTypeFilter" class="text-sm" style="min-width:160px">
|
||
<option value="">全部</option>
|
||
<option value="error">错误</option>
|
||
<option value="exception">异常</option>
|
||
<option value="requestfailed">请求失败</option>
|
||
<option value="response">响应异常</option>
|
||
<option value="console">控制台错误</option>
|
||
<option value="pageerror">页面异常</option>
|
||
<option value="checkpoint">关键节点</option>
|
||
<option value="navigate">页面跳转</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="flex items-center gap-2">
|
||
<span class="text-sm text-gray-600">任务</span>
|
||
<select x-model="diagTaskFilter" class="text-sm" style="min-width:140px">
|
||
<option value="">全部</option>
|
||
<option value="login">登录</option>
|
||
<option value="change_2fa">更改2FA</option>
|
||
<option value="change_password">更改密码</option>
|
||
<option value="chatgpt_register">ChatGPT注册/登录</option>
|
||
<option value="change_country">更改国家</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="flex items-center gap-2 flex-1 min-w-[220px]">
|
||
<input type="text" x-model="diagSearch" placeholder="搜索 信息/URL/步骤..." class="text-sm flex-1"
|
||
style="min-width:220px">
|
||
<button class="btn btn-gray" @click="loadDiagEvents()" :disabled="diagLoading || !diagEmail">刷新</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mt-3 flex items-center gap-2 flex-wrap">
|
||
<span class="text-sm text-gray-600">快捷筛选</span>
|
||
<button class="btn btn-gray" style="padding:2px 10px;font-size:12px"
|
||
:class="diagQuickFilter==='errors' ? 'active' : ''"
|
||
@click="diagQuickFilter = (diagQuickFilter==='errors' ? '' : 'errors')">仅错误</button>
|
||
<button class="btn btn-gray" style="padding:2px 10px;font-size:12px"
|
||
:class="diagQuickFilter==='network' ? 'active' : ''"
|
||
@click="diagQuickFilter = (diagQuickFilter==='network' ? '' : 'network')">仅网络</button>
|
||
<button class="btn btn-gray" style="padding:2px 10px;font-size:12px"
|
||
:class="diagQuickFilter==='checkpoint' ? 'active' : ''"
|
||
@click="diagQuickFilter = (diagQuickFilter==='checkpoint' ? '' : 'checkpoint')">仅关键节点</button>
|
||
<button class="btn btn-gray" style="padding:2px 10px;font-size:12px" @click="diagQuickFilter=''">清除</button>
|
||
</div>
|
||
|
||
<div class="mt-3 bg-gray-50 border border-gray-200 rounded p-3" x-show="diagEmail && !diagLoading">
|
||
<div class="text-sm font-medium text-gray-700">本页汇总</div>
|
||
<div class="mt-1 text-xs text-gray-600 flex items-center gap-2 flex-wrap">
|
||
<template x-if="diagFinalReason()">
|
||
<div class="flex items-center gap-2">
|
||
<span class="px-2 py-0.5 rounded text-xs font-medium" :class="diagTypeClass(diagFinalReason().type)"
|
||
x-text="diagTypeLabel(diagFinalReason().type)"></span>
|
||
<span class="text-gray-800 break-all" x-text="diagSummary(diagFinalReason())"></span>
|
||
<button class="btn btn-gray" style="padding:2px 8px;font-size:12px"
|
||
x-show="!!(diagFinalReason().screenshotPath)"
|
||
@click="viewScreenshotPath(diagFinalReason().screenshotPath, diagEmail)">查看截图</button>
|
||
</div>
|
||
</template>
|
||
<template x-if="!diagFinalReason()">
|
||
<div class="text-gray-500">未发现明显错误事件(可切换日期或取消筛选查看更多)。</div>
|
||
</template>
|
||
</div>
|
||
<div class="mt-1 text-xs text-gray-500" x-show="diagFinalHint()" x-text="'建议:' + diagFinalHint()"></div>
|
||
</div>
|
||
|
||
<div class="mt-3 text-xs text-gray-500 flex items-center justify-between">
|
||
<div>
|
||
<span x-show="diagLoading">加载中...</span>
|
||
<span x-show="!diagLoading"
|
||
x-text="diagEmail ? ('共 ' + filteredDiagEvents().length + ' 条') : '请选择账号查看'"></span>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<button class="btn btn-gray" @click="diagPrevPage()" :disabled="diagLoading || diagOffset===0">上一页</button>
|
||
<button class="btn btn-gray" @click="diagNextPage()"
|
||
:disabled="diagLoading || (diagOffset + diagLimit) >= diagTotal">下一页</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mt-3 overflow-auto border border-gray-100 rounded diag-table">
|
||
<table class="min-w-full text-sm">
|
||
<thead class="bg-gray-50 diag-thead">
|
||
<tr class="text-left text-xs text-gray-600">
|
||
<th class="px-3 py-2" style="min-width:170px">时间</th>
|
||
<th class="px-3 py-2" style="min-width:120px">类型</th>
|
||
<th class="px-3 py-2" style="min-width:120px">任务</th>
|
||
<th class="px-3 py-2" style="min-width:140px">步骤</th>
|
||
<th class="px-3 py-2">摘要</th>
|
||
<th class="px-3 py-2" style="min-width:90px">截图</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<template x-for="ev in filteredDiagEvents()"
|
||
:key="(ev.ts||'') + '|' + (ev.type||'') + '|' + (ev.url||'') + '|' + (ev.name||'') + '|' + (ev.message||'')">
|
||
<tr class="border-t border-gray-100 hover:bg-gray-50 cursor-pointer" @click="selectDiagEvent(ev)">
|
||
<td class="px-3 py-2 whitespace-nowrap" x-text="formatDiagTs(ev.ts)"></td>
|
||
<td class="px-3 py-2 whitespace-nowrap">
|
||
<span class="px-2 py-0.5 rounded text-xs font-medium" :class="diagTypeClass(ev.type)"
|
||
x-text="diagTypeLabel(ev.type)"></span>
|
||
</td>
|
||
<td class="px-3 py-2 whitespace-nowrap" x-text="diagTaskLabel(ev.taskType)"></td>
|
||
<td class="px-3 py-2 whitespace-nowrap" x-text="ev.step || '-'"></td>
|
||
<td class="px-3 py-2 truncate" x-text="diagSummary(ev)"></td>
|
||
<td class="px-3 py-2">
|
||
<button class="btn btn-gray" style="padding:2px 8px;font-size:12px" x-show="!!(ev.screenshotPath)"
|
||
@click.stop="viewScreenshotPath(ev.screenshotPath, diagEmail)">查看</button>
|
||
</td>
|
||
</tr>
|
||
</template>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<template x-if="diagSelected">
|
||
<div class="mt-3">
|
||
<div class="bg-gray-50 border border-gray-200 rounded p-3">
|
||
<div class="text-sm font-medium text-gray-700 flex items-center justify-between">
|
||
<span>详情</span>
|
||
<button class="btn btn-gray" style="padding:2px 8px;font-size:12px" @click="diagSelected=null">关闭</button>
|
||
</div>
|
||
<div class="mt-2 text-xs text-gray-600 space-y-1">
|
||
<div><span class="font-medium">时间:</span> <span x-text="formatDiagTs(diagSelected.ts)"></span></div>
|
||
<div><span class="font-medium">类型:</span> <span x-text="diagTypeLabel(diagSelected.type)"></span></div>
|
||
<div><span class="font-medium">任务:</span> <span x-text="diagTaskLabel(diagSelected.taskType)"></span>
|
||
</div>
|
||
<div><span class="font-medium">步骤:</span> <span x-text="diagSelected.step || '-'"></span></div>
|
||
<div><span class="font-medium">URL:</span> <span class="break-all"
|
||
x-text="diagSelected.url || '-'"></span></div>
|
||
<div><span class="font-medium">信息:</span> <span class="break-all"
|
||
x-text="diagSelected.message || '-'"></span></div>
|
||
<div x-show="diagSelected.name"><span class="font-medium">节点:</span> <span
|
||
x-text="diagSelected.name"></span></div>
|
||
<div x-show="diagSelected.screenshotPath"><span class="font-medium">截图:</span> <span class="break-all"
|
||
x-text="diagSelected.screenshotPath"></span></div>
|
||
<div x-show="diagSelected.meta"><span class="font-medium">元数据:</span>
|
||
<pre class="bg-white border border-gray-200 rounded p-2 overflow-auto"
|
||
x-text="JSON.stringify(diagSelected.meta, null, 2)"></pre>
|
||
</div>
|
||
<div x-show="diagSelected.stack"><span class="font-medium">堆栈:</span>
|
||
<pre class="bg-white border border-gray-200 rounded p-2 overflow-auto"
|
||
x-text="diagSelected.stack"></pre>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
|
||
|
||
<!-- ===== 一次性导出问题号模态框 ===== -->
|
||
<div x-show="showProblemExportModal" x-cloak class="modal-backdrop" @click.self="showProblemExportModal=false">
|
||
<div class="modal-box modal-compact">
|
||
<h3 class="text-base font-semibold text-gray-800 mb-3">导出问题号</h3>
|
||
|
||
<div class="space-y-3">
|
||
<div>
|
||
<label class="block text-xs font-medium text-gray-600 mb-2">选择要导出的类别(默认全选)</label>
|
||
<div class="space-y-2">
|
||
<label class="flex items-center justify-between gap-3 border border-gray-200 rounded px-3 py-2 cursor-pointer">
|
||
<span class="flex items-center gap-2 text-sm">
|
||
<input type="checkbox" x-model="problemExportCategories.password">
|
||
<span>密码错误</span>
|
||
</span>
|
||
<span class="text-xs text-gray-500" x-text="problemExportCount('password') + ' 个'"></span>
|
||
</label>
|
||
<label class="flex items-center justify-between gap-3 border border-gray-200 rounded px-3 py-2 cursor-pointer">
|
||
<span class="flex items-center gap-2 text-sm">
|
||
<input type="checkbox" x-model="problemExportCategories.twofa">
|
||
<span>2FA错误</span>
|
||
</span>
|
||
<span class="text-xs text-gray-500" x-text="problemExportCount('twofa') + ' 个'"></span>
|
||
</label>
|
||
<label class="flex items-center justify-between gap-3 border border-gray-200 rounded px-3 py-2 cursor-pointer">
|
||
<span class="flex items-center gap-2 text-sm">
|
||
<input type="checkbox" x-model="problemExportCategories.captcha">
|
||
<span>人机认证</span>
|
||
</span>
|
||
<span class="text-xs text-gray-500" x-text="problemExportCount('captcha') + ' 个'"></span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="bg-gray-50 border border-gray-200 rounded p-2 text-xs text-gray-500 leading-5">
|
||
将生成一个 TXT 文件,账号一行一个,类别之间空一行。<br>
|
||
新导入账号使用导入时的原始整行;旧数据如未保留原始行,将按当前保存的账号四字段导出。
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex gap-2 mt-3">
|
||
<button @click="showProblemExportModal=false" class="btn btn-red">取消</button>
|
||
<button @click="confirmProblemExport()" class="btn btn-primary flex-1">导出 TXT</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== 导出账号模态框 ===== -->
|
||
<div x-show="showExportModal" x-cloak class="modal-backdrop">
|
||
<div class="modal-box modal-compact">
|
||
<h3 class="text-base font-semibold text-gray-800 mb-3">
|
||
导出账号
|
||
<span class="text-xs font-normal text-gray-400 ml-2" x-text="'共 ' + _exportList.length + ' 个'"></span>
|
||
</h3>
|
||
|
||
<div class="space-y-3">
|
||
<!-- 字段选择 -->
|
||
<div>
|
||
<label class="block text-xs font-medium text-gray-600 mb-2">选择导出字段</label>
|
||
<div class="grid grid-cols-2 gap-1">
|
||
<label class="flex items-center gap-2 text-sm cursor-pointer">
|
||
<input type="checkbox" x-model="exportFields.email"> 邮箱
|
||
</label>
|
||
<label class="flex items-center gap-2 text-sm cursor-pointer">
|
||
<input type="checkbox" x-model="exportFields.password"> 密码
|
||
</label>
|
||
<label class="flex items-center gap-2 text-sm cursor-pointer">
|
||
<input type="checkbox" x-model="exportFields.recovery_email"> 辅助邮箱
|
||
</label>
|
||
<label class="flex items-center gap-2 text-sm cursor-pointer">
|
||
<input type="checkbox" x-model="exportFields.secret_key"> 2FA密钥
|
||
</label>
|
||
<label class="flex items-center gap-2 text-sm cursor-pointer">
|
||
<input type="checkbox" x-model="exportFields.country"> 国家
|
||
</label>
|
||
<label class="flex items-center gap-2 text-sm cursor-pointer">
|
||
<input type="checkbox" x-model="exportFields.customer_name"> 客户名
|
||
</label>
|
||
<label class="flex items-center gap-2 text-sm cursor-pointer">
|
||
<input type="checkbox" x-model="exportFields.account_refund_status"> 账号退款
|
||
</label>
|
||
<label class="flex items-center gap-2 text-sm cursor-pointer">
|
||
<input type="checkbox" x-model="exportFields.refund_deadline_at"> 退款截止时间
|
||
</label>
|
||
<label class="flex items-center gap-2 text-sm cursor-pointer">
|
||
<input type="checkbox" x-model="exportFields.sheerid_link"> SheerID链接
|
||
</label>
|
||
<label class="flex items-center gap-2 text-sm cursor-pointer">
|
||
<input type="checkbox" x-model="exportFields.status"> 状态
|
||
</label>
|
||
<label class="flex items-center gap-2 text-sm cursor-pointer">
|
||
<input type="checkbox" x-model="exportFields.message"> 消息
|
||
</label>
|
||
<label class="flex items-center gap-2 text-sm cursor-pointer">
|
||
<input type="checkbox" x-model="exportFields.recharged_at"> 充值时间
|
||
</label>
|
||
<label class="flex items-center gap-2 text-sm cursor-pointer">
|
||
<input type="checkbox" x-model="exportFields.sms_url"> 接码链接
|
||
</label>
|
||
<label class="flex items-center gap-2 text-sm cursor-pointer" style="color:#7c3aed;font-weight:500">
|
||
<input type="checkbox" x-model="exportFields.ag_refresh_token"> AG凭证
|
||
</label>
|
||
<label class="flex items-center gap-2 text-sm cursor-pointer" style="color:#6d28d9;font-weight:500">
|
||
<input type="checkbox" x-model="exportFields.cli_ac_credential"> Cli_AC凭证
|
||
</label>
|
||
<label class="flex items-center gap-2 text-sm cursor-pointer" style="color:#0f766e;font-weight:500">
|
||
<input type="checkbox" x-model="exportFields.chatgpt_session"> ChatGPT Session
|
||
</label>
|
||
<label class="flex items-center gap-2 text-sm cursor-pointer" style="color:#2563eb;font-weight:500">
|
||
<input type="checkbox" x-model="exportFields.openai_auth_json"> 授权JSON
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 导出方式 -->
|
||
<div>
|
||
<label class="block text-xs font-medium text-gray-600 mb-2">导出方式</label>
|
||
<div class="flex flex-wrap gap-x-4 gap-y-2">
|
||
<label class="flex items-center gap-2 text-sm cursor-pointer">
|
||
<input type="checkbox" x-model="exportMode.file"> 导出到文件
|
||
</label>
|
||
<label class="flex items-center gap-2 text-sm cursor-pointer" style="color:#047857;font-weight:500">
|
||
<input type="checkbox" x-model="exportMode.csv"> 导出到 CSV
|
||
</label>
|
||
<label class="flex items-center gap-2 text-sm cursor-pointer" style="color:#c2410c;font-weight:500">
|
||
<input type="checkbox" x-model="exportMode.sub2api_json"> 导出 sub2api JSON
|
||
</label>
|
||
<label class="flex items-center gap-2 text-sm cursor-pointer">
|
||
<input type="checkbox" x-model="exportMode.clipboard"> 复制到剪贴板
|
||
</label>
|
||
<label class="flex items-center gap-2 text-sm cursor-pointer" style="color:#6d28d9;font-weight:500">
|
||
<input type="checkbox" x-model="exportMode.staging"> 放入暂存箱
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div x-show="exportMode.staging" x-cloak>
|
||
<label class="block text-xs font-medium text-gray-600 mb-1">暂存文件夹名称</label>
|
||
<textarea x-model="exportStagingNote" rows="2" maxlength="500" placeholder="例如:登录成功,留档给客户 A"
|
||
class="text-sm" style="resize:vertical"></textarea>
|
||
<label class="staging-browser-toggle mt-2">
|
||
<span>
|
||
<strong>同时删除比特浏览器窗口</strong>
|
||
<small>默认关闭;打开后仅删除本批账号绑定的窗口</small>
|
||
</span>
|
||
<input type="checkbox" x-model="deleteBrowserWindowsOnStage">
|
||
<span class="staging-browser-toggle__switch" aria-hidden="true"></span>
|
||
</label>
|
||
</div>
|
||
|
||
<!-- 格式说明 -->
|
||
<div class="bg-gray-50 border border-gray-200 rounded p-2 text-xs text-gray-500 leading-5">
|
||
文本文件和剪贴板使用 <code class="text-gray-700">----</code> 分隔;CSV 含字段名;sub2api JSON 可直接用于账号数据导入
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex gap-2 mt-3">
|
||
<button @click="showExportModal=false" :disabled="exportInProgress" class="btn btn-red">取消</button>
|
||
<button @click="confirmExport()" :disabled="exportInProgress" class="btn btn-primary flex-1"
|
||
x-text="exportInProgress ? '正在导出...' : '确认执行'"></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== 批量邮箱查询模态框 ===== -->
|
||
<div x-show="showBatchEmailSearchModal" x-cloak class="modal-backdrop"
|
||
@click.self="showBatchEmailSearchModal=false" @keydown.escape.window="showBatchEmailSearchModal=false">
|
||
<div class="modal-box modal-compact" style="max-width:640px" role="dialog" aria-modal="true"
|
||
aria-labelledby="batch-email-search-title">
|
||
<div class="flex items-start justify-between gap-3 mb-3">
|
||
<div>
|
||
<h3 id="batch-email-search-title" class="text-base font-semibold text-gray-800">批量邮箱查询</h3>
|
||
<p class="text-xs text-gray-400 mt-1">每行一个邮箱;Gmail 匹配会忽略用户名中的点和大小写,其他邮箱按不区分大小写精确匹配。</p>
|
||
</div>
|
||
<button type="button" class="text-gray-400 hover:text-gray-600 text-xl leading-none"
|
||
@click="showBatchEmailSearchModal=false" aria-label="关闭">×</button>
|
||
</div>
|
||
|
||
<textarea x-model="batchEmailSearchText" rows="12" spellcheck="false"
|
||
class="font-mono text-xs" placeholder="qwertyu0134q@gmail.com tachenymolchan@gmail.com watjack128@gmail.com"></textarea>
|
||
|
||
<div x-show="batchEmailSearchActive" x-cloak class="mt-3 space-y-2">
|
||
<div class="grid grid-cols-3 gap-2 text-center text-xs">
|
||
<div class="rounded border border-gray-200 bg-gray-50 p-2">输入<br><b x-text="batchEmailSearchEmails.length"></b></div>
|
||
<div class="rounded border border-green-200 bg-green-50 p-2 text-green-700">找到<br><b x-text="batchEmailSearchAccounts.length"></b></div>
|
||
<div class="rounded border border-red-200 bg-red-50 p-2 text-red-600">未找到<br><b x-text="batchEmailSearchMissingEmails.length"></b></div>
|
||
</div>
|
||
<div x-show="batchEmailSearchMissingEmails.length > 0">
|
||
<div class="flex items-center justify-between mb-1">
|
||
<label class="text-xs font-medium text-gray-600">未找到的邮箱</label>
|
||
<button type="button" @click="copyBatchEmailSearchMissing()" class="btn btn-sm btn-outline">复制未找到</button>
|
||
</div>
|
||
<textarea readonly rows="5" spellcheck="false" class="font-mono text-xs bg-gray-50"
|
||
:value="batchEmailSearchMissingEmails.join('\n')"></textarea>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex gap-2 mt-3">
|
||
<button @click="showBatchEmailSearchModal=false" class="btn btn-gray">取消</button>
|
||
<button x-show="batchEmailSearchActive" @click="clearBatchEmailSearch()" class="btn btn-red">清除查询</button>
|
||
<button @click="applyBatchEmailSearch()" class="btn btn-primary flex-1"
|
||
x-text="batchEmailSearchActive ? '重新查询' : '开始查询'"></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== 导入账号模态框 ===== -->
|
||
<div x-show="showImportAccountModal" x-cloak class="modal-backdrop">
|
||
<div class="modal-box modal-compact">
|
||
<h3 class="text-base font-semibold text-gray-800 mb-3">导入账号</h3>
|
||
|
||
<div class="space-y-2">
|
||
<!-- 客户名 -->
|
||
<div>
|
||
<label class="block text-xs font-medium text-gray-600 mb-1">客户名</label>
|
||
<input type="text" x-model="importCustomerName" placeholder="输入客户名(可选)" class="text-sm">
|
||
</div>
|
||
|
||
<!-- 模板选择 -->
|
||
<div>
|
||
<label class="block text-xs font-medium text-gray-600 mb-1">导入模板</label>
|
||
<div class="flex items-center gap-2">
|
||
<select x-model.number="importSelectedIndex" class="flex-1" style="height:32px;padding:4px 10px;">
|
||
<template x-for="(tpl, idx) in importTemplates" :key="idx">
|
||
<option :value="idx" x-text="tpl.name"></option>
|
||
</template>
|
||
</select>
|
||
<button @click="deleteImportTemplate(importSelectedIndex)" class="btn btn-red" title="删除当前模板"
|
||
:disabled="importTemplates.length <= 1">删除</button>
|
||
<button @click="showTemplateEditor = !showTemplateEditor" class="btn btn-outline" title="新建模板"
|
||
x-text="showTemplateEditor ? '收起' : '新建'"></button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 新建模板(可折叠) -->
|
||
<div x-show="showTemplateEditor" x-cloak class="bg-blue-50 border border-blue-200 rounded p-2 space-y-2">
|
||
<input type="text" x-model="importNewTemplateName" placeholder="模板名称" class="text-xs">
|
||
<input type="text" x-model="importNewTemplatePattern" placeholder="如:{{账号}}|{{密码}}|{{无用}}|{{2fa}}"
|
||
class="font-mono text-xs">
|
||
<div class="flex items-center gap-2">
|
||
<span class="text-xs text-gray-400">可用: {{账号}} {{密码}} {{辅助邮箱}} {{2fa}} {{接码链接}} {{rt凭证}} {{无用}}</span>
|
||
<button @click="addImportTemplate()" class="btn btn-green ml-auto">保存模板</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 账号列表 -->
|
||
<div>
|
||
<label class="block text-xs font-medium text-gray-600 mb-1">账号列表</label>
|
||
<textarea x-model="importText" rows="8" placeholder="粘贴账号列表..." class="font-mono text-xs"></textarea>
|
||
</div>
|
||
|
||
<label class="flex items-center gap-2 text-xs text-gray-600 cursor-pointer select-none">
|
||
<input type="checkbox" x-model="importSkipDuplicates" class="h-4 w-4">
|
||
<span>跳过重复邮箱(已有账号不做任何修改)</span>
|
||
</label>
|
||
<div class="text-xs text-blue-600 bg-blue-50 border border-blue-100 rounded p-2">
|
||
导入只保存账号;执行具体任务时才创建临时比特浏览器窗口,单个账号处理结束后自动删除。
|
||
</div>
|
||
|
||
<!-- 解析预览(第1行) -->
|
||
<template x-if="importText.trim() && getImportPreview().length > 0">
|
||
<div class="bg-gray-50 border border-gray-200 rounded p-2">
|
||
<div class="text-xs font-medium text-gray-600 mb-1">解析预览</div>
|
||
<div class="space-y-0.5">
|
||
<template x-for="(col, ci) in getImportPreview()[0]" :key="ci">
|
||
<div class="flex items-center gap-2 text-xs">
|
||
<span class="px-1.5 py-0.5 rounded text-white shrink-0" :class="col.ok ? 'bg-blue-500' : 'bg-red-400'"
|
||
x-text="col.label"></span>
|
||
<span class="font-mono text-gray-700 truncate" x-text="col.value"></span>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<div class="flex gap-2 mt-3">
|
||
<button
|
||
@click="showImportAccountModal=false; importText=''; importCustomerName=''; showTemplateEditor=false; importNewTemplateName=''; importNewTemplatePattern=''"
|
||
class="btn btn-red">取消</button>
|
||
<button @click="importAccounts()" class="btn btn-primary flex-1">导入<span x-show="importText.trim()"
|
||
x-text="' ' + importText.trim().split('\n').filter(l => l.trim() && !l.trim().startsWith('#')).length + ' 个'"></span></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== 暂存箱 ===== -->
|
||
<div x-show="showStagingModal" x-cloak class="modal-backdrop" @click.self="showStagingModal=false">
|
||
<div class="modal-box modal-compact staging-modal">
|
||
<template x-if="!stagedBatchDetail">
|
||
<div>
|
||
<div class="flex items-center justify-between gap-3 mb-3">
|
||
<div>
|
||
<h3 class="text-base font-semibold text-gray-800">暂存箱</h3>
|
||
<p class="text-xs text-gray-400 mt-1"
|
||
x-text="'共 ' + stagedBatches.length + ' 个文件夹,' + stagedBatches.reduce((sum, item) => sum + Number(item.account_count || 0), 0) + ' 个账号'"></p>
|
||
</div>
|
||
<button @click="loadStagingBatches()" class="btn btn-gray btn-sm">刷新</button>
|
||
</div>
|
||
|
||
<div class="staging-folder-grid">
|
||
<template x-for="batch in stagedBatches" :key="batch.id">
|
||
<button type="button" class="staging-folder" @click="openStagedBatch(batch)">
|
||
<span class="staging-folder__icon" aria-hidden="true"></span>
|
||
<span class="staging-folder__body">
|
||
<strong class="staging-folder__name" x-text="batch.name"></strong>
|
||
<span class="staging-folder__meta" x-text="batch.account_count + ' 个账号 · ' + formatDate(batch.created_at)"></span>
|
||
<span class="staging-folder__fields" x-text="getStagingFieldLabels(batch)"></span>
|
||
</span>
|
||
<span class="staging-folder__arrow">›</span>
|
||
</button>
|
||
</template>
|
||
<div x-show="stagedBatches.length === 0" class="staging-empty">暂存箱为空</div>
|
||
</div>
|
||
|
||
<div class="flex gap-2 mt-3">
|
||
<button @click="showStagingModal=false" class="btn btn-gray">关闭</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template x-if="stagedBatchDetail">
|
||
<div>
|
||
<div class="staging-detail-header">
|
||
<button @click="stagedBatchDetail=null" class="page-icon-btn" title="返回暂存箱">‹</button>
|
||
<div class="min-w-0 flex-1">
|
||
<h3 class="text-base font-semibold text-gray-800 truncate" x-text="stagedBatchDetail.batch.name"></h3>
|
||
<p class="text-xs text-gray-400 mt-1"
|
||
x-text="stagedBatchDetail.batch.account_count + ' 个账号 · ' + getStagingFieldLabels(stagedBatchDetail.batch)"></p>
|
||
</div>
|
||
</div>
|
||
|
||
<textarea readonly class="staging-output" :value="stagedBatchText" spellcheck="false"></textarea>
|
||
|
||
<div class="flex gap-2 mt-3 staging-detail-actions">
|
||
<button @click="copyStagedBatch()" class="btn btn-primary">复制全部</button>
|
||
<button @click="downloadStagedBatch()" class="btn btn-green">导出 TXT</button>
|
||
<button @click="restoreStagedBatch()" :disabled="isRunning" class="btn btn-gray"
|
||
title="恢复只回写账号,具体任务执行时再创建临时比特窗口">恢复到主列表</button>
|
||
<button @click="permanentlyDeleteStagedBatch()" class="btn btn-red">彻底删除文件夹</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== 批量卡付充值模态框 ===== -->
|
||
<div x-show="showRechargeModal" x-cloak class="modal-backdrop"
|
||
@click.self="closeRechargeModal()" @keydown.escape.window="closeRechargeModal()">
|
||
<div class="modal-box modal-compact" role="dialog" aria-modal="true" aria-labelledby="recharge-title">
|
||
<h3 id="recharge-title" class="text-base font-semibold text-gray-800 mb-2">批量充值</h3>
|
||
<p class="text-xs text-gray-500 mb-3">
|
||
已选择 <b x-text="rechargeAccounts.length"></b> 个账号。请按下方账号顺序逐行填写卡密,一行只分配给一个账号。
|
||
</p>
|
||
|
||
<div class="border border-gray-200 rounded bg-gray-50 mb-3" style="max-height:150px;overflow:auto">
|
||
<template x-for="(row, index) in rechargeAccounts" :key="row.email">
|
||
<div class="flex items-center gap-2 px-3 py-1.5 text-xs border-b border-gray-100 last:border-b-0">
|
||
<span class="text-gray-400 w-6 text-right" x-text="(index + 1) + '.'"></span>
|
||
<span class="font-mono text-gray-700 truncate" x-text="row.email" :title="row.email"></span>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<label class="block text-xs font-medium text-gray-600 mb-1">卡密(每行一个)</label>
|
||
<textarea x-model="rechargeCardKeys" rows="10" class="w-full text-xs font-mono"
|
||
autocomplete="off" spellcheck="false" placeholder="第1行卡密对应第1个账号 第2行卡密对应第2个账号"></textarea>
|
||
<div class="flex items-center justify-between gap-2 mt-1 text-xs">
|
||
<span class="text-gray-400"
|
||
x-text="'已填写 ' + rechargeCardLineCount + ' / ' + rechargeAccounts.length + ' 行'"></span>
|
||
<span x-show="rechargeInputError" class="text-red-500" x-text="rechargeInputError"></span>
|
||
</div>
|
||
<div class="flex items-center justify-between gap-3 mt-3">
|
||
<div>
|
||
<label for="recharge-concurrency" class="block text-xs font-medium text-gray-600">充值并发</label>
|
||
<p class="text-xs text-gray-400 mt-1">默认 2,最大 5;相邻请求仍错峰 1 秒发起。</p>
|
||
</div>
|
||
<input id="recharge-concurrency" type="number" x-model.number="rechargeConcurrency"
|
||
@change="rechargeConcurrency = normalizedRechargeConcurrency"
|
||
min="1" max="5" step="1" class="w-20 text-center" style="width:70px">
|
||
</div>
|
||
<p class="text-xs text-orange-600 bg-orange-50 border border-orange-100 rounded p-2 mt-3">
|
||
确认后将对每个账号自动执行“卡付预创建 → 卡付确认”;使用该账号数据库中已保存的完整 ChatGPT Session(结构化 JSON)。
|
||
</p>
|
||
|
||
<div class="flex gap-2 mt-3">
|
||
<button @click="closeRechargeModal()" :disabled="rechargeSubmitting" class="btn btn-gray">取消</button>
|
||
<button @click="confirmRecharge()" :disabled="rechargeSubmitting || !!rechargeInputError"
|
||
class="btn btn-orange flex-1"
|
||
x-text="rechargeSubmitting ? '正在启动...' : '确认并开始充值'"></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== 批量改写客户名模态框 ===== -->
|
||
<div x-show="showBatchCustomerNameModal" x-cloak class="modal-backdrop"
|
||
@click.self="showBatchCustomerNameModal=false" @keydown.escape.window="showBatchCustomerNameModal=false">
|
||
<div class="modal-box modal-compact" role="dialog" aria-modal="true" aria-labelledby="batch-customer-name-title">
|
||
<h3 id="batch-customer-name-title" class="text-base font-semibold text-gray-800 mb-3">
|
||
批量改写客户名
|
||
<span class="text-xs font-normal text-gray-400 ml-2" x-text="'已选 ' + selectedCount + ' 个账号'"></span>
|
||
</h3>
|
||
<div class="space-y-3">
|
||
<div>
|
||
<label class="block text-xs font-medium text-gray-600 mb-1">新客户名</label>
|
||
<input type="text" x-model="batchCustomerName" maxlength="100" class="w-full"
|
||
placeholder="输入客户名,留空表示清空"
|
||
@keydown.enter.prevent="confirmBatchCustomerName()">
|
||
<p class="text-xs text-gray-400 mt-1">留空后确认,将清空所选账号的客户名。</p>
|
||
</div>
|
||
</div>
|
||
<div class="flex gap-2 mt-3">
|
||
<button @click="showBatchCustomerNameModal=false" :disabled="batchCustomerNameSaving"
|
||
class="btn btn-red">取消</button>
|
||
<button @click="confirmBatchCustomerName()" :disabled="batchCustomerNameSaving"
|
||
class="btn btn-primary flex-1"
|
||
x-text="batchCustomerNameSaving ? '保存中...' : '确认改写'"></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== 退款截止时间模态框 ===== -->
|
||
<div x-show="showRefundDeadlineModal" x-cloak class="modal-backdrop"
|
||
@click.self="showRefundDeadlineModal=false" @keydown.escape.window="showRefundDeadlineModal=false">
|
||
<div class="modal-box modal-compact" role="dialog" aria-modal="true" aria-labelledby="refund-deadline-title">
|
||
<h3 id="refund-deadline-title" class="text-base font-semibold text-gray-800 mb-2">
|
||
设置退款截止时间
|
||
<span class="text-xs font-normal text-gray-400 ml-2" x-text="'账号 ' + refundDeadlineEmails.length + ' 个'"></span>
|
||
</h3>
|
||
<p class="text-xs text-gray-500 mb-3">
|
||
可指定北京时间,也可按每个账号自己的充值时间增加分钟、小时或天数。截止时间到达或套餐用量达到阈值,任一条件满足都会触发。
|
||
</p>
|
||
<div class="rounded border border-gray-200 p-3">
|
||
<label class="flex items-center gap-2 text-xs font-medium text-gray-700 mb-2 cursor-pointer">
|
||
<input type="radio" x-model="refundDeadlineMode" value="absolute">
|
||
指定退款截止时间(北京时间)
|
||
</label>
|
||
<input type="datetime-local" step="1" x-model="refundDeadlineValue"
|
||
@focus="refundDeadlineMode='absolute'" @input="refundDeadlineMode='absolute'" class="w-full">
|
||
<div class="flex flex-wrap gap-2 mt-2">
|
||
<button @click="setRefundDeadlineQuick(1)" class="btn btn-gray">当前时间 + 1小时</button>
|
||
<button @click="setRefundDeadlineQuick(24)" class="btn btn-gray">当前时间 + 1天</button>
|
||
<button @click="setRefundDeadlineQuick(72)" class="btn btn-gray">当前时间 + 3天</button>
|
||
</div>
|
||
</div>
|
||
<div class="rounded border border-purple-200 bg-purple-50 p-3 mt-3">
|
||
<label class="flex items-center gap-2 text-xs font-medium text-purple-800 mb-2 cursor-pointer">
|
||
<input type="radio" x-model="refundDeadlineMode" value="recharge_offset">
|
||
按每个账号的充值时间计算
|
||
</label>
|
||
<div class="flex gap-2 items-center">
|
||
<input type="number" min="1" step="1" x-model.number="refundDeadlineOffsetValue"
|
||
@focus="refundDeadlineMode='recharge_offset'" @input="refundDeadlineMode='recharge_offset'"
|
||
class="flex-1" placeholder="数量">
|
||
<select x-model="refundDeadlineOffsetUnit" @change="refundDeadlineMode='recharge_offset'"
|
||
class="settings-select refund-deadline-unit-select">
|
||
<option value="minutes">分钟</option>
|
||
<option value="hours">小时</option>
|
||
<option value="days">天</option>
|
||
</select>
|
||
</div>
|
||
<div class="flex flex-wrap gap-2 mt-2">
|
||
<button @click="setRefundDeadlineRechargeOffset(30, 'minutes')" class="btn btn-purple">+30分钟</button>
|
||
<button @click="setRefundDeadlineRechargeOffset(1, 'hours')" class="btn btn-purple">+1小时</button>
|
||
<button @click="setRefundDeadlineRechargeOffset(6, 'hours')" class="btn btn-purple">+6小时</button>
|
||
<button @click="setRefundDeadlineRechargeOffset(12, 'hours')" class="btn btn-purple">+12小时</button>
|
||
<button @click="setRefundDeadlineRechargeOffset(1, 'days')" class="btn btn-purple">+1天</button>
|
||
<button @click="setRefundDeadlineRechargeOffset(3, 'days')" class="btn btn-purple">+3天</button>
|
||
</div>
|
||
<p class="text-xs text-purple-600 mt-2">批量设置时,每个账号分别使用自己的充值时间;没有充值时间的账号会跳过并提示。</p>
|
||
</div>
|
||
<div class="flex gap-2 mt-4">
|
||
<button @click="showRefundDeadlineModal=false" :disabled="refundDeadlineSaving" class="btn btn-gray">取消</button>
|
||
<button @click="saveRefundDeadline(true)" :disabled="refundDeadlineSaving" class="btn btn-red">清除时间</button>
|
||
<button @click="saveRefundDeadline(false)" :disabled="refundDeadlineSaving" class="btn btn-primary flex-1"
|
||
x-text="refundDeadlineSaving ? '保存中...' : '确认保存'"></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== 批量更改状态模态框 ===== -->
|
||
<div x-show="showBatchStatusModal" x-cloak class="modal-backdrop" @click.self="showBatchStatusModal=false">
|
||
<div class="modal-box modal-compact">
|
||
<h3 class="text-base font-semibold text-gray-800 mb-3">
|
||
批量更改状态
|
||
<span class="text-xs font-normal text-gray-400 ml-2" x-text="'已选 ' + selectedCount + ' 个账号'"></span>
|
||
</h3>
|
||
<div class="space-y-3">
|
||
<div>
|
||
<label class="block text-xs font-medium text-gray-600 mb-1">目标状态</label>
|
||
<select x-model="batchStatus" class="status-select w-full">
|
||
<option value="未登录">未登录</option>
|
||
<option value="登录成功">登录成功</option>
|
||
<option value="登录失败">登录失败</option>
|
||
<option value="改密成功">改密成功</option>
|
||
<option value="改密失败">改密失败</option>
|
||
<option value="2FA修改成功">2FA修改成功</option>
|
||
<option value="2FA修改失败">2FA修改失败</option>
|
||
<option value="未验证已改密和2FA">未验证已改密和2FA</option>
|
||
<option value="已验证改密和2FA">已验证改密和2FA</option>
|
||
<option value="未注册成功已改密和2FA">未注册成功已改密和2FA</option>
|
||
<option value="ChatGPT成功">ChatGPT成功</option>
|
||
<option value="ChatGPT失败">ChatGPT失败</option>
|
||
<option value="半价Ultra">半价Ultra</option>
|
||
<option value="订阅成功">订阅成功</option>
|
||
<option value="已充值提取">已充值提取</option>
|
||
<option value="超时">超时</option>
|
||
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="flex gap-2 mt-3">
|
||
<button @click="showBatchStatusModal=false" class="btn btn-red">取消</button>
|
||
<button @click="confirmBatchStatus()" class="btn btn-primary flex-1">确认更改</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== 链接取号 + 反重力凭证弹窗 ===== -->
|
||
<div x-show="showGoogleAccountAgModal" x-cloak class="modal-backdrop"
|
||
@click.self="showGoogleAccountAgModal=false" @keydown.escape.window="showGoogleAccountAgModal=false">
|
||
<div class="modal-box modal-compact" role="dialog" aria-modal="true" aria-labelledby="google-account-ag-title">
|
||
<h3 id="google-account-ag-title" class="text-base font-semibold text-gray-800 mb-2">链接提取 + 获取凭证</h3>
|
||
<p class="text-xs text-gray-500 mb-3">从已配置的供应接口批量取号,只保存邮箱和密码;随后无代理创建浏览器,并执行反重力“获取凭证”。</p>
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="block text-xs font-medium text-gray-600 mb-1">处理账号数量(1~5000)</label>
|
||
<input type="number" min="1" max="5000" step="1" x-model.number="googleAccountAgQuantity"
|
||
class="w-full" @keydown.enter.prevent="confirmGoogleAccountAgTask()">
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-medium text-gray-600 mb-1">并发参数(兼容保留)</label>
|
||
<input type="number" min="1" max="30" step="1" x-model.number="googleAccountAgConcurrency"
|
||
class="w-full" @keydown.enter.prevent="confirmGoogleAccountAgTask()">
|
||
</div>
|
||
</div>
|
||
<div class="rounded border border-blue-100 bg-blue-50 p-3 text-xs text-blue-700 leading-5 mt-3">
|
||
供应数据中的辅助邮箱及后续字段会被忽略;数据库已有邮箱会跳过且不覆盖。临时比特浏览器固定使用直连(无代理),并严格按邮箱串行处理:完成一个、删除一个,再开始下一个。
|
||
</div>
|
||
<div class="flex gap-2 mt-3">
|
||
<button @click="showGoogleAccountAgModal=false" class="btn btn-red">取消</button>
|
||
<button @click="confirmGoogleAccountAgTask()" class="btn btn-blue flex-1">开始处理</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== 改密 + 2FA 执行方式弹窗 ===== -->
|
||
<div x-show="showSecurityRunModal" x-cloak class="modal-backdrop"
|
||
@click.self="showSecurityRunModal=false" @keydown.escape.window="showSecurityRunModal=false">
|
||
<div class="modal-box modal-compact" role="dialog" aria-modal="true" aria-labelledby="security-run-title">
|
||
<h3 id="security-run-title" class="text-base font-semibold text-gray-800 mb-2">改密 + 2FA</h3>
|
||
<p class="text-xs text-gray-500 mb-3"
|
||
x-text="'本次将处理 ' + (selectedCount > 0 ? selectedCount : accounts.length) + ' 个账号,请选择执行方式。'"></p>
|
||
|
||
<div class="space-y-2">
|
||
<label class="flex items-start gap-2 border border-orange-200 bg-orange-50 rounded p-3 cursor-pointer">
|
||
<input type="radio" name="security_run_mode" value="overwrite" x-model="securityRunMode" class="mt-0.5">
|
||
<span>
|
||
<span class="block text-sm font-medium text-orange-800">覆盖修改</span>
|
||
<span class="block text-xs text-orange-600 mt-0.5">保持当前逻辑,重新修改密码和2FA。</span>
|
||
</span>
|
||
</label>
|
||
<label class="flex items-start gap-2 border border-gray-200 rounded p-3 cursor-pointer">
|
||
<input type="radio" name="security_run_mode" value="skip" x-model="securityRunMode" class="mt-0.5">
|
||
<span>
|
||
<span class="block text-sm font-medium text-gray-700">跳过已完成</span>
|
||
<span class="block text-xs text-gray-400 mt-0.5">按数据库记录逐项判断;已改密则只处理2FA,已改2FA则只处理密码。</span>
|
||
</span>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="flex gap-2 mt-3">
|
||
<button @click="showSecurityRunModal=false" class="btn btn-red">取消</button>
|
||
<button @click="confirmSecurityTask()" class="btn btn-purple flex-1">开始执行</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== ChatGPT 登录/注册选项弹窗 ===== -->
|
||
<div x-show="showChatgptRunModal" x-cloak class="modal-backdrop"
|
||
@click.self="showChatgptRunModal=false" @keydown.escape.window="showChatgptRunModal=false">
|
||
<div class="modal-box modal-compact" role="dialog" aria-modal="true" aria-labelledby="chatgpt-run-title">
|
||
<h3 id="chatgpt-run-title" class="text-base font-semibold text-gray-800 mb-2">ChatGPT登录/注册</h3>
|
||
<p class="text-xs text-gray-500 mb-3"
|
||
x-text="'本次将处理 ' + (selectedCount > 0 ? selectedCount : accounts.length) + ' 个账号,请选择登录方式。'"></p>
|
||
|
||
<div class="space-y-2">
|
||
<label class="flex items-start gap-2 border border-gray-200 rounded p-3 cursor-pointer">
|
||
<input type="radio" name="chatgpt_auth_mode" value="keep" x-model="chatgptAuthMode" class="mt-0.5">
|
||
<span>
|
||
<span class="block text-sm font-medium text-gray-700">保留现有登录数据</span>
|
||
<span class="block text-xs text-gray-400 mt-0.5">沿用当前缓存、Cookie和Token,已登录账号可直接复用。</span>
|
||
</span>
|
||
</label>
|
||
<label class="flex items-start gap-2 border border-orange-200 bg-orange-50 rounded p-3 cursor-pointer">
|
||
<input type="radio" name="chatgpt_auth_mode" value="clear" x-model="chatgptAuthMode" class="mt-0.5">
|
||
<span>
|
||
<span class="block text-sm font-medium text-orange-800">清除缓存、Cookie和Token</span>
|
||
<span class="block text-xs text-orange-600 mt-0.5">清除每个账号浏览器窗口的登录数据,并重新走Google授权流程。</span>
|
||
</span>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="flex gap-2 mt-3">
|
||
<button @click="showChatgptRunModal=false" class="btn btn-red">取消</button>
|
||
<button @click="confirmChatgptRegister()" class="btn btn-green flex-1">开始执行</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== 查询套餐连接方式弹窗 ===== -->
|
||
<div x-show="showOpenAIPlanRunModal" x-cloak class="modal-backdrop"
|
||
@click.self="showOpenAIPlanRunModal=false" @keydown.escape.window="showOpenAIPlanRunModal=false">
|
||
<div class="modal-box modal-compact" role="dialog" aria-modal="true" aria-labelledby="openai-plan-run-title">
|
||
<h3 id="openai-plan-run-title" class="text-base font-semibold text-gray-800 mb-2">查询套餐</h3>
|
||
<p class="text-xs text-gray-500 mb-3"
|
||
x-text="'本次将查询 ' + (selectedCount > 0 ? selectedCount : accounts.length) + ' 个账号,请选择连接方式。'"></p>
|
||
|
||
<div class="space-y-2">
|
||
<label class="flex items-start gap-2 border border-gray-200 rounded p-3 cursor-pointer">
|
||
<input type="radio" name="openai_plan_connection_mode" value="browser_proxy"
|
||
x-model="openAIPlanConnectionMode" class="mt-0.5">
|
||
<span>
|
||
<span class="block text-sm font-medium text-gray-700">使用浏览器代理</span>
|
||
<span class="block text-xs text-gray-400 mt-0.5">使用每个账号绑定的代理;代理失败时不会回退直连。</span>
|
||
</span>
|
||
</label>
|
||
<label class="flex items-start gap-2 border border-blue-200 bg-blue-50 rounded p-3 cursor-pointer">
|
||
<input type="radio" name="openai_plan_connection_mode" value="direct"
|
||
x-model="openAIPlanConnectionMode" class="mt-0.5">
|
||
<span>
|
||
<span class="block text-sm font-medium text-blue-800">直连</span>
|
||
<span class="block text-xs text-blue-600 mt-0.5">本次查询明确绕过账号代理,直接请求套餐和用量接口。</span>
|
||
</span>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="flex gap-2 mt-3">
|
||
<button @click="showOpenAIPlanRunModal=false" class="btn btn-red">取消</button>
|
||
<button @click="confirmOpenAIQueryPlan()" class="btn btn-green flex-1">开始查询</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== ChatGPT Session 弹窗 ===== -->
|
||
<div x-show="showChatgptSessionModal" x-cloak class="modal-backdrop"
|
||
@click.self="closeChatgptSession()" @keydown.escape.window="closeChatgptSession()">
|
||
<div class="modal-box modal-compact chatgpt-session-modal" role="dialog" aria-modal="true"
|
||
aria-labelledby="chatgpt-session-title">
|
||
<div class="chatgpt-session-header">
|
||
<div class="min-w-0">
|
||
<h3 id="chatgpt-session-title" class="text-base font-semibold text-gray-800">ChatGPT Session</h3>
|
||
<p class="text-xs text-gray-400 truncate mt-1" x-text="chatgptSessionEmail"></p>
|
||
</div>
|
||
<button type="button" class="chatgpt-session-close" title="关闭"
|
||
@click="closeChatgptSession()" aria-label="关闭">×</button>
|
||
</div>
|
||
|
||
<div x-show="chatgptSessionLoading" class="chatgpt-session-state">正在读取 Session...</div>
|
||
<div x-show="chatgptSessionError" x-text="chatgptSessionError"
|
||
class="chatgpt-session-state chatgpt-session-state--error"></div>
|
||
<textarea x-show="!chatgptSessionLoading && !chatgptSessionError" readonly spellcheck="false"
|
||
class="chatgpt-session-output" :value="chatgptSessionText"
|
||
aria-label="ChatGPT Session JSON"></textarea>
|
||
|
||
<div class="chatgpt-session-actions">
|
||
<button type="button" @click="closeChatgptSession()" class="btn btn-gray">关闭</button>
|
||
<button type="button" @click="copyChatgptSession()" class="btn btn-primary"
|
||
:disabled="chatgptSessionLoading || !!chatgptSessionError || !chatgptSessionText">复制全部</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== 最近充值卡密弹窗 ===== -->
|
||
<div x-show="showRechargeCardModal" x-cloak class="modal-backdrop"
|
||
@click.self="closeRechargeCard()" @keydown.escape.window="closeRechargeCard()">
|
||
<div class="modal-box modal-compact recharge-card-modal" role="dialog" aria-modal="true"
|
||
aria-labelledby="recharge-card-title">
|
||
<div class="chatgpt-session-header">
|
||
<div class="min-w-0">
|
||
<h3 id="recharge-card-title" class="text-base font-semibold text-gray-800">本次充值卡密</h3>
|
||
<p class="text-xs text-gray-400 truncate mt-1" x-text="rechargeCardEmail"></p>
|
||
</div>
|
||
<button type="button" class="chatgpt-session-close" title="关闭"
|
||
@click="closeRechargeCard()" aria-label="关闭">×</button>
|
||
</div>
|
||
|
||
<div x-show="rechargeCardLoading" class="chatgpt-session-state recharge-card-state">正在读取卡密...</div>
|
||
<div x-show="rechargeCardError" x-text="rechargeCardError"
|
||
class="chatgpt-session-state recharge-card-state chatgpt-session-state--error"></div>
|
||
<div x-show="!rechargeCardLoading && !rechargeCardError"
|
||
class="text-xs text-gray-500 mt-3 mb-2">
|
||
<span>充值完成时间:</span>
|
||
<span x-text="rechargeCardCompletedAt ? formatDate(rechargeCardCompletedAt) : '暂无完成时间'"></span>
|
||
</div>
|
||
<textarea x-show="!rechargeCardLoading && !rechargeCardError" readonly spellcheck="false"
|
||
class="chatgpt-session-output recharge-card-output" :value="rechargeCardText"
|
||
aria-label="本次充值卡密"></textarea>
|
||
|
||
<div class="chatgpt-session-actions">
|
||
<button type="button" @click="closeRechargeCard()" class="btn btn-gray">关闭</button>
|
||
<button type="button" @click="copyRechargeCard()" class="btn btn-primary"
|
||
:disabled="rechargeCardLoading || !!rechargeCardError || !rechargeCardText">复制卡密</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== OpenAI 授权 JSON 弹窗 ===== -->
|
||
<div x-show="showOpenAIAuthModal" x-cloak class="modal-backdrop"
|
||
@click.self="closeOpenAIAuth()" @keydown.escape.window="closeOpenAIAuth()">
|
||
<div class="modal-box modal-compact chatgpt-session-modal" role="dialog" aria-modal="true"
|
||
aria-labelledby="openai-auth-title">
|
||
<div class="chatgpt-session-header">
|
||
<div class="min-w-0">
|
||
<h3 id="openai-auth-title" class="text-base font-semibold text-gray-800">授权 JSON</h3>
|
||
<p class="text-xs text-gray-400 truncate mt-1" x-text="openAIAuthEmail"></p>
|
||
</div>
|
||
<button type="button" class="chatgpt-session-close" title="关闭"
|
||
@click="closeOpenAIAuth()" aria-label="关闭">×</button>
|
||
</div>
|
||
|
||
<div x-show="openAIAuthLoading" class="chatgpt-session-state">正在读取授权 JSON...</div>
|
||
<div x-show="openAIAuthError" x-text="openAIAuthError"
|
||
class="chatgpt-session-state chatgpt-session-state--error"></div>
|
||
<textarea x-show="!openAIAuthLoading && !openAIAuthError" readonly spellcheck="false"
|
||
class="chatgpt-session-output" :value="openAIAuthText" aria-label="OpenAI 授权 JSON"></textarea>
|
||
|
||
<div class="chatgpt-session-actions">
|
||
<button type="button" @click="closeOpenAIAuth()" class="btn btn-gray">关闭</button>
|
||
<button type="button" @click="copyOpenAIAuth()" class="btn btn-primary"
|
||
:disabled="openAIAuthLoading || !!openAIAuthError || !openAIAuthText">复制全部</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== 反重力 AC 凭证弹窗 ===== -->
|
||
<div x-show="showAgCredentialModal" x-cloak class="modal-backdrop"
|
||
@click.self="closeAgCredential()" @keydown.escape.window="closeAgCredential()">
|
||
<div class="modal-box modal-compact chatgpt-session-modal" role="dialog" aria-modal="true"
|
||
aria-labelledby="ag-credential-title">
|
||
<div class="chatgpt-session-header">
|
||
<div class="min-w-0">
|
||
<h3 id="ag-credential-title" class="text-base font-semibold text-gray-800">AC 凭证</h3>
|
||
<p class="text-xs text-gray-400 truncate mt-1" x-text="agCredentialEmail"></p>
|
||
</div>
|
||
<button type="button" class="chatgpt-session-close" title="关闭"
|
||
@click="closeAgCredential()" aria-label="关闭">×</button>
|
||
</div>
|
||
|
||
<div x-show="agCredentialLoading" class="chatgpt-session-state">正在读取 AC 凭证...</div>
|
||
<div x-show="agCredentialError" x-text="agCredentialError"
|
||
class="chatgpt-session-state chatgpt-session-state--error"></div>
|
||
<textarea x-show="!agCredentialLoading && !agCredentialError" readonly spellcheck="false"
|
||
class="chatgpt-session-output" :value="agCredentialText" aria-label="反重力 AC 凭证"></textarea>
|
||
|
||
<div class="chatgpt-session-actions">
|
||
<button type="button" @click="closeAgCredential()" class="btn btn-gray">关闭</button>
|
||
<button type="button" @click="copyAgCredential()" class="btn btn-primary"
|
||
:disabled="agCredentialLoading || !!agCredentialError || !agCredentialText">复制全部</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== Gemini CLI AC 凭证弹窗 ===== -->
|
||
<div x-show="showCliAcCredentialModal" x-cloak class="modal-backdrop"
|
||
@click.self="closeCliAcCredential()" @keydown.escape.window="closeCliAcCredential()">
|
||
<div class="modal-box modal-compact chatgpt-session-modal" role="dialog" aria-modal="true"
|
||
aria-labelledby="cli-ac-credential-title">
|
||
<div class="chatgpt-session-header">
|
||
<div class="min-w-0">
|
||
<h3 id="cli-ac-credential-title" class="text-base font-semibold text-gray-800">Cli_AC 凭证</h3>
|
||
<p class="text-xs text-gray-400 truncate mt-1" x-text="cliAcCredentialEmail"></p>
|
||
</div>
|
||
<button type="button" class="chatgpt-session-close" title="关闭"
|
||
@click="closeCliAcCredential()" aria-label="关闭">×</button>
|
||
</div>
|
||
|
||
<div x-show="cliAcCredentialLoading" class="chatgpt-session-state">正在读取 Cli_AC 凭证...</div>
|
||
<div x-show="cliAcCredentialError" x-text="cliAcCredentialError"
|
||
class="chatgpt-session-state chatgpt-session-state--error"></div>
|
||
<textarea x-show="!cliAcCredentialLoading && !cliAcCredentialError" readonly spellcheck="false"
|
||
class="chatgpt-session-output" :value="cliAcCredentialText" aria-label="Gemini CLI AC 凭证"></textarea>
|
||
|
||
<div class="chatgpt-session-actions">
|
||
<button type="button" @click="closeCliAcCredential()" class="btn btn-gray">关闭</button>
|
||
<button type="button" @click="copyCliAcCredential()" class="btn btn-primary"
|
||
:disabled="cliAcCredentialLoading || !!cliAcCredentialError || !cliAcCredentialText">复制全部</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== 单个邮箱的 AI Studio API Key(JSON)弹窗 ===== -->
|
||
<div x-show="showAistudioKeysModal" x-cloak class="modal-backdrop"
|
||
@click.self="showAistudioKeysModal=false" @keydown.escape.window="showAistudioKeysModal=false">
|
||
<div class="modal-box modal-compact chatgpt-session-modal" role="dialog" aria-modal="true"
|
||
aria-labelledby="aistudio-keys-title">
|
||
<div class="chatgpt-session-header">
|
||
<div class="min-w-0">
|
||
<h3 id="aistudio-keys-title" class="text-base font-semibold text-gray-800">AI Studio API Key(明文)</h3>
|
||
<p class="text-xs text-gray-400 truncate mt-1" x-text="aistudioKeyEmail"></p>
|
||
</div>
|
||
<button type="button" class="chatgpt-session-close" title="关闭"
|
||
@click="showAistudioKeysModal=false" aria-label="关闭">×</button>
|
||
</div>
|
||
<textarea readonly spellcheck="false" class="chatgpt-session-output"
|
||
:value="JSON.stringify(aistudioKeyRecord || {}, null, 2)" aria-label="单个邮箱 AI Studio API Key JSON"></textarea>
|
||
<div class="chatgpt-session-actions">
|
||
<button type="button" @click="showAistudioKeysModal=false" class="btn btn-gray">关闭</button>
|
||
<button type="button" @click="copyAistudioKeys()" class="btn btn-primary"
|
||
:disabled="!aistudioKeyRecord">复制全部</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 复制成功 Toast -->
|
||
<div x-show="copyToastVisible" x-cloak
|
||
style="position:fixed;bottom:32px;left:50%;transform:translateX(-50%);z-index:9999;pointer-events:none"
|
||
class="copy-toast">
|
||
✓ 已复制
|
||
</div>
|
||
|
||
<script src="/app.js"></script>
|
||
|
||
|
||
|
||
<!-- ===== 截图弹窗 ===== -->
|
||
<div x-show="showScreenshotModal" x-cloak
|
||
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-75" @click.self="closeScreenshot()"
|
||
@keydown.escape.window="closeScreenshot()">
|
||
<div class="bg-white rounded-lg shadow-2xl" style="width:90vw;height:90vh;display:flex;flex-direction:column">
|
||
<div class="flex items-center justify-between px-5 py-3 border-b border-gray-200 flex-shrink-0">
|
||
<span class="text-sm font-medium text-gray-700">截图 · <span class="text-gray-400"
|
||
x-text="screenshotEmail"></span></span>
|
||
<button @click="closeScreenshot()" class="text-gray-400 hover:text-gray-600 text-xl leading-none">✕</button>
|
||
</div>
|
||
<div class="p-4 overflow-auto flex-1 flex items-center justify-center">
|
||
<img :src="screenshotUrl" style="max-width:100%;max-height:100%;object-fit:contain"
|
||
class="rounded border border-gray-100" alt="失败截图">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 截图悬浮预览 -->
|
||
<div id="screenshotPreview" x-show="screenshotHover" x-cloak
|
||
style="position:fixed;z-index:9999;pointer-events:none;background:#fff;border-radius:8px;box-shadow:0 8px 30px rgba(0,0,0,.3);padding:4px;max-width:400px;max-height:320px">
|
||
<img :src="screenshotHover" style="max-width:100%;max-height:310px;object-fit:contain;border-radius:6px" alt="预览">
|
||
</div>
|
||
|
||
</body>
|
||
|
||
</html>
|