181 lines
6.7 KiB
Vue
181 lines
6.7 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" />
|
||
|
|
|
||
|
|
<!-- 内容区域 -->
|
||
|
|
<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'])
|
||
|
|
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)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Listeners specific to browser view
|
||
|
|
// ...
|
||
|
|
})
|
||
|
|
|
||
|
|
// 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)
|
||
|
|
})
|
||
|
|
|
||
|
|
// 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
|
||
|
|
if (isElectron()) {
|
||
|
|
await window.electronAPI.switchToView(viewId)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleGoToConfig = async () => {
|
||
|
|
emit('go-back')
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleStopAll = async () => {
|
||
|
|
emit('stop-all')
|
||
|
|
}
|
||
|
|
</script>
|