Files
web-fusion/src/components/Sidebar.vue

251 lines
12 KiB
Vue
Raw Normal View History

2026-02-04 19:56:19 +08:00
<template>
2026-03-03 21:57:18 +08:00
<aside :style="{ width: sidebarWidth + 'px', minWidth: '96px', maxWidth: '400px' }" class="h-full bg-gradient-to-b from-white to-gray-50 border-r border-gray-200 flex flex-col shadow-sm flex-shrink-0">
2026-02-04 19:56:19 +08:00
<!-- 返回和停止按钮 -->
<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 })
},
2026-03-03 21:57:18 +08:00
automationLogs: { type: Array, default: () => [] },
sidebarWidth: { type: Number, default: 144 }
2026-02-04 19:56:19 +08:00
})
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>