码桶
发现社区成员的开源项目
NetworkPanel.vue6.7 KB
<script setup lang="ts">
import { computed, ref, onMounted, onUnmounted } from 'vue'
import NodeSelector from './NodeSelector.vue'
import ThreadSlider from './ThreadSlider.vue'
import ControlButton from './ControlButton.vue'
import Toolbar from './Toolbar.vue'
import SettingsPanel from './SettingsPanel.vue'
import CustomNodes from './CustomNodes.vue'
import UserPanel from './UserPanel.vue'
import { usePanelStore } from '@/stores/panel'
import { useSettingsStore } from '@/stores/settings'
import { useUserStore } from '@/stores/user'
import { useFormatter } from '@/composables/useFormatter'
type PanelView = 'main' | 'settings' | 'nodes' | 'user'
const currentView = ref<PanelView>('main')
const panel = usePanelStore()
const settings = useSettingsStore()
const userStore = useUserStore()
const { formatBytes, formatBandwidth } = useFormatter()
const runningStatus = computed(() => {
if (panel.isChecking) return '检测中'
return panel.isRunning ? '运行中' : '待命'
})
const flowLimit = computed(() => settings.maxUse > 0 ? formatBytes(settings.maxUse) : '不限')
const speedLimit = computed(() => settings.maxSpeed > 0 ? formatBandwidth(settings.maxSpeed) : '不限')
const scheduleStatus = computed(() => settings.cronTab.status ? '已开启' : '未开启')
const rotationStatus = computed(() => settings.nodeRotation.enabled ? '已开启' : '未开启')
const groupModeStatus = computed(() => settings.groupMode ? '已开启' : '未开启')
const overviewItems = computed(() => [
{ label: '定时任务', value: scheduleStatus.value },
{ label: '节点联测', value: groupModeStatus.value },
{ label: '节点轮换', value: rotationStatus.value },
])
const overviewIndex = ref(0)
let overviewTimer: ReturnType<typeof setInterval> | null = null
onMounted(() => {
overviewTimer = setInterval(() => {
overviewIndex.value = (overviewIndex.value + 1) % overviewItems.value.length
}, 5000)
})
onUnmounted(() => {
if (overviewTimer) clearInterval(overviewTimer)
})
const emit = defineEmits<{
(e: 'toggleRun'): void
(e: 'openRankings'): void
(e: 'openScreen'): void
(e: 'setFlowMax'): void
(e: 'setLimitMax'): void
}>()
function switchView(view: PanelView) {
currentView.value = view
}
async function handleOpenUser() {
await userStore.checkStatus()
switchView('user')
}
</script>
<template>
<div class="fl-card h-full">
<!-- Header -->
<div class="flex items-center justify-between" style="margin-bottom: 12px;">
<div class="flex items-center gap-2">
<div class="w-5 h-5 flex items-center justify-center" style="color: var(--text-secondary)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="3" width="20" height="14" rx="2" />
<path d="M8 21h8M12 17v4" />
</svg>
</div>
<h2 class="text-[14px] font-semibold tracking-tight" style="color: var(--text-primary)">
控制台
</h2>
</div>
<span class="text-[11px]" style="color: var(--text-muted)">Control</span>
</div>
<Transition name="fade-slide" mode="out-in">
<div v-if="currentView === 'main'" key="main" class="space-y-5">
<NodeSelector @edit-nodes="switchView('nodes')" />
<ThreadSlider />
<ControlButton class="!mt-3" @toggle="emit('toggleRun')" />
<div style="border-top: 1px solid var(--border-subtle)" />
<Toolbar
@open-rankings="emit('openRankings')"
@open-screen="emit('openScreen')"
@open-user="handleOpenUser"
@open-settings="switchView('settings')"
/>
<div class="panel-overview">
<div class="overview-head">
<span>运行概览</span>
</div>
<div class="overview-grid">
<div class="overview-item">
<span>线程</span>
<strong>{{ settings.threadNum }}</strong>
</div>
<div class="overview-item">
<span>流量上限</span>
<strong>{{ flowLimit }}</strong>
</div>
<div class="overview-item">
<span>带宽上限</span>
<strong>{{ speedLimit }}</strong>
</div>
<div class="overview-item overview-item--rotating">
<Transition name="overview-fade" mode="out-in">
<div :key="overviewIndex" class="overview-rotate-inner">
<span>{{ overviewItems[overviewIndex].label }}</span>
<strong>{{ overviewItems[overviewIndex].value }}</strong>
</div>
</Transition>
</div>
</div>
</div>
</div>
<SettingsPanel v-else-if="currentView === 'settings'" key="settings" @back="switchView('main')" />
<CustomNodes v-else-if="currentView === 'nodes'" key="nodes" @back="switchView('main')" />
<UserPanel v-else-if="currentView === 'user'" key="user" @back="switchView('main')" />
</Transition>
</div>
</template>
<style scoped>
.panel-overview {
margin-top: 2px;
padding: 10px 0;
border: none;
background: none;
}
.overview-head {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 10px;
color: var(--text-muted);
font-size: 11px;
font-weight: 600;
}
.overview-head::before,
.overview-head::after {
content: '';
flex: 1;
height: 1px;
background: var(--border);
}
.overview-head span:first-child {
color: var(--text-secondary);
white-space: nowrap;
}
.overview-grid {
display: flex;
gap: 0;
}
.overview-item {
flex: 1;
min-width: 0;
padding: 0;
border: none;
border-radius: 0;
background: none;
text-align: center;
}
.overview-item + .overview-item {
border-left: 1px solid var(--border-subtle);
}
.overview-item span {
display: block;
color: var(--text-muted);
font-size: 10px;
line-height: 1.3;
}
.overview-item strong {
display: block;
margin-top: 2px;
color: var(--text-primary);
font-size: 12px;
font-weight: 650;
letter-spacing: 0;
line-height: 1.25;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.overview-item--rotating {
overflow: hidden;
}
.overview-rotate-inner {
text-align: inherit;
}
.overview-rotate-inner span {
display: block;
color: var(--text-muted);
font-size: 10px;
line-height: 1.3;
}
.overview-rotate-inner strong {
display: block;
margin-top: 2px;
color: var(--text-primary);
font-size: 12px;
font-weight: 650;
line-height: 1.25;
}
.overview-fade-enter-active,
.overview-fade-leave-active {
transition: opacity 0.25s ease, transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.overview-fade-enter-from {
opacity: 0;
transform: scale(0.85);
}
.overview-fade-leave-to {
opacity: 0;
transform: scale(0.85);
}
</style>