2026-02-04 19:56:19 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="min-h-screen overflow-auto bg-gradient-to-br from-slate-100 to-slate-200 p-6">
|
|
|
|
|
|
<div class="max-w-5xl mx-auto pb-8">
|
|
|
|
|
|
<!-- 白色卡片容器 -->
|
|
|
|
|
|
<div class="bg-white rounded-2xl shadow-xl p-8">
|
|
|
|
|
|
<!-- 顶部标题栏 -->
|
|
|
|
|
|
<div class="flex items-end justify-between mb-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h1 class="text-2xl font-semibold text-gray-900">自动私信工作台</h1>
|
|
|
|
|
|
<p class="text-sm text-gray-500 mt-1">
|
|
|
|
|
|
配置账号 · 设置 AI 回复策略 · 一键运行任务
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
|
<span :class="[
|
|
|
|
|
|
'px-3 py-1 rounded-full text-xs font-medium',
|
|
|
|
|
|
aiConfigured ? 'bg-green-100 text-green-700' : 'bg-yellow-100 text-yellow-700'
|
|
|
|
|
|
]">
|
|
|
|
|
|
AI 人设:{{ aiConfigured ? '已配置' : '未配置' }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span v-if="expireTime"
|
|
|
|
|
|
class="text-xs text-gray-600 bg-orange-50 px-3 py-1 rounded-full border border-orange-200">
|
|
|
|
|
|
到期时间:{{ expireTime }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="text-xs text-gray-500 bg-gray-100 px-3 py-1 rounded-full">
|
|
|
|
|
|
{{ currentTime }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<!-- 分隔线 -->
|
|
|
|
|
|
<span class="w-px h-6 bg-gray-200" />
|
|
|
|
|
|
<!-- 退出登录 -->
|
|
|
|
|
|
<button @click="emit('logout')"
|
|
|
|
|
|
class="px-3 py-1.5 text-xs text-red-600 bg-red-50 hover:bg-red-100 border border-red-200 rounded-lg transition-colors">
|
|
|
|
|
|
退出登录
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<!-- 打开浏览器 -->
|
|
|
|
|
|
<button @click="emit('goToBrowser')"
|
|
|
|
|
|
class="px-4 py-2 text-sm bg-gradient-to-r from-blue-500 to-cyan-500 text-white rounded-lg hover:from-blue-600 hover:to-cyan-600 transition-all shadow-sm flex items-center gap-2">
|
|
|
|
|
|
<span>打开浏览器</span>
|
|
|
|
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
|
|
|
|
d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 提示条 -->
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="flex items-center gap-2 mb-6 px-4 py-2 rounded-full text-sm text-gray-700 bg-gradient-to-r from-blue-50 to-green-50 border border-blue-200 w-fit">
|
|
|
|
|
|
<span class="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
|
|
|
|
|
|
建议:最多配置 3 个账号轮询发送,开启 AI 自动回复可提升转化率。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="grid grid-cols-3 gap-6">
|
|
|
|
|
|
<!-- 左侧:运行配置 -->
|
|
|
|
|
|
<div class="col-span-2">
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="bg-gradient-to-b from-white to-gray-50 rounded-xl border border-gray-200 shadow-sm p-6">
|
|
|
|
|
|
<!-- 卡片头部 -->
|
|
|
|
|
|
<div class="flex items-center justify-between mb-6">
|
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<span
|
|
|
|
|
|
class="w-1 h-4 rounded-full bg-gradient-to-b from-blue-500 to-green-500" />
|
|
|
|
|
|
<span class="font-medium text-gray-900">运行配置</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="!isRunning" class="relative group">
|
|
|
|
|
|
<button @click="handleStart()"
|
|
|
|
|
|
class="flex items-center gap-2 px-4 py-2 rounded-full text-sm font-medium bg-gradient-to-r from-blue-500 to-blue-600 text-white hover:from-blue-400 hover:to-blue-500 transition-all shadow-sm">
|
|
|
|
|
|
<span class="w-2 h-2 rounded-full bg-green-400 animate-pulse" />
|
|
|
|
|
|
开始运行
|
|
|
|
|
|
<!-- 下拉箭头 -->
|
|
|
|
|
|
<svg class="w-4 h-4 ml-1 opacity-80" fill="none" viewBox="0 0 24 24"
|
|
|
|
|
|
stroke="currentColor">
|
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
|
|
|
|
d="M19 9l-7 7-7-7" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 下拉菜单 -->
|
|
|
|
|
|
<div class="absolute top-full right-0 pt-2 w-40 hidden group-hover:block z-20">
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="bg-white rounded-xl shadow-xl border border-gray-100 overflow-hidden transform transition-all duration-200 origin-top-right">
|
|
|
|
|
|
<div class="py-1">
|
|
|
|
|
|
<button v-for="(group, index) in visibleGroups" :key="index"
|
|
|
|
|
|
@click="handleStart(config.accountGroups.indexOf(group))"
|
|
|
|
|
|
class="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600 transition-colors flex items-center justify-between group/item">
|
|
|
|
|
|
<span>运行 {{ group.name }}</span>
|
|
|
|
|
|
<span
|
|
|
|
|
|
class="text-xs text-gray-400 group-hover/item:text-blue-400">
|
|
|
|
|
|
{{ config.accountGroups.indexOf(group) + 1 }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button v-else @click="handleStop"
|
|
|
|
|
|
class="flex items-center gap-2 px-4 py-2 rounded-full text-sm font-medium bg-gradient-to-r from-red-500 to-red-600 text-white hover:from-red-400 hover:to-red-500 transition-all shadow-sm">
|
|
|
|
|
|
<span class="w-2 h-2 rounded-full bg-white animate-pulse" />
|
|
|
|
|
|
停止运行
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 账号组 -->
|
|
|
|
|
|
<div class="space-y-4">
|
|
|
|
|
|
<label class="text-sm font-medium text-gray-700">账号组(最多3组)</label>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-for="(group, gIndex) in visibleGroups" :key="gIndex" :class="[
|
|
|
|
|
|
'rounded-lg p-4 border-2 transition-all duration-300',
|
|
|
|
|
|
isGroupActive(group) ? 'bg-blue-50 border-blue-400 shadow-md' : hasRunningAccounts(group) ? 'bg-gray-50 border-gray-300' : 'bg-gray-50 border-gray-200'
|
|
|
|
|
|
]">
|
|
|
|
|
|
<div class="flex items-center justify-between mb-3">
|
|
|
|
|
|
<span class="text-sm font-medium text-gray-800 flex items-center gap-2">
|
|
|
|
|
|
<input type="text" :value="getGroupName(group, gIndex)"
|
|
|
|
|
|
@input="updateGroupName($event.target.value, gIndex)"
|
|
|
|
|
|
:placeholder="`第${gIndex + 1}组`"
|
|
|
|
|
|
class="w-24 px-2 py-1 text-sm text-gray-900 font-medium border border-gray-300 rounded bg-white hover:border-blue-400 focus:border-blue-500 focus:outline-none" />
|
|
|
|
|
|
<span v-if="isRunning && currentGroupIndex === gIndex"
|
|
|
|
|
|
class="px-2 py-0.5 rounded text-xs bg-green-500 text-white">
|
|
|
|
|
|
运行中
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<!-- 运行模式标签 -->
|
|
|
|
|
|
<span v-if="getGroupMode(group)"
|
|
|
|
|
|
:class="['px-2 py-0.5 rounded text-xs', getGroupMode(group) === 'active' ? 'bg-emerald-100 text-emerald-700' : 'bg-gray-200 text-gray-600']">
|
|
|
|
|
|
{{ getGroupMode(group) === 'active' ? '全功能' : '仅回复' }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<!-- 活跃组标记 + 运行时间 -->
|
|
|
|
|
|
<span v-if="isGroupActive(group)"
|
|
|
|
|
|
class="px-2 py-0.5 rounded text-xs bg-blue-100 text-blue-700 flex items-center gap-1">
|
|
|
|
|
|
活跃
|
|
|
|
|
|
<span class="font-mono">{{ elapsedTime }}</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<button @click="addAccount(gIndex)" :disabled="group.accounts.length >= 3"
|
|
|
|
|
|
class="text-xs text-blue-600 hover:text-blue-700 disabled:opacity-50">
|
|
|
|
|
|
新增账号
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-for="(acc, aIndex) in group.accounts" :key="aIndex"
|
|
|
|
|
|
class="flex items-center gap-2 mb-2 p-2 rounded-lg bg-white border border-gray-200">
|
|
|
|
|
|
<input type="email" placeholder="邮箱" :value="acc.email"
|
|
|
|
|
|
@input="updateAccount(gIndex, aIndex, 'email', $event.target.value)"
|
|
|
|
|
|
class="flex-1 px-3 py-2 text-sm text-gray-900 rounded-lg border border-gray-300 focus:border-blue-500 focus:outline-none" />
|
|
|
|
|
|
<div class="flex-1 relative">
|
|
|
|
|
|
<input :type="showPasswordMap[`${gIndex}-${aIndex}`] ? 'text' : 'password'"
|
|
|
|
|
|
placeholder="密码" :value="acc.pwd"
|
|
|
|
|
|
@input="updateAccount(gIndex, aIndex, 'pwd', $event.target.value)"
|
|
|
|
|
|
class="w-full px-3 py-2 text-sm text-gray-900 rounded-lg border border-gray-300 focus:border-blue-500 focus:outline-none pr-10" />
|
|
|
|
|
|
<button @click="togglePasswordVisibility(gIndex, aIndex)"
|
|
|
|
|
|
class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 focus:outline-none p-1">
|
|
|
|
|
|
<svg v-if="showPasswordMap[`${gIndex}-${aIndex}`]" class="w-4 h-4"
|
|
|
|
|
|
fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
|
|
|
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round"
|
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
|
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<svg v-else class="w-4 h-4" fill="none" viewBox="0 0 24 24"
|
|
|
|
|
|
stroke="currentColor">
|
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
|
|
|
|
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- 账号运行状态 -->
|
|
|
|
|
|
<span v-if="getAccountStatus(gIndex, aIndex)"
|
|
|
|
|
|
:class="['px-2 py-1 rounded text-xs font-medium whitespace-nowrap', getAccountStatus(gIndex, aIndex).mode === 'active' ? 'bg-emerald-50 text-emerald-600' : 'bg-gray-100 text-gray-500']">
|
|
|
|
|
|
视图{{ getAccountViewId(gIndex, aIndex) }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<button @click="removeAccount(gIndex, aIndex)"
|
|
|
|
|
|
:disabled="group.accounts.length === 1"
|
|
|
|
|
|
class="text-xs text-red-500 hover:text-red-600 disabled:opacity-50 px-2">
|
|
|
|
|
|
删除
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p class="text-xs text-gray-400 mt-2">每组最多 3 个账号,将按组轮换运行</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 轮换设置 -->
|
|
|
|
|
|
<div class="mt-6 space-y-4">
|
|
|
|
|
|
|
|
|
|
|
|
<div class="flex items-center gap-4">
|
|
|
|
|
|
<label class="text-sm font-medium text-gray-700 w-28">国家语言</label>
|
|
|
|
|
|
<select :value="config.lang || 'en'"
|
|
|
|
|
|
@change="updateConfig('lang', $event.target.value)"
|
|
|
|
|
|
class="flex-1 px-3 py-2 text-sm text-gray-900 rounded-lg border border-gray-300 focus:border-blue-500 focus:outline-none bg-white">
|
|
|
|
|
|
<option v-for="lang in languageList" :key="lang.code" :value="lang.code">
|
|
|
|
|
|
{{ lang.name }} ({{ lang.code }})
|
|
|
|
|
|
</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex items-center gap-4">
|
|
|
|
|
|
<label class="text-sm font-medium text-gray-700 w-28">轮换账号组</label>
|
|
|
|
|
|
<label class="relative inline-flex items-center cursor-pointer">
|
|
|
|
|
|
<input type="checkbox" :checked="config.rotateEnabled"
|
|
|
|
|
|
@change="updateConfig('rotateEnabled', $event.target.checked)"
|
|
|
|
|
|
class="sr-only peer" />
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-500">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<span class="text-xs text-gray-500">关闭时只跑当前组不切换</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<template v-if="config.rotateEnabled">
|
|
|
|
|
|
<div class="flex items-center gap-4">
|
|
|
|
|
|
<label class="text-sm font-medium text-gray-700 w-28">账号组数量</label>
|
|
|
|
|
|
<div class="flex gap-4">
|
|
|
|
|
|
<label v-for="num in [2, 3]" :key="num"
|
|
|
|
|
|
class="flex items-center gap-2 cursor-pointer">
|
|
|
|
|
|
<input type="radio" :checked="config.groupCount === num"
|
|
|
|
|
|
@change="updateConfig('groupCount', num)"
|
|
|
|
|
|
class="w-4 h-4 text-blue-500" />
|
|
|
|
|
|
<span class="text-sm text-gray-700">{{ num }}组</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="flex items-center gap-4">
|
|
|
|
|
|
<label class="text-sm font-medium text-gray-700 w-28">轮换间隔(分钟)</label>
|
|
|
|
|
|
<input type="number" min="1"
|
|
|
|
|
|
:value="config.switchMinutes === 0 ? '' : config.switchMinutes"
|
|
|
|
|
|
@input="handleNumberInput('switchMinutes', $event.target.value)"
|
|
|
|
|
|
@blur="handleNumberBlur('switchMinutes', $event.target.value, 1)"
|
|
|
|
|
|
class="w-20 px-3 py-2 text-sm text-gray-900 rounded-lg border border-gray-300 focus:border-blue-500 focus:outline-none" />
|
|
|
|
|
|
<span class="text-xs text-gray-500">每隔 N 分钟切换到下一组</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- AI 回复 -->
|
|
|
|
|
|
<div class="flex items-center gap-4">
|
|
|
|
|
|
<label class="text-sm font-medium text-gray-700 w-28">AI 自动回复</label>
|
|
|
|
|
|
<label class="relative inline-flex items-center cursor-pointer">
|
|
|
|
|
|
<input type="checkbox" :checked="config.aiReply"
|
|
|
|
|
|
@change="updateConfig('aiReply', $event.target.checked)"
|
|
|
|
|
|
class="sr-only peer" />
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-500">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<span class="text-xs text-gray-500">开启后由 AI 自动根据对话内容生成回复</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 第一条消息 -->
|
|
|
|
|
|
<div class="flex items-center gap-4">
|
|
|
|
|
|
<label class="text-sm font-medium text-gray-700 w-28">第一条消息</label>
|
|
|
|
|
|
<div class="flex gap-4">
|
|
|
|
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
|
|
|
|
<input type="radio" :checked="config.sendInviteFirst === false"
|
|
|
|
|
|
@change="updateConfig('sendInviteFirst', false)"
|
|
|
|
|
|
class="w-4 h-4 text-blue-500" />
|
|
|
|
|
|
<span class="text-sm text-gray-700">打招呼</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
|
|
|
|
<input type="radio" :checked="config.sendInviteFirst === true"
|
|
|
|
|
|
@change="updateConfig('sendInviteFirst', true)"
|
|
|
|
|
|
class="w-4 h-4 text-blue-500" />
|
|
|
|
|
|
<span class="text-sm text-gray-700">发送邀请链接</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 睡眠时间 -->
|
|
|
|
|
|
<div class="flex items-center gap-4">
|
|
|
|
|
|
<label class="text-sm font-medium text-gray-700 w-28">睡眠时间(秒)</label>
|
|
|
|
|
|
<input type="number" min="0" placeholder="0"
|
|
|
|
|
|
:value="config.sleepTime < 0 ? '' : config.sleepTime"
|
|
|
|
|
|
@input="handleSleepTimeInput($event.target.value)"
|
|
|
|
|
|
class="w-20 px-3 py-2 text-sm text-gray-900 rounded-lg border border-gray-300 focus:border-blue-500 focus:outline-none" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 邀请阈值 -->
|
|
|
|
|
|
<div v-if="!config.sendInviteFirst" class="flex items-center gap-4">
|
|
|
|
|
|
<label class="text-sm font-medium text-gray-700 w-28">链接发送时机</label>
|
|
|
|
|
|
<input type="number" min="1"
|
|
|
|
|
|
:value="config.inviteThreshold === 0 ? '' : config.inviteThreshold"
|
|
|
|
|
|
@input="handleNumberInput('inviteThreshold', $event.target.value)"
|
|
|
|
|
|
@blur="handleNumberBlur('inviteThreshold', $event.target.value, 1)"
|
|
|
|
|
|
class="w-20 px-3 py-2 text-sm text-gray-900 rounded-lg border border-gray-300 focus:border-blue-500 focus:outline-none" />
|
|
|
|
|
|
<span class="text-xs text-gray-500">打招呼后回复 N 句再发送邀请链接</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 右侧:快捷操作 -->
|
|
|
|
|
|
<div class="col-span-1">
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="bg-gradient-to-b from-white to-gray-50 rounded-xl border border-gray-200 shadow-sm p-6">
|
|
|
|
|
|
<div class="flex items-center gap-2 mb-6">
|
|
|
|
|
|
<span class="w-1 h-4 rounded-full bg-gradient-to-b from-blue-500 to-green-500" />
|
|
|
|
|
|
<span class="font-medium text-gray-900">快捷操作</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="space-y-2">
|
|
|
|
|
|
<button @click="showHostDialog = true"
|
|
|
|
|
|
class="w-full text-left px-4 py-3 text-sm text-blue-600 hover:bg-blue-50 rounded-lg transition-colors">
|
|
|
|
|
|
执行主播库
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button @click="showGreetingDialog = true"
|
|
|
|
|
|
class="w-full text-left px-4 py-3 text-sm text-blue-600 hover:bg-blue-50 rounded-lg transition-colors">
|
|
|
|
|
|
打招呼内容配置
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button @click="showAIDialog = true"
|
|
|
|
|
|
class="w-full text-left px-4 py-3 text-sm text-blue-600 hover:bg-blue-50 rounded-lg transition-colors">
|
|
|
|
|
|
配置 / 修改 AI 人设
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- AI 配置弹窗 -->
|
|
|
|
|
|
<AIConfigDialog :visible="showAIDialog" :config="aiConfig" @close="showAIDialog = false"
|
|
|
|
|
|
@save="handleSaveAIConfig" @change="(key, value) => aiConfig[key] = value" />
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 主播列表弹窗 -->
|
|
|
|
|
|
<HostListDialog :visible="showHostDialog" @close="showHostDialog = false" @save="() => { }" />
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 打招呼内容弹窗 -->
|
|
|
|
|
|
<GreetingDialog :visible="showGreetingDialog" @close="showGreetingDialog = false" @confirm="handleGreetingConfirm" />
|
2026-02-09 21:04:09 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 预热 Loading 遮罩 -->
|
|
|
|
|
|
<transition name="fade">
|
|
|
|
|
|
<div v-if="warmingUp"
|
|
|
|
|
|
class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/30 backdrop-blur-sm">
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="bg-white/90 rounded-2xl shadow-2xl border border-white/60 px-6 py-5 flex items-center gap-4">
|
|
|
|
|
|
<!-- spinner -->
|
|
|
|
|
|
<div class="w-10 h-10 rounded-full border-4 border-gray-200 border-t-blue-500 animate-spin"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="space-y-1">
|
|
|
|
|
|
<div class="text-sm font-semibold text-gray-900">正在预热视图</div>
|
|
|
|
|
|
<div class="text-xs text-gray-500">
|
|
|
|
|
|
这会提升后台视图渲染稳定性,请稍候…
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 可选:进度小点点动画 -->
|
|
|
|
|
|
<div class="flex gap-1 pt-1">
|
|
|
|
|
|
<span
|
|
|
|
|
|
class="w-1.5 h-1.5 rounded-full bg-blue-400 animate-bounce [animation-delay:-0.2s]"></span>
|
|
|
|
|
|
<span
|
|
|
|
|
|
class="w-1.5 h-1.5 rounded-full bg-blue-400 animate-bounce [animation-delay:-0.1s]"></span>
|
|
|
|
|
|
<span class="w-1.5 h-1.5 rounded-full bg-blue-400 animate-bounce"></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</transition>
|
2026-02-04 19:56:19 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
|
|
|
|
|
import HostListDialog from '../components/HostListDialog.vue'
|
|
|
|
|
|
import GreetingDialog from '../components/GreetingDialog.vue'
|
|
|
|
|
|
import AIConfigDialog from '../components/AIConfigDialog.vue'
|
|
|
|
|
|
import { isElectron } from '../utils/electronBridge'
|
|
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits(['goToBrowser', 'logout'])
|
|
|
|
|
|
|
|
|
|
|
|
const CONFIG_KEY = 'autoDm_runConfig'
|
|
|
|
|
|
|
|
|
|
|
|
// Default Config
|
|
|
|
|
|
const defaultConfig = {
|
|
|
|
|
|
rotateEnabled: false,
|
|
|
|
|
|
groupCount: 1,
|
|
|
|
|
|
accountGroups: [
|
|
|
|
|
|
{ name: '第1组', accounts: [{ email: '', pwd: '' }] },
|
|
|
|
|
|
{ name: '第2组', accounts: [{ email: '', pwd: '' }] },
|
|
|
|
|
|
{ name: '第3组', accounts: [{ email: '', pwd: '' }] },
|
|
|
|
|
|
],
|
|
|
|
|
|
aiReply: true,
|
|
|
|
|
|
sendInviteFirst: false,
|
|
|
|
|
|
sleepTime: 30,
|
|
|
|
|
|
inviteThreshold: 3,
|
|
|
|
|
|
switchMinutes: 60,
|
|
|
|
|
|
prologueList: {},
|
|
|
|
|
|
needTranslate: false,
|
|
|
|
|
|
maxAnchorCount: 100,
|
|
|
|
|
|
lang: 'en'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Language List
|
|
|
|
|
|
const languageList = [
|
|
|
|
|
|
{ name: '中文 (简体)', code: 'zh-CN' },
|
|
|
|
|
|
{ name: 'English', code: 'en' },
|
|
|
|
|
|
{ name: 'ภาษาไทย', code: 'th-TH' },
|
|
|
|
|
|
{ name: 'العربية', code: 'ar' },
|
|
|
|
|
|
{ name: 'Bahasa Indonesia', code: 'id-ID' },
|
|
|
|
|
|
{ name: 'Русский', code: 'ru-RU' },
|
|
|
|
|
|
{ name: 'Tiếng Việt', code: 'vi-VN' },
|
|
|
|
|
|
{ name: 'Bahasa Melayu', code: 'ms-MY' },
|
|
|
|
|
|
{ name: '日本語', code: 'ja-JP' },
|
|
|
|
|
|
{ name: 'Türkçe', code: 'tr-TR' },
|
|
|
|
|
|
{ name: 'Português', code: 'pt-PT' },
|
|
|
|
|
|
{ name: '한국어', code: 'ko-KR' },
|
|
|
|
|
|
{ name: 'Español', code: 'es-ES' },
|
|
|
|
|
|
{ name: '中文 (繁體)', code: 'zh-Hant-TW' },
|
|
|
|
|
|
{ name: 'Deutsch', code: 'de-DE' },
|
|
|
|
|
|
{ name: 'Italiano', code: 'it-IT' },
|
|
|
|
|
|
{ name: 'Français', code: 'fr-FR' },
|
|
|
|
|
|
{ name: 'Română', code: 'ro-RO' },
|
|
|
|
|
|
{ name: 'Polski', code: 'pl-PL' },
|
|
|
|
|
|
{ name: 'Nederlands', code: 'nl-NL' },
|
|
|
|
|
|
{ name: 'Svenska', code: 'sv-SE' },
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
// State
|
|
|
|
|
|
const config = ref(JSON.parse(JSON.stringify(defaultConfig)))
|
|
|
|
|
|
const aiConfig = ref({
|
|
|
|
|
|
agentName: '',
|
|
|
|
|
|
guildName: '',
|
|
|
|
|
|
contactTool: '',
|
|
|
|
|
|
contact: '',
|
|
|
|
|
|
})
|
|
|
|
|
|
const isRunning = ref(false)
|
|
|
|
|
|
const currentGroupIndex = ref(0)
|
|
|
|
|
|
const showAIDialog = ref(false)
|
|
|
|
|
|
const showHostDialog = ref(false)
|
|
|
|
|
|
const showGreetingDialog = ref(false)
|
|
|
|
|
|
const aiConfigured = ref(false)
|
|
|
|
|
|
const configLoaded = ref(false)
|
|
|
|
|
|
const rotationStatus = ref(null)
|
|
|
|
|
|
const elapsedTime = ref('00:00')
|
|
|
|
|
|
const expireTime = ref('')
|
|
|
|
|
|
const currentTime = ref('')
|
|
|
|
|
|
const showPasswordMap = ref({})
|
|
|
|
|
|
|
|
|
|
|
|
const isElectronEnv = isElectron()
|
|
|
|
|
|
let timeInterval = null
|
|
|
|
|
|
let rotationTimer = null
|
|
|
|
|
|
let saveTimer = null
|
|
|
|
|
|
|
|
|
|
|
|
// Computed
|
|
|
|
|
|
const visibleGroups = computed(() => {
|
|
|
|
|
|
return config.value.rotateEnabled
|
|
|
|
|
|
? config.value.accountGroups.slice(0, config.value.groupCount)
|
|
|
|
|
|
: config.value.accountGroups.slice(0, 1)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// Lifecycle
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
updateCurrentTime()
|
|
|
|
|
|
timeInterval = setInterval(updateCurrentTime, 1000)
|
|
|
|
|
|
|
|
|
|
|
|
// Load User Data
|
|
|
|
|
|
try {
|
|
|
|
|
|
const userData = localStorage.getItem('user_data')
|
|
|
|
|
|
if (userData) {
|
|
|
|
|
|
const user = JSON.parse(userData)
|
|
|
|
|
|
if (user.aiExpireTime) {
|
|
|
|
|
|
const timestamp = Number(user.aiExpireTime)
|
|
|
|
|
|
const date = new Date(timestamp)
|
|
|
|
|
|
expireTime.value = date.toLocaleDateString('zh-CN', {
|
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
|
month: '2-digit',
|
|
|
|
|
|
day: '2-digit'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch { }
|
|
|
|
|
|
|
|
|
|
|
|
await loadConfig()
|
|
|
|
|
|
checkAIConfig()
|
|
|
|
|
|
|
|
|
|
|
|
if (isElectronEnv) {
|
|
|
|
|
|
const status = await window.electronAPI.getRotationStatus()
|
|
|
|
|
|
rotationStatus.value = status
|
|
|
|
|
|
if (status?.instanceModes?.length > 0) {
|
|
|
|
|
|
setIsRunning(true)
|
|
|
|
|
|
}
|
|
|
|
|
|
handleStatusChange(status)
|
|
|
|
|
|
|
|
|
|
|
|
const unsubRotation = window.electronAPI.onRotationStatusChanged((status) => {
|
|
|
|
|
|
rotationStatus.value = status
|
|
|
|
|
|
setIsRunning(status?.instanceModes?.length > 0)
|
|
|
|
|
|
handleStatusChange(status)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const unsubSave = window.electronAPI.onRequestSaveConfig(() => {
|
|
|
|
|
|
if (saveTimer) clearTimeout(saveTimer)
|
|
|
|
|
|
saveToLocalStorage()
|
|
|
|
|
|
saveToFile()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// Cleanup function for listeners is not directly supported in onMounted unless we store unsubscribers
|
|
|
|
|
|
// We will just add window listener for unload as in React
|
|
|
|
|
|
window.addEventListener('beforeunload', handleBeforeUnload)
|
|
|
|
|
|
|
|
|
|
|
|
// Store unsubs for unmount
|
|
|
|
|
|
// Note: In Vue component unmount, we should clean up.
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
if (timeInterval) clearInterval(timeInterval)
|
|
|
|
|
|
if (rotationTimer) clearInterval(rotationTimer)
|
|
|
|
|
|
if (saveTimer) clearTimeout(saveTimer)
|
|
|
|
|
|
window.removeEventListener('beforeunload', handleBeforeUnload)
|
|
|
|
|
|
saveToLocalStorage()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const updateCurrentTime = () => {
|
|
|
|
|
|
currentTime.value = new Date().toLocaleString('zh-CN')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Config Loading/Saving
|
|
|
|
|
|
const loadConfig = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (isElectronEnv) {
|
|
|
|
|
|
const saved = await window.electronAPI.loadRunConfig()
|
|
|
|
|
|
if (saved) {
|
|
|
|
|
|
config.value = { ...config.value, ...saved }
|
|
|
|
|
|
localStorage.setItem(CONFIG_KEY, JSON.stringify(saved))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const localSaved = localStorage.getItem(CONFIG_KEY)
|
|
|
|
|
|
if (localSaved) {
|
|
|
|
|
|
config.value = { ...config.value, ...JSON.parse(localSaved) }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const localSaved = localStorage.getItem(CONFIG_KEY)
|
|
|
|
|
|
if (localSaved) {
|
|
|
|
|
|
config.value = { ...config.value, ...JSON.parse(localSaved) }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('加载配置失败:', e)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
configLoaded.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const saveToLocalStorage = () => {
|
|
|
|
|
|
localStorage.setItem(CONFIG_KEY, JSON.stringify(config.value))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const saveToFile = async () => {
|
|
|
|
|
|
if (!isElectronEnv) return
|
|
|
|
|
|
try {
|
2026-02-06 20:03:56 +08:00
|
|
|
|
const configToSave = JSON.parse(JSON.stringify(config.value))
|
|
|
|
|
|
// ConfigPage 不管理 filters,HostListDialog 会单独管理
|
|
|
|
|
|
// 删除 filters 避免用 ConfigPage 中可能过期的状态覆盖后端
|
|
|
|
|
|
delete configToSave.filters
|
|
|
|
|
|
await window.electronAPI.saveRunConfig(configToSave)
|
2026-02-04 19:56:19 +08:00
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('保存配置失败:', e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleBeforeUnload = () => {
|
|
|
|
|
|
saveToLocalStorage()
|
|
|
|
|
|
saveToFile()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Auto Save
|
|
|
|
|
|
watch(config, (newVal) => {
|
|
|
|
|
|
if (!configLoaded.value) return
|
|
|
|
|
|
saveToLocalStorage()
|
|
|
|
|
|
|
|
|
|
|
|
if (saveTimer) clearTimeout(saveTimer)
|
|
|
|
|
|
saveTimer = setTimeout(() => {
|
|
|
|
|
|
saveToFile()
|
|
|
|
|
|
}, 500)
|
|
|
|
|
|
}, { deep: true })
|
|
|
|
|
|
|
|
|
|
|
|
const checkAIConfig = async () => {
|
|
|
|
|
|
if (!isElectronEnv) return
|
|
|
|
|
|
try {
|
|
|
|
|
|
const saved = await window.electronAPI.loadAIConfig()
|
|
|
|
|
|
if (saved && (saved.agentName || saved.guildName || saved.contactTool || saved.contact)) {
|
|
|
|
|
|
aiConfig.value = saved
|
|
|
|
|
|
aiConfigured.value = true
|
|
|
|
|
|
} else {
|
|
|
|
|
|
aiConfigured.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
aiConfigured.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleSaveAIConfig = async () => {
|
|
|
|
|
|
if (isElectronEnv) {
|
|
|
|
|
|
await window.electronAPI.saveAIConfig(JSON.parse(JSON.stringify(aiConfig.value)))
|
|
|
|
|
|
}
|
|
|
|
|
|
aiConfigured.value = true
|
|
|
|
|
|
showAIDialog.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Rotation Timer Logic
|
|
|
|
|
|
const handleStatusChange = (status) => {
|
|
|
|
|
|
if (rotationTimer) {
|
|
|
|
|
|
clearInterval(rotationTimer)
|
|
|
|
|
|
rotationTimer = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (status?.modeStartTime) {
|
|
|
|
|
|
const updateTimer = () => {
|
|
|
|
|
|
const elapsed = Math.floor((Date.now() - status.modeStartTime) / 1000)
|
|
|
|
|
|
const minutes = Math.floor(elapsed / 60).toString().padStart(2, '0')
|
|
|
|
|
|
const seconds = (elapsed % 60).toString().padStart(2, '0')
|
|
|
|
|
|
elapsedTime.value = `${minutes}:${seconds}`
|
|
|
|
|
|
}
|
|
|
|
|
|
updateTimer()
|
|
|
|
|
|
rotationTimer = setInterval(updateTimer, 1000)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
elapsedTime.value = '00:00'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Actions
|
|
|
|
|
|
const updateConfig = (key, value) => {
|
|
|
|
|
|
config.value[key] = value
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const updateGroupName = (val, index) => {
|
|
|
|
|
|
config.value.accountGroups[index].name = val || `第${index + 1}组`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const addAccount = (groupIndex) => {
|
|
|
|
|
|
if (config.value.accountGroups[groupIndex].accounts.length < 3) {
|
|
|
|
|
|
config.value.accountGroups[groupIndex].accounts.push({ email: '', pwd: '' })
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const removeAccount = (groupIndex, accountIndex) => {
|
|
|
|
|
|
if (config.value.accountGroups[groupIndex].accounts.length > 1) {
|
|
|
|
|
|
config.value.accountGroups[groupIndex].accounts.splice(accountIndex, 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const updateAccount = (groupIndex, accountIndex, field, value) => {
|
|
|
|
|
|
config.value.accountGroups[groupIndex].accounts[accountIndex][field] = value
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Input Handlers
|
|
|
|
|
|
const handleNumberInput = (key, val) => {
|
|
|
|
|
|
if (val === '') {
|
|
|
|
|
|
config.value[key] = 0
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const num = parseInt(val)
|
|
|
|
|
|
if (!isNaN(num)) config.value[key] = num
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleNumberBlur = (key, val, min) => {
|
|
|
|
|
|
if (!val || parseInt(val) < min) {
|
|
|
|
|
|
config.value[key] = min
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleSleepTimeInput = (val) => {
|
|
|
|
|
|
if (val === '') {
|
|
|
|
|
|
config.value.sleepTime = -1
|
|
|
|
|
|
} else {
|
|
|
|
|
|
config.value.sleepTime = parseInt(val) || 0
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-09 21:04:09 +08:00
|
|
|
|
const warmingUp = ref(false)
|
2026-02-04 19:56:19 +08:00
|
|
|
|
// Start/Stop
|
|
|
|
|
|
const handleStart = async (specificGroupIndex) => {
|
|
|
|
|
|
const activeGroupIndex = specificGroupIndex ?? 0
|
|
|
|
|
|
const activeGroup = config.value.accountGroups[activeGroupIndex]
|
|
|
|
|
|
|
|
|
|
|
|
if (!activeGroup) return alert('请选择要运行的账号组')
|
|
|
|
|
|
|
|
|
|
|
|
const hasValidAccount = activeGroup.accounts.some(a => a.email && a.pwd)
|
|
|
|
|
|
if (!hasValidAccount) return alert('请至少填写一组有效的账号(邮箱和密码)')
|
|
|
|
|
|
|
|
|
|
|
|
const errors = []
|
|
|
|
|
|
if (config.value.rotateEnabled && (!config.value.switchMinutes || config.value.switchMinutes < 1)) {
|
|
|
|
|
|
errors.push('轮换间隔必须大于 0 分钟')
|
|
|
|
|
|
}
|
|
|
|
|
|
if (config.value.sleepTime < 0) {
|
|
|
|
|
|
errors.push('请填写睡眠时间')
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!config.value.sendInviteFirst && (!config.value.inviteThreshold || config.value.inviteThreshold < 1)) {
|
|
|
|
|
|
errors.push('链接发送时机必须大于 0')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (errors.length > 0) {
|
|
|
|
|
|
return alert('配置检查失败:\n\n' + errors.map((e, i) => `${i + 1}. ${e}`).join('\n'))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!isElectronEnv) return alert('非 Electron 环境无法运行')
|
|
|
|
|
|
|
|
|
|
|
|
// Deep clone to remove Vue reactivity (Proxy)
|
|
|
|
|
|
const prologueList = JSON.parse(JSON.stringify(config.value.prologueList || {}))
|
|
|
|
|
|
const activeGroupName = activeGroup.name
|
|
|
|
|
|
|
|
|
|
|
|
await window.electronAPI.updateAutomationConfig({
|
|
|
|
|
|
aiReplyEnabled: config.value.aiReply,
|
|
|
|
|
|
isGreetFirst: config.value.sendInviteFirst,
|
|
|
|
|
|
sleepTime: config.value.sleepTime,
|
|
|
|
|
|
inviteThreshold: config.value.inviteThreshold,
|
|
|
|
|
|
prologueList,
|
|
|
|
|
|
needTranslate: config.value.needTranslate, // 添加翻译开关配置
|
|
|
|
|
|
maxAnchorCount: config.value.maxAnchorCount,
|
|
|
|
|
|
rotationEnabled: config.value.rotateEnabled,
|
|
|
|
|
|
rotationIntervalMinutes: config.value.switchMinutes,
|
|
|
|
|
|
currentActiveGroup: activeGroupName,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const groupsToStart = config.value.rotateEnabled
|
|
|
|
|
|
? visibleGroups.value.map(g => ({
|
|
|
|
|
|
group: g,
|
|
|
|
|
|
index: config.value.accountGroups.indexOf(g),
|
|
|
|
|
|
isActive: g.name === activeGroupName
|
|
|
|
|
|
}))
|
|
|
|
|
|
: [{ group: activeGroup, index: activeGroupIndex, isActive: true }]
|
|
|
|
|
|
|
|
|
|
|
|
const startTasks = []
|
|
|
|
|
|
for (const { group, index } of groupsToStart) {
|
|
|
|
|
|
const startViewId = index * 3 + 1
|
|
|
|
|
|
let currentViewId = startViewId
|
|
|
|
|
|
for (const acc of group.accounts) {
|
|
|
|
|
|
if (acc.email && acc.pwd && currentViewId < startViewId + 3 && currentViewId <= 9) {
|
|
|
|
|
|
// Deep clone account object to remove Vue reactivity
|
|
|
|
|
|
const cleanAccount = JSON.parse(JSON.stringify(acc))
|
|
|
|
|
|
startTasks.push({
|
|
|
|
|
|
viewId: currentViewId,
|
|
|
|
|
|
account: {
|
|
|
|
|
|
...cleanAccount,
|
|
|
|
|
|
group: group.name,
|
|
|
|
|
|
lang: config.value.lang || 'en' // 传递语言配置
|
|
|
|
|
|
},
|
|
|
|
|
|
delay: Math.random() * 2500 + 500
|
|
|
|
|
|
})
|
|
|
|
|
|
currentViewId++
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-09 21:04:09 +08:00
|
|
|
|
// 预热所有视图,确保后台视图完成渲染,解决自动化执行失败问题
|
|
|
|
|
|
warmingUp.value = true
|
2026-02-06 20:03:56 +08:00
|
|
|
|
try {
|
|
|
|
|
|
console.log('[ConfigPage] 预热所有视图...')
|
|
|
|
|
|
await window.electronAPI.warmUpViews()
|
|
|
|
|
|
console.log('[ConfigPage] 视图预热完成')
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.warn('[ConfigPage] 视图预热失败,继续启动:', e)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 18:46:35 +08:00
|
|
|
|
const results = await Promise.allSettled(
|
2026-02-04 19:56:19 +08:00
|
|
|
|
startTasks.map(async ({ viewId, account, delay }) => {
|
|
|
|
|
|
await new Promise(r => setTimeout(r, delay))
|
|
|
|
|
|
return window.electronAPI.startTikTokAutomation(viewId, account)
|
|
|
|
|
|
})
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-02-05 18:46:35 +08:00
|
|
|
|
// 检查启动结果
|
|
|
|
|
|
const failedResults = results
|
|
|
|
|
|
.map((r, i) => ({ result: r, task: startTasks[i] }))
|
|
|
|
|
|
.filter(({ result }) => {
|
|
|
|
|
|
if (result.status === 'rejected') return true
|
|
|
|
|
|
if (result.status === 'fulfilled' && !result.value.success) return true
|
|
|
|
|
|
return false
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 如果全部失败,显示错误并不跳转
|
|
|
|
|
|
if (failedResults.length === startTasks.length) {
|
|
|
|
|
|
const firstError = failedResults[0]
|
|
|
|
|
|
let errorMsg = '启动失败'
|
|
|
|
|
|
if (firstError.result.status === 'rejected') {
|
|
|
|
|
|
errorMsg = firstError.result.reason?.message || '启动失败'
|
|
|
|
|
|
} else if (firstError.result.status === 'fulfilled') {
|
|
|
|
|
|
errorMsg = firstError.result.value.error || '启动失败'
|
|
|
|
|
|
}
|
2026-03-13 16:08:34 +08:00
|
|
|
|
warmingUp.value = false
|
2026-02-05 18:46:35 +08:00
|
|
|
|
alert(`启动失败:${errorMsg}`)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果部分失败,显示警告但继续
|
|
|
|
|
|
if (failedResults.length > 0) {
|
|
|
|
|
|
console.warn(`部分账号启动失败: ${failedResults.length}/${startTasks.length}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-04 19:56:19 +08:00
|
|
|
|
setIsRunning(true)
|
|
|
|
|
|
currentGroupIndex.value = activeGroupIndex
|
|
|
|
|
|
const status = await window.electronAPI.getRotationStatus()
|
|
|
|
|
|
rotationStatus.value = status
|
|
|
|
|
|
handleStatusChange(status)
|
2026-02-09 21:04:09 +08:00
|
|
|
|
warmingUp.value = false //关闭遮罩
|
2026-02-04 19:56:19 +08:00
|
|
|
|
emit('goToBrowser')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleStop = async () => {
|
|
|
|
|
|
if (!isElectronEnv) return
|
|
|
|
|
|
|
|
|
|
|
|
await Promise.allSettled(
|
|
|
|
|
|
Array.from({ length: 9 }, (_, i) => i + 1).map(viewId =>
|
|
|
|
|
|
window.electronAPI.stopTikTokAutomation(viewId).catch(() => { })
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
await window.electronAPI.updateAutomationConfig({ rotationEnabled: false })
|
|
|
|
|
|
if (window.electronAPI.clearAllCache) await window.electronAPI.clearAllCache()
|
|
|
|
|
|
|
|
|
|
|
|
setIsRunning(false)
|
|
|
|
|
|
rotationStatus.value = null
|
|
|
|
|
|
elapsedTime.value = '00:00'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleGreetingConfirm = (data) => {
|
|
|
|
|
|
const newPrologueList = {
|
|
|
|
|
|
yolo: data.sentences,
|
|
|
|
|
|
...data.translations
|
|
|
|
|
|
}
|
|
|
|
|
|
config.value.prologueList = newPrologueList
|
|
|
|
|
|
config.value.needTranslate = data.needTranslate
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Helpers for Template
|
|
|
|
|
|
const getGroupName = (group, index) => {
|
|
|
|
|
|
if (group.name.startsWith('第') && group.name.endsWith('组')) return ''
|
|
|
|
|
|
return group.name
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const getGroupMode = (group) => {
|
|
|
|
|
|
return rotationStatus.value?.instanceModes.find(i => i.group === group.name)?.mode
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const isGroupActive = (group) => {
|
|
|
|
|
|
return rotationStatus.value?.enabled && rotationStatus.value?.currentActiveGroup === group.name
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const hasRunningAccounts = (group) => {
|
|
|
|
|
|
return rotationStatus.value?.instanceModes.some(i => i.group === group.name)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const getAccountViewId = (groupIndex, accountIndex) => {
|
|
|
|
|
|
return groupIndex * 3 + accountIndex + 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const getAccountStatus = (groupIndex, accountIndex) => {
|
|
|
|
|
|
const viewId = getAccountViewId(groupIndex, accountIndex)
|
|
|
|
|
|
return rotationStatus.value?.instanceModes.find(i => i.viewId === viewId)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function setIsRunning(val) {
|
|
|
|
|
|
isRunning.value = val
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const togglePasswordVisibility = (gIndex, aIndex) => {
|
|
|
|
|
|
const key = `${gIndex}-${aIndex}`
|
|
|
|
|
|
showPasswordMap.value[key] = !showPasswordMap.value[key]
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
2026-02-09 21:04:09 +08:00
|
|
|
|
<style scoped>
|
|
|
|
|
|
.fade-enter-active,
|
|
|
|
|
|
.fade-leave-active {
|
|
|
|
|
|
transition: opacity 0.2s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.fade-enter-from,
|
|
|
|
|
|
.fade-leave-to {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|