大哥 主播 即时消息 三合一
This commit is contained in:
249
src/components/Sidebar.vue
Normal file
249
src/components/Sidebar.vue
Normal file
@@ -0,0 +1,249 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user