250 lines
11 KiB
Vue
250 lines
11 KiB
Vue
|
|
<template>
|
|||
|
|
<aside class="w-[200px] h-full bg-gradient-to-b from-white to-gray-50 border-r border-gray-200 flex flex-col shadow-sm">
|
|||
|
|
<!-- 返回和停止按钮 -->
|
|||
|
|
<div class="m-3 mb-0 flex gap-2">
|
|||
|
|
<button @click="onGoBack"
|
|||
|
|
class="flex-1 px-3 py-2 text-xs bg-gray-100 text-gray-700 border border-gray-200 rounded-lg hover:bg-gray-200 transition-colors text-left">
|
|||
|
|
← 返回
|
|||
|
|
</button>
|
|||
|
|
<button @click="onStopAll"
|
|||
|
|
class="px-3 py-2 text-xs bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
|
|||
|
|
title="停止所有任务并清空缓存">
|
|||
|
|
停止全部
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Logo / 标题 -->
|
|||
|
|
<div class="p-4 border-b border-gray-200">
|
|||
|
|
<h1 class="text-lg font-bold bg-gradient-to-r from-blue-600 to-cyan-500 bg-clip-text text-transparent">
|
|||
|
|
多视图浏览器
|
|||
|
|
</h1>
|
|||
|
|
<p class="text-xs text-gray-500 mt-1">9 个独立浏览器视图</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 标签页列表 -->
|
|||
|
|
<nav class="flex-1 p-3 space-y-2 overflow-auto">
|
|||
|
|
<div class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">
|
|||
|
|
账号组
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<button v-for="(tab, tabIndex) in tabs" :key="tab.id" @click="onTabSwitch(tab.id)" :disabled="isLoading"
|
|||
|
|
:class="[
|
|||
|
|
'w-full px-3 py-2.5 rounded-lg text-left transition-all duration-200 flex flex-col',
|
|||
|
|
currentTab === tab.id ? 'bg-blue-50 text-blue-700 border border-blue-200 shadow-sm' : 'text-gray-600 hover:bg-gray-100 border border-transparent',
|
|||
|
|
isLoading ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'
|
|||
|
|
]">
|
|||
|
|
<!-- 第一行:组名 + 运行模式 + 活跃组运行时间 -->
|
|||
|
|
<div class="flex items-center justify-between w-full">
|
|||
|
|
<div class="flex items-center gap-2">
|
|||
|
|
<span class="font-medium text-sm">{{ getGroup(tabIndex)?.name || tab.label }}</span>
|
|||
|
|
<span v-if="rotationStatus?.enabled"
|
|||
|
|
:class="['px-1.5 py-0.5 text-[10px] font-bold rounded', isActiveGroup(getGroup(tabIndex)?.name || tab.label) ? 'bg-emerald-500 text-white' : 'bg-gray-200 text-gray-600']">
|
|||
|
|
{{ isActiveGroup(getGroup(tabIndex)?.name || tab.label) ? '全功能' : '仅回复' }}
|
|||
|
|
</span>
|
|||
|
|
<!-- 活跃组显示运行时间 -->
|
|||
|
|
<span v-if="isActiveGroup(getGroup(tabIndex)?.name || tab.label) && rotationStatus?.enabled"
|
|||
|
|
class="text-[10px] text-white font-mono bg-blue-500 px-1.5 py-0.5 rounded shadow-sm">
|
|||
|
|
{{ elapsedTime }}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 第二行:运行账号数 / 视图ID -->
|
|||
|
|
<div class="flex items-center justify-between w-full mt-1.5 text-xs">
|
|||
|
|
<div class="flex items-center gap-1.5">
|
|||
|
|
<template v-if="getRunningAccounts(getGroup(tabIndex)?.name || tab.label) > 0">
|
|||
|
|
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
|
|||
|
|
<span class="text-emerald-600">{{ getRunningAccounts(getGroup(tabIndex)?.name ||
|
|||
|
|
tab.label)
|
|||
|
|
}} 个运行中</span>
|
|||
|
|
</template>
|
|||
|
|
<template v-else>
|
|||
|
|
<span class="text-gray-500">{{ getTotalAccounts(tabIndex) }} 个账号</span>
|
|||
|
|
</template>
|
|||
|
|
</div>
|
|||
|
|
<span class="text-gray-400 text-[10px]">
|
|||
|
|
视图 {{ tabIndex * 3 + 1 }},{{ tabIndex * 3 + 2 }},{{ tabIndex * 3 + 3 }}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</button>
|
|||
|
|
</nav>
|
|||
|
|
|
|||
|
|
<!-- 详细统计 -->
|
|||
|
|
<div class="flex-1 min-h-0 border-t border-gray-200 flex flex-col">
|
|||
|
|
<div class="px-3 py-2 text-xs font-semibold text-gray-500 uppercase tracking-wider bg-gray-50 flex justify-between items-center">
|
|||
|
|
<span>详细统计</span>
|
|||
|
|
<span class="text-[10px] font-normal text-gray-400">招呼/邀请/回复</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="flex-1 overflow-y-auto bg-gray-50/50">
|
|||
|
|
<div v-if="!greetingStats.details || greetingStats.details.length === 0"
|
|||
|
|
class="text-gray-400 text-xs text-center py-4">
|
|||
|
|
暂无统计数据
|
|||
|
|
</div>
|
|||
|
|
<template v-else>
|
|||
|
|
<div v-for="(groupStats, groupName) in statsByGroup" :key="groupName" class="border-b border-gray-100 last:border-0">
|
|||
|
|
<div class="px-3 py-1.5 bg-gray-100/50 text-xs font-medium text-gray-600">
|
|||
|
|
{{ groupName }}
|
|||
|
|
</div>
|
|||
|
|
<div v-for="stat in groupStats" :key="stat.viewId" class="px-3 py-1.5 flex items-center justify-between hover:bg-white transition-colors text-xs">
|
|||
|
|
<div class="flex items-center gap-1">
|
|||
|
|
<span class="text-gray-500">视图 {{ stat.viewId }}</span>
|
|||
|
|
<span v-if="stat.unread > 0" class="w-1.5 h-1.5 rounded-full bg-red-500" :title="`${stat.unread} 条未读消息`"></span>
|
|||
|
|
</div>
|
|||
|
|
<div class="flex items-center gap-3 font-mono text-gray-700">
|
|||
|
|
<span class="text-blue-600 w-6 text-right">{{ stat.greeting }}</span>
|
|||
|
|
<span class="text-gray-300">/</span>
|
|||
|
|
<span class="text-purple-600 w-6 text-right">{{ stat.invite }}</span>
|
|||
|
|
<span class="text-gray-300">/</span>
|
|||
|
|
<span class="text-emerald-600 w-6 text-right">{{ stat.reply }}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 底部运行状态 -->
|
|||
|
|
<div class="p-3 border-t border-gray-200 bg-gray-50">
|
|||
|
|
<div v-if="rotationStatus?.enabled" class="space-y-2">
|
|||
|
|
<div class="flex items-center justify-between text-xs">
|
|||
|
|
<span class="text-gray-500">当前活跃组</span>
|
|||
|
|
<span class="text-emerald-600 font-medium">{{ rotationStatus.currentActiveGroup }}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="flex items-center justify-between text-xs">
|
|||
|
|
<span class="text-gray-500">总运行时间</span>
|
|||
|
|
<span class="text-blue-600 font-mono">{{ totalElapsedTime }}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="flex items-center justify-between text-xs">
|
|||
|
|
<span class="text-gray-500">总运行账号</span>
|
|||
|
|
<span class="text-gray-700">{{ rotationStatus.instanceModes.length }} 个</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div v-else class="text-center text-xs text-gray-400">
|
|||
|
|
未启动任务
|
|||
|
|
</div>
|
|||
|
|
<!-- 统计数据 -->
|
|||
|
|
<div class="mt-2 pt-2 border-t border-gray-200 space-y-1">
|
|||
|
|
<div class="flex items-center justify-between text-xs">
|
|||
|
|
<span class="text-gray-500">已打招呼</span>
|
|||
|
|
<span class="text-blue-600 font-medium">{{ greetingStats.greetingCount }} 位</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="flex items-center justify-between text-xs">
|
|||
|
|
<span class="text-gray-500">已发邀请</span>
|
|||
|
|
<span class="text-purple-600 font-medium">{{ greetingStats.inviteCount }} 个</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="flex items-center justify-between text-xs">
|
|||
|
|
<span class="text-gray-500">已回复</span>
|
|||
|
|
<span class="text-emerald-600 font-medium">{{ greetingStats.replyCount || 0 }} 条</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</aside>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import { ref, computed, watch, onUnmounted } from 'vue'
|
|||
|
|
|
|||
|
|
const props = defineProps({
|
|||
|
|
tabs: { type: Array, required: true },
|
|||
|
|
currentTab: { type: String, required: true },
|
|||
|
|
isLoading: { type: Boolean, default: false },
|
|||
|
|
accountGroups: { type: Array, default: () => [] },
|
|||
|
|
rotationStatus: { type: Object, default: undefined },
|
|||
|
|
greetingStats: {
|
|||
|
|
type: Object,
|
|||
|
|
default: () => ({ greetingCount: 0, inviteCount: 0 })
|
|||
|
|
},
|
|||
|
|
automationLogs: { type: Array, default: () => [] }
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const emit = defineEmits(['tabSwitch', 'goBack', 'stopAll'])
|
|||
|
|
|
|||
|
|
// Event handlers
|
|||
|
|
const onTabSwitch = (id) => emit('tabSwitch', id)
|
|||
|
|
const onGoBack = () => emit('goBack')
|
|||
|
|
const onStopAll = () => emit('stopAll')
|
|||
|
|
|
|||
|
|
// Helper functions
|
|||
|
|
const getGroup = (index) => props.accountGroups[index]
|
|||
|
|
|
|||
|
|
const statsByGroup = computed(() => {
|
|||
|
|
const map = {}
|
|||
|
|
if (props.greetingStats?.details) {
|
|||
|
|
props.greetingStats.details.forEach(stat => {
|
|||
|
|
const groupName = stat.group || '未分组'
|
|||
|
|
if (!map[groupName]) map[groupName] = []
|
|||
|
|
map[groupName].push(stat)
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
return map
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const isActiveGroup = (groupName) => {
|
|||
|
|
if (!props.rotationStatus?.enabled) return false
|
|||
|
|
return props.rotationStatus.currentActiveGroup === groupName
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const getRunningAccounts = (groupName) => {
|
|||
|
|
return props.rotationStatus?.instanceModes.filter(i => i.group === groupName).length || 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const getTotalAccounts = (index) => {
|
|||
|
|
return props.accountGroups[index]?.accounts?.filter(a => a.email && a.pwd).length || 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const formatTime = (timestamp) => {
|
|||
|
|
return new Date(timestamp).toLocaleTimeString('zh-CN', { hour12: false })
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Timer logic
|
|||
|
|
const elapsedTime = ref('00:00')
|
|||
|
|
const totalElapsedTime = ref('00:00')
|
|||
|
|
|
|||
|
|
let timer1 = null
|
|||
|
|
let timer2 = null
|
|||
|
|
|
|||
|
|
watch(() => props.rotationStatus?.modeStartTime, (newVal) => {
|
|||
|
|
if (timer1) clearInterval(timer1)
|
|||
|
|
if (!newVal) {
|
|||
|
|
elapsedTime.value = '00:00'
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const update = () => {
|
|||
|
|
const elapsed = Math.floor((Date.now() - newVal) / 1000)
|
|||
|
|
const minutes = Math.floor(elapsed / 60).toString().padStart(2, '0')
|
|||
|
|
const seconds = (elapsed % 60).toString().padStart(2, '0')
|
|||
|
|
elapsedTime.value = `${minutes}:${seconds}`
|
|||
|
|
}
|
|||
|
|
update()
|
|||
|
|
timer1 = setInterval(update, 1000)
|
|||
|
|
}, { immediate: true })
|
|||
|
|
|
|||
|
|
watch(() => props.rotationStatus?.totalStartTime, (newVal) => {
|
|||
|
|
if (timer2) clearInterval(timer2)
|
|||
|
|
if (!newVal) {
|
|||
|
|
totalElapsedTime.value = '00:00'
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const update = () => {
|
|||
|
|
const elapsed = Math.floor((Date.now() - (newVal || 0)) / 1000)
|
|||
|
|
const hours = Math.floor(elapsed / 3600)
|
|||
|
|
const minutes = Math.floor((elapsed % 3600) / 60).toString().padStart(2, '0')
|
|||
|
|
const seconds = (elapsed % 60).toString().padStart(2, '0')
|
|||
|
|
if (hours > 0) {
|
|||
|
|
totalElapsedTime.value = `${hours}:${minutes}:${seconds}`
|
|||
|
|
} else {
|
|||
|
|
totalElapsedTime.value = `${minutes}:${seconds}`
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
update()
|
|||
|
|
timer2 = setInterval(update, 1000)
|
|||
|
|
}, { immediate: true })
|
|||
|
|
|
|||
|
|
onUnmounted(() => {
|
|||
|
|
if (timer1) clearInterval(timer1)
|
|||
|
|
if (timer2) clearInterval(timer2)
|
|||
|
|
})
|
|||
|
|
</script>
|