220 lines
8.0 KiB
Vue
220 lines
8.0 KiB
Vue
<template>
|
||
<div class="flex h-full w-full bg-gradient-to-br from-gray-50 to-gray-100 animate-fadeIn">
|
||
<!-- 侧边栏 -->
|
||
<Sidebar :tabs="tabs" :current-tab="currentTab" @tab-switch="handleTabSwitch" @go-back="handleGoToConfig"
|
||
@stop-all="handleStopAll" :is-loading="isLoading" :account-groups="accountGroups"
|
||
:rotation-status="rotationStatus" :greeting-stats="greetingStats" :automation-logs="automationLogs"
|
||
:sidebar-width="navSidebarWidth" />
|
||
|
||
<!-- 内容区域 -->
|
||
<main class="flex-1 flex flex-col relative">
|
||
<!-- 顶部视图切换栏 -->
|
||
<div class="h-12 bg-white border-b border-gray-200 flex items-center px-4 gap-2 shadow-sm">
|
||
<span class="text-gray-500 text-sm mr-2">视图:</span>
|
||
<button v-for="viewId in currentTabConfig.viewIds" :key="viewId" @click="handleViewSwitch(viewId)"
|
||
:class="[
|
||
'px-3 py-1.5 rounded-lg text-sm font-medium transition-all',
|
||
(selectedViewId || currentTabConfig.viewIds[0]) === viewId
|
||
? 'bg-blue-500 text-white shadow-md'
|
||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 border border-gray-200'
|
||
]">
|
||
视图 {{ viewId }}
|
||
<span v-if="viewAccountMap[viewId]" class="ml-1.5 text-xs opacity-70">
|
||
({{ viewAccountMap[viewId].email.split('@')[0] }})
|
||
</span>
|
||
</button>
|
||
|
||
<div class="flex-1" />
|
||
|
||
<!-- 状态指示 -->
|
||
<span v-if="automationStatus[selectedViewId || currentTabConfig.viewIds[0]]"
|
||
class="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded border border-gray-200">
|
||
{{ automationStatus[selectedViewId || currentTabConfig.viewIds[0]] }}
|
||
</span>
|
||
</div>
|
||
|
||
<!-- 单个视图显示区域 -->
|
||
<div class="flex-1 relative">
|
||
<ViewPlaceholder class="absolute inset-0" />
|
||
</div>
|
||
|
||
<div v-if="isLoading" class="absolute inset-0 bg-slate-900/80 flex items-center justify-center z-50">
|
||
<div class="flex flex-col items-center gap-3">
|
||
<div class="w-10 h-10 border-3 border-t-primary-400 border-slate-600 rounded-full animate-spin" />
|
||
<span class="text-slate-400 text-sm">切换中...</span>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
<!-- 更新通知 -->
|
||
<div style="display: none;">
|
||
<!-- Hack to keep UpdateNotification if needed, or move it to layout -->
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||
import { isElectron } from '@/utils/electronBridge'
|
||
import Sidebar from '@/components/Sidebar.vue'
|
||
import ViewPlaceholder from '@/components/ViewPlaceholder.vue'
|
||
|
||
// Props passed from parent (App.vue or Layout) can replace some local state if we want to lift state up.
|
||
// For now, I'll keep local state to minimize refactor risk, assuming this component is mounted once and kept alive.
|
||
// BUT, App.vue passed some props like `accountGroups` etc.
|
||
// Wait, in App.vue these were local state. I needs to move the logic here or keep it in App.vue and pass via props.
|
||
// To keep valid functionality, I will copy the logic 1:1 here.
|
||
|
||
const props = defineProps(['accountGroups', 'rotationStatus', 'greetingStats', 'automationLogs', 'navSidebarWidth'])
|
||
const emit = defineEmits(['go-back', 'stop-all', 'request-config-load'])
|
||
|
||
// Constants
|
||
const USER_KEY = 'user_data'
|
||
const CONFIG_KEY = 'autoDm_runConfig'
|
||
|
||
// State
|
||
const currentTab = ref('A')
|
||
const isLoading = ref(false)
|
||
const automationStatus = ref({})
|
||
const selectedViewId = ref(null)
|
||
const viewAccountMap = ref({})
|
||
|
||
// We use props for these now? Or still load config here?
|
||
// The original App.vue loaded config.
|
||
// Let's use the props if provided, otherwise load locally?
|
||
// Actually App.vue code showed `loadConfig` was called on mounted.
|
||
// Since `accountGroups` is passed as prop in my new design (implied by previous thought), I should use it.
|
||
// BUT, to be safe and independent:
|
||
|
||
const internalAccountGroups = ref([])
|
||
// use props if available, else local
|
||
const effectiveAccountGroups = computed(() => props.accountGroups || internalAccountGroups.value)
|
||
|
||
// Computed
|
||
const tabs = computed(() => [
|
||
{ id: 'A', label: effectiveAccountGroups.value[0]?.name || 'Tab A', viewIds: [1, 2, 3] },
|
||
{ id: 'B', label: effectiveAccountGroups.value[1]?.name || 'Tab B', viewIds: [4, 5, 6] },
|
||
{ id: 'C', label: effectiveAccountGroups.value[2]?.name || 'Tab C', viewIds: [7, 8, 9] }
|
||
])
|
||
|
||
const currentTabConfig = computed(() => tabs.value.find(t => t.id === currentTab.value) || tabs.value[0])
|
||
|
||
// Lifecycle
|
||
onMounted(() => {
|
||
// If props are not provided (standalone usage), load config
|
||
if (!props.accountGroups) {
|
||
loadConfig()
|
||
} else {
|
||
// Build viewAccountMap from props
|
||
buildViewMap(props.accountGroups)
|
||
}
|
||
|
||
// 监听本地存储变化,当配置更新时重新加载
|
||
window.addEventListener('storage', handleStorageChange)
|
||
// 监听自定义配置更新事件
|
||
window.addEventListener('config-updated', handleConfigUpdate)
|
||
|
||
// Listeners specific to browser view
|
||
// ...
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
// 清理监听器
|
||
window.removeEventListener('storage', handleStorageChange)
|
||
window.removeEventListener('config-updated', handleConfigUpdate)
|
||
})
|
||
|
||
// 处理本地存储变化
|
||
const handleStorageChange = (event) => {
|
||
if (event.key === CONFIG_KEY) {
|
||
loadConfig()
|
||
// 强制重新构建 viewAccountMap,确保使用最新的账号信息
|
||
if (props.accountGroups) {
|
||
buildViewMap(props.accountGroups)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理配置更新事件
|
||
const handleConfigUpdate = () => {
|
||
console.log('[YoloBrowser] 收到配置更新事件,重新加载配置')
|
||
loadConfig()
|
||
// 强制重新构建 viewAccountMap,确保使用最新的账号信息
|
||
if (props.accountGroups) {
|
||
buildViewMap(props.accountGroups)
|
||
}
|
||
}
|
||
|
||
// Original loadConfig logic
|
||
const loadConfig = () => {
|
||
try {
|
||
const savedConfig = localStorage.getItem(CONFIG_KEY)
|
||
if (savedConfig) {
|
||
const config = JSON.parse(savedConfig)
|
||
internalAccountGroups.value = config.accountGroups || []
|
||
buildViewMap(config.accountGroups)
|
||
}
|
||
} catch { }
|
||
}
|
||
|
||
const buildViewMap = (groups) => {
|
||
const map = {}
|
||
if (!groups) return
|
||
groups.forEach((group, groupIndex) => {
|
||
const viewsPerGroup = 3
|
||
group.accounts.forEach((account, accIndex) => {
|
||
const viewId = groupIndex * viewsPerGroup + accIndex + 1
|
||
if (viewId <= 9 && account.email && account.pwd) {
|
||
map[viewId] = { ...account, group: group.name }
|
||
}
|
||
})
|
||
})
|
||
viewAccountMap.value = map
|
||
}
|
||
|
||
watch(() => props.accountGroups, (newVal) => {
|
||
if (newVal) buildViewMap(newVal)
|
||
}, { deep: true })
|
||
|
||
// Actions
|
||
const handleTabSwitch = async (tab) => {
|
||
if (tab === currentTab.value) return
|
||
|
||
if (isElectron()) {
|
||
try {
|
||
const result = await window.electronAPI.switchTab(tab)
|
||
if (result.success) {
|
||
currentTab.value = tab
|
||
selectedViewId.value = null
|
||
}
|
||
} catch (error) {
|
||
console.error('切换标签失败:', error)
|
||
}
|
||
} else {
|
||
currentTab.value = tab
|
||
selectedViewId.value = null
|
||
}
|
||
}
|
||
|
||
const handleViewSwitch = async (viewId) => {
|
||
selectedViewId.value = viewId
|
||
// 重新加载配置,确保获取最新的账号信息
|
||
loadConfig()
|
||
// 强制重新构建 viewAccountMap,确保使用最新的账号信息
|
||
if (props.accountGroups) {
|
||
buildViewMap(props.accountGroups)
|
||
}
|
||
if (isElectron()) {
|
||
await window.electronAPI.switchToView(viewId)
|
||
}
|
||
}
|
||
|
||
const handleGoToConfig = async () => {
|
||
emit('go-back')
|
||
}
|
||
|
||
const handleStopAll = async () => {
|
||
emit('stop-all')
|
||
}
|
||
</script>
|