码桶

发现社区成员的开源项目

StatsRow.vue8.1 KB
<script setup lang="ts">
import { ref } from 'vue'
import { usePanelStore } from '@/stores/panel'

const panel = usePanelStore()
const predictOpen = ref(false)

const emit = defineEmits<{
  (e: 'setFlowMax'): void
  (e: 'setLimitMax'): void
}>()

function handleResetTraffic() {
  panel.resetTraffic()
}
</script>

<template>
  <div class="grid grid-cols-2 gap-3">
    <!-- Total Traffic -->
    <div class="fl-stat" @click="emit('setFlowMax')">
      <div class="flex items-center justify-between mb-3">
        <span class="text-[11px] font-medium uppercase tracking-wider" style="color: var(--text-muted); letter-spacing: 0.05em">
          总流量
        </span>
        <svg class="w-3.5 h-3.5" style="color: var(--text-muted)" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
          <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
        </svg>
      </div>
      <div class="font-mono text-[22px] font-semibold tabular-nums truncate" style="color: var(--text-primary); letter-spacing: -0.03em">
        {{ panel.show.allUsed || '--' }}
      </div>
      <div class="stat-footer mt-1">
        <span>累计下载</span>
        <button
          class="reset-traffic-btn"
          type="button"
          aria-label="重置总流量"
          title="重置总流量"
          :disabled="panel.bytesUsed <= 0"
          @click.stop="handleResetTraffic"
        >
          重置
        </button>
      </div>
    </div>

    <!-- Speed -->
    <div
      class="fl-stat speed-stat"
      tabindex="0"
      @mouseenter="predictOpen = true"
      @mouseleave="predictOpen = false"
      @focus="predictOpen = true"
      @blur="predictOpen = false"
      @click="predictOpen = !predictOpen"
    >
      <div class="flex items-center justify-between mb-3">
        <span class="text-[11px] font-medium uppercase tracking-wider" style="color: var(--text-muted); letter-spacing: 0.05em">
          {{ panel.isRunning ? '实时速度' : '平均速度' }}
        </span>
        <svg class="w-3.5 h-3.5" style="color: var(--text-muted)" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
          <path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
        </svg>
      </div>
      <div class="font-mono text-[22px] font-semibold tabular-nums truncate" :style="{ color: panel.isRunning ? 'var(--color-accent)' : 'var(--text-primary)', letterSpacing: '-0.03em' }">
        {{ panel.show.speed || '--' }}
      </div>
      <div class="mt-1 flex items-center gap-1 text-[11px]" style="color: var(--text-muted)">
        <span v-if="panel.isRunning" class="fl-pulse-dot" style="width: 6px; height: 6px" />
        {{ panel.isRunning ? '正在监测' : '已停止' }}
      </div>

      <Transition name="predict-pop">
        <div v-if="predictOpen" class="predict-panel" @click.stop>
          <div class="predict-head">
            <span>用量预测</span>
            <small>按当前速度估算</small>
          </div>
          <div class="predict-grid">
            <span>每分钟</span>
            <strong>{{ panel.predict.min }}</strong>
            <span>每小时</span>
            <strong>{{ panel.predict.hour }}</strong>
            <span>每天</span>
            <strong>{{ panel.predict.day }}</strong>
            <span>每月</span>
            <strong>{{ panel.predict.mon }}</strong>
          </div>
        </div>
      </Transition>
    </div>

    <!-- Bandwidth -->
    <div class="fl-stat" @click="emit('setLimitMax')">
      <div class="flex items-center justify-between mb-3">
        <span class="text-[11px] font-medium uppercase tracking-wider" style="color: var(--text-muted); letter-spacing: 0.05em">
          带宽
        </span>
        <svg class="w-3.5 h-3.5" style="color: var(--text-muted)" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
          <path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" />
        </svg>
      </div>
      <div class="font-mono text-[22px] font-semibold tabular-nums truncate" style="color: var(--text-primary); letter-spacing: -0.03em">
        {{ panel.show.speedBit || '--' }}
      </div>
      <div class="mt-1 text-[11px]" style="color: var(--text-muted)">
        峰值吞吐
      </div>
    </div>

    <!-- Runtime -->
    <div class="fl-stat">
      <div class="flex items-center justify-between mb-3">
        <span class="text-[11px] font-medium uppercase tracking-wider" style="color: var(--text-muted); letter-spacing: 0.05em">
          运行时长
        </span>
        <svg class="w-3.5 h-3.5" style="color: var(--text-muted)" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
          <circle cx="12" cy="12" r="10" />
          <polyline points="12 6 12 12 16 14" />
        </svg>
      </div>
      <div class="font-mono text-[22px] font-semibold tabular-nums truncate" style="color: var(--text-primary); letter-spacing: -0.03em">
        {{ panel.runTimeShow || '00:00' }}
      </div>
      <div class="mt-1 text-[11px]" style="color: var(--text-muted)">
        本次会话
      </div>
    </div>
  </div>
</template>

<style scoped>
.stat-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  color: var(--text-muted);
  font-size: 11px;
}


.reset-traffic-btn {
  min-width: 38px;
  height: 22px;
  padding: 0 6px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--bg-card);
  color: var(--text-secondary);
  font-size: 11px;
  font-weight: 650;
  line-height: 1;
  cursor: pointer;
  transition: background-color 0.16s ease, border-color 0.16s ease, color 0.16s ease, opacity 0.16s ease;
}

.reset-traffic-btn:hover:not(:disabled),
.reset-traffic-btn:focus-visible {
  border-color: var(--text-muted);
  background: var(--bg-subtle);
  color: var(--text-primary);
}

.reset-traffic-btn:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.16);
}

.reset-traffic-btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.speed-stat {
  position: relative;
  cursor: pointer;
  outline: none;
}

.speed-stat:focus-visible {
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.16);
}

.predict-panel {
  position: absolute;
  left: 50%;
  top: calc(100% + 10px);
  z-index: 30;
  width: min(240px, calc(100vw - 36px));
  padding: 12px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg-card);
  box-shadow: var(--shadow-lg);
  transform: translateX(-50%);
}

.predict-panel::before {
  content: '';
  position: absolute;
  top: -6px;
  left: 50%;
  width: 10px;
  height: 10px;
  border-left: 1px solid var(--border);
  border-top: 1px solid var(--border);
  background: var(--bg-card);
  transform: translateX(-50%) rotate(45deg);
}

.predict-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 9px;
}

.predict-head span {
  color: var(--text-primary);
  font-size: 13px;
  font-weight: 750;
}

.predict-head small {
  color: var(--text-muted);
  font-size: 10px;
}

.predict-grid {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 7px 14px;
  align-items: center;
}

.predict-grid span {
  color: var(--text-muted);
  font-size: 11px;
}

.predict-grid strong {
  min-width: 0;
  color: var(--text-primary);
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 750;
  text-align: right;
  overflow-wrap: anywhere;
}

.predict-pop-enter-active,
.predict-pop-leave-active {
  transition: opacity 0.16s ease, transform 0.16s ease;
}

.predict-pop-enter-from,
.predict-pop-leave-to {
  opacity: 0;
  transform: translateX(-50%) translateY(-4px);
}

@media (max-width: 520px) {
  .predict-panel {
    left: auto;
    right: 0;
    transform: none;
  }

  .predict-panel::before {
    left: auto;
    right: 28px;
    transform: rotate(45deg);
  }

  .predict-pop-enter-from,
  .predict-pop-leave-to {
    transform: translateY(-4px);
  }
}
</style>