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

444 lines
21 KiB
Vue
Raw Normal View History

2026-02-04 19:56:19 +08:00
<template>
2026-03-18 09:09:51 +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)
2026-03-18 20:33:28 +08:00
}} 个运行中</span>
2026-02-04 19:56:19 +08:00
</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">
2026-03-18 09:09:51 +08:00
<div
class="px-3 py-2 text-xs font-semibold text-gray-500 uppercase tracking-wider bg-gray-50 flex justify-between items-center">
2026-02-04 19:56:19 +08:00
<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>
2026-03-18 09:09:51 +08:00
<div v-for="(groupStats, groupName) in statsByGroup" :key="groupName"
class="border-b border-gray-100 last:border-0">
2026-02-04 19:56:19 +08:00
<div class="px-3 py-1.5 bg-gray-100/50 text-xs font-medium text-gray-600">
{{ groupName }}
</div>
2026-03-18 09:09:51 +08:00
<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">
2026-02-04 19:56:19 +08:00
<div class="flex items-center gap-1">
<span class="text-gray-500">视图 {{ stat.viewId }}</span>
2026-03-18 09:09:51 +08:00
<span v-if="stat.unread > 0" class="w-1.5 h-1.5 rounded-full bg-red-500"
:title="`${stat.unread} 条未读消息`"></span>
2026-02-04 19:56:19 +08:00
</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>
<!-- 统计数据 -->
2026-03-12 18:03:30 +08:00
<div class="mt-2 pt-2 border-t border-gray-200 space-y-1 relative">
2026-02-04 19:56:19 +08:00
<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>
2026-03-23 14:46:34 +08:00
2026-02-04 19:56:19 +08:00
<div class="flex items-center justify-between text-xs">
<span class="text-gray-500">已回复</span>
2026-03-23 14:46:34 +08:00
<button @click="showReplyList"
class="text-blue-500 hover:text-blue-600 hover:underline cursor-pointer" title="查看回复列表">
历史回复列表
</button>
2026-03-12 18:03:30 +08:00
<div class="flex items-center gap-1">
2026-03-23 14:46:34 +08:00
2026-03-12 18:03:30 +08:00
<span class="text-emerald-600 font-medium">{{ greetingStats.replyCount || 0 }} </span>
</div>
</div>
2026-03-23 14:46:34 +08:00
<div class="flex items-center justify-between text-xs">
<span class="text-gray-500">已邀请</span>
<button @click="showInviteList"
class="text-purple-500 hover:text-purple-600 hover:underline cursor-pointer" title="查看邀请列表">
回复邀请列表
</button>
<div class="flex items-center gap-1">
<span class="text-purple-600 font-medium">{{ greetingStats.inviteCount || 0 }} </span>
</div>
</div>
2026-03-12 18:03:30 +08:00
<!-- 回复列表弹出框 -->
2026-03-18 09:09:51 +08:00
<div v-if="replyListVisible"
class="absolute left-0 right-0 bottom-full mb-1 bg-white border border-gray-200 rounded-lg shadow-lg z-50 max-h-48 overflow-hidden"
:style="{ maxWidth: sidebarWidth + 'px' }">
2026-03-12 18:03:30 +08:00
<div class="flex items-center justify-between px-3 py-2 border-b border-gray-100 bg-gray-50">
<span class="text-xs font-semibold text-gray-600">历史回复列表</span>
<button @click="replyListVisible = false" class="text-gray-400 hover:text-gray-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
2026-03-18 09:09:51 +08:00
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12" />
2026-03-12 18:03:30 +08:00
</svg>
</button>
</div>
<div class="overflow-y-auto max-h-36">
<div v-if="repliedSessions.length === 0" class="text-gray-400 text-xs text-center py-3">
暂无回复记录
</div>
2026-03-23 14:46:34 +08:00
<div v-els e>
2026-03-18 09:09:51 +08:00
<div v-for="(session, index) in repliedSessions" :key="index"
class="px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-50 border-b border-gray-50 last:border-0 flex items-center justify-between">
2026-03-18 20:33:28 +08:00
<div class="truncate w-16" :title="session.name">
2026-03-18 09:09:51 +08:00
{{ session.name }}
</div>
<div class="flex items-center gap-2">
2026-03-18 20:33:28 +08:00
<span class="text-gray-400 text-[10px]">
视图: {{ session.viewId }}
</span>
<button @click="copyAnchorId(session.anchorId)"
2026-03-18 09:09:51 +08:00
class="text-blue-500 hover:text-blue-600 p-1 rounded hover:bg-blue-50 transition-colors"
title="复制 ID">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</button>
</div>
2026-03-12 18:03:30 +08:00
</div>
</div>
</div>
2026-02-04 19:56:19 +08:00
</div>
2026-03-23 14:46:34 +08:00
<!-- 邀请列表弹出框 -->
<div v-if="inviteListVisible"
class="absolute left-0 right-0 bottom-full mb-1 bg-white border border-gray-200 rounded-lg shadow-lg z-50 max-h-48 overflow-hidden"
:style="{ maxWidth: sidebarWidth + 'px' }">
<div class="flex items-center justify-between px-3 py-2 border-b border-gray-100 bg-gray-50">
<span class="text-xs font-semibold text-purple-600">邀请列表</span>
<button @click="inviteListVisible = false" class="text-gray-400 hover:text-gray-600">
<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="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="overflow-y-auto max-h-36">
<div v-if="invitedSessions.length === 0" class="text-gray-400 text-xs text-center py-3">
暂无邀请记录
</div>
<div v-else>
<div v-for="(session, index) in invitedSessions" :key="index"
class="px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-50 border-b border-gray-50 last:border-0 flex items-center justify-between">
<div class="truncate w-16" :title="session.name">
{{ session.name }}
</div>
<div class="flex items-center gap-2">
<span class="text-gray-400 text-[10px]">
视图: {{ session.viewId }}
</span>
<button @click="copyAnchorId(session.anchorId)"
class="text-purple-500 hover:text-purple-600 p-1 rounded hover:bg-purple-50 transition-colors"
title="复制 ID">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
2026-02-04 19:56:19 +08:00
</div>
</div>
</aside>
</template>
<script setup>
import { ref, computed, watch, onUnmounted } from 'vue'
2026-03-18 09:09:51 +08:00
import { ElMessage } from 'element-plus'
2026-02-04 19:56:19 +08:00
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'])
2026-03-12 18:03:30 +08:00
// 回复列表相关
const replyListVisible = ref(false)
2026-03-23 14:46:34 +08:00
const inviteListVisible = ref(false)
/** @type {import('vue').Ref<Array<{name: string; anchorId: string; viewId: number; invited: boolean}>>} */
2026-03-12 18:03:30 +08:00
const repliedSessions = ref([])
2026-03-23 14:46:34 +08:00
/** @type {import('vue').Ref<Array<{name: string; anchorId: string; viewId: number; invited: boolean}>>} */
const invitedSessions = ref([])
2026-03-12 18:03:30 +08:00
// 显示回复列表
const showReplyList = async () => {
replyListVisible.value = !replyListVisible.value
2026-03-23 14:46:34 +08:00
inviteListVisible.value = false
2026-03-12 18:03:30 +08:00
if (replyListVisible.value && window.electronAPI?.getRepliedSessions) {
try {
const result = await window.electronAPI.getRepliedSessions()
2026-03-18 20:33:28 +08:00
console.log("回复列表里是", result)
// 按倒序展示,最新的在最前面
2026-03-23 14:46:34 +08:00
// 过滤出未邀请的会话
repliedSessions.value = (result || []).filter(session => !session.invited).reverse()
2026-03-12 18:03:30 +08:00
} catch (e) {
console.error('获取回复列表失败:', e)
repliedSessions.value = []
}
}
}
2026-03-23 14:46:34 +08:00
// 显示邀请列表
const showInviteList = async () => {
inviteListVisible.value = !inviteListVisible.value
replyListVisible.value = false
if (inviteListVisible.value && window.electronAPI?.getRepliedSessions) {
try {
const result = await window.electronAPI.getRepliedSessions()
console.log("邀请列表里是", result)
// 按倒序展示,最新的在最前面
// 过滤出已邀请的会话
invitedSessions.value = (result || []).filter(session => session.invited).reverse()
} catch (e) {
console.error('获取邀请列表失败:', e)
invitedSessions.value = []
}
}
}
2026-03-18 09:09:51 +08:00
// 复制主播 ID
const copyAnchorId = (id) => {
if (navigator.clipboard && window.isSecureContext) {
// 现代浏览器使用 Clipboard API
navigator.clipboard.writeText(id).then(() => {
showCopySuccess()
}).catch(err => {
console.error('复制失败:', err)
// 回退到传统方法
fallbackCopyTextToClipboard(id)
})
} else {
// 传统方法
fallbackCopyTextToClipboard(id)
}
}
// 回退复制方法(兼容旧浏览器)
const fallbackCopyTextToClipboard = (text) => {
const textArea = document.createElement('textarea')
textArea.value = text
textArea.style.position = 'fixed'
textArea.style.left = '-999999px'
textArea.style.top = '-999999px'
document.body.appendChild(textArea)
textArea.focus()
textArea.select()
try {
const successful = document.execCommand('copy')
if (successful) {
showCopySuccess()
} else {
console.error('复制命令执行失败')
}
} catch (err) {
console.error('回退复制失败:', err)
} finally {
document.body.removeChild(textArea)
}
}
// 复制成功提示
const showCopySuccess = () => {
// 回退到 alert
ElMessage.success('复制成功!')
}
2026-02-04 19:56:19 +08:00
// 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>