File size: 7,392 Bytes
395e9dc
 
 
 
 
 
 
1f4cfcb
395e9dc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1f4cfcb
395e9dc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1f4cfcb
395e9dc
 
 
1f4cfcb
395e9dc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1f4cfcb
395e9dc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1f4cfcb
 
395e9dc
 
 
 
 
1f4cfcb
 
395e9dc
 
 
 
 
1f4cfcb
 
395e9dc
 
 
 
 
1f4cfcb
 
395e9dc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
090976a
 
 
1f4cfcb
 
 
 
090976a
 
395e9dc
 
1f4cfcb
 
 
 
395e9dc
 
 
 
1f4cfcb
 
 
 
395e9dc
 
 
 
1f4cfcb
 
 
 
395e9dc
 
 
1f4cfcb
 
 
 
395e9dc
 
 
 
 
 
090976a
 
395e9dc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
090976a
 
 
 
 
 
 
395e9dc
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
<!--
  PoolHealthCard — F6 Dashboard 顶部账号池健康度卡片
  - 4 数字 (可用 / Grace / 待机 / 不可用)
  - 占比环形图 (Donut)
  - master_health inline 状态
-->
<template>
  <div class="glass rounded-lg p-5 lg:p-6 lift-hover relative overflow-hidden">
    <div class="relative flex flex-col lg:flex-row items-start lg:items-center gap-6">
      <!-- 左:donut + 中心总数 -->
      <div class="shrink-0">
        <HealthDonut
          :size="140"
          :thickness="14"
          :segments="segments"
          :center-value="counts.total"
          center-label="账号池"
          :center-hint="trendHint" />
      </div>

      <!-- 中:四档数字 -->
      <div class="flex-1 grid grid-cols-2 sm:grid-cols-4 gap-3 w-full">
        <div v-for="card in cards" :key="card.key"
          class="rounded-lg p-3 border bg-surface hover:bg-ink-50 transition-all"
          :class="card.borderClass">
          <div class="flex items-center gap-1.5 text-[10px] uppercase tracking-widest font-semibold"
            :class="card.labelClass">
            <span class="w-1.5 h-1.5 rounded-full" :style="{ background: card.color }"></span>
            {{ card.label }}
          </div>
          <div class="text-3xl font-bold tabular mt-1" :class="card.valueClass">
            {{ card.value }}
          </div>
          <div class="text-[10px] mt-0.5 opacity-60" :class="card.valueClass">
            {{ percent(card.value) }}
          </div>
        </div>
      </div>

      <!-- 右:master health inline -->
      <div class="shrink-0 lg:max-w-xs w-full lg:w-auto">
        <div class="rounded-lg border p-3 flex items-start gap-3"
          :class="masterTone.border" :style="{ background: masterTone.bg }">
          <div class="shrink-0 w-9 h-9 rounded-lg flex items-center justify-center"
            :class="masterTone.iconWrap">
            <ShieldCheck class="w-4 h-4" :stroke-width="2" />
          </div>
          <div class="min-w-0 flex-1">
            <div class="text-[10px] uppercase tracking-widest font-semibold opacity-70" :class="masterTone.text">
              母号订阅
            </div>
            <div class="text-sm font-bold leading-tight mt-0.5" :class="masterTone.text">
              {{ masterTitle }}
            </div>
            <div class="text-[10px] mt-0.5 opacity-70 truncate" :class="masterTone.text">
              {{ masterSubtitle }}
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { computed } from 'vue'
import HealthDonut from './HealthDonut.vue'
import { computeUsability, formatGraceRemain } from '../composables/useStatus.js'
import { ShieldCheck } from 'lucide-vue-next'

const props = defineProps({
  accounts: { type: Array, default: () => [] },
  masterHealth: { type: Object, default: null },
  minGraceUntil: { type: [Number, null], default: null },
})

const counts = computed(() => {
  const c = { usable: 0, grace: 0, standby: 0, unusable: 0, total: 0 }
  for (const acc of props.accounts || []) {
    if (acc.is_main_account) continue
    const u = computeUsability(acc).kind
    if (u === 'usable') c.usable++
    else if (u === 'grace') c.grace++
    else if (u === 'standby') c.standby++
    else if (u === 'unusable') c.unusable++
    c.total++
  }
  return c
})

const cards = computed(() => [
  {
    key: 'usable', label: '可用', value: counts.value.usable,
    color: 'rgba(52, 211, 153, 1)',
    borderClass: 'border-emerald-500/20',
    labelClass: 'text-emerald-700',
    valueClass: 'text-emerald-700',
  },
  {
    key: 'grace', label: 'Grace', value: counts.value.grace,
    color: 'rgba(251, 146, 60, 1)',
    borderClass: 'border-orange-500/30',
    labelClass: 'text-orange-700',
    valueClass: 'text-orange-700',
  },
  {
    key: 'standby', label: '待机', value: counts.value.standby,
    color: 'rgba(251, 191, 36, 1)',
    borderClass: 'border-amber-500/20',
    labelClass: 'text-amber-700',
    valueClass: 'text-amber-700',
  },
  {
    key: 'unusable', label: '不可用', value: counts.value.unusable,
    color: 'rgba(244, 63, 94, 1)',
    borderClass: 'border-rose-500/20',
    labelClass: 'text-rose-700',
    valueClass: 'text-rose-700',
  },
])

const segments = computed(() =>
  cards.value.filter((c) => c.value > 0).map((c) => ({ key: c.key, value: c.value, color: c.color }))
)

const trendHint = computed(() => {
  const total = counts.value.total
  if (!total) return '无账号'
  const ratio = ((counts.value.usable / total) * 100).toFixed(0)
  return `${ratio}% 可用`
})

function percent(v) {
  if (!counts.value.total) return '0%'
  return `${((v / counts.value.total) * 100).toFixed(0)}%`
}

// 母号订阅 inline 状态
const masterTone = computed(() => {
  const m = props.masterHealth
  const r = m?.reason
  // Round 11:subscription_grace = healthy=True 但 grace 期内,渲染橙色提示 (而非 healthy 绿色)
  if (r === 'subscription_grace') {
    return {
      bg: '#fff7ed',
      border: 'border-orange-200',
      iconWrap: 'bg-orange-100 text-orange-700',
      text: 'text-orange-800',
    }
  }
  if (!m || m.healthy === true || r === 'active') {
    return {
      bg: '#ecfdf5',
      border: 'border-emerald-200',
      iconWrap: 'bg-emerald-100 text-emerald-700',
      text: 'text-emerald-800',
    }
  }
  if (r === 'subscription_cancelled') {
    return {
      bg: '#fff7ed',
      border: 'border-orange-200',
      iconWrap: 'bg-orange-100 text-orange-700',
      text: 'text-orange-800',
    }
  }
  if (r === 'network_error') {
    return {
      bg: '#f8fafc',
      border: 'border-slate-200',
      iconWrap: 'bg-slate-100 text-slate-700',
      text: 'text-slate-700',
    }
  }
  return {
    bg: '#fff1f2',
    border: 'border-rose-200',
    iconWrap: 'bg-rose-100 text-rose-700',
    text: 'text-rose-800',
  }
})

const masterTitle = computed(() => {
  const m = props.masterHealth
  if (!m) return '未探测'
  // Round 11:subscription_grace healthy=True,显示 "Grace 期内"
  if (m.reason === 'subscription_grace') return 'Grace 期内'
  if (m.healthy === true || m.reason === 'active') return 'Healthy'
  if (m.reason === 'subscription_cancelled') {
    if (props.minGraceUntil && props.minGraceUntil * 1000 > Date.now()) return 'Grace 期内'
    return 'Cancelled'
  }
  if (m.reason === 'workspace_missing') return 'Workspace 漂移'
  if (m.reason === 'role_not_owner') return '权限异常'
  if (m.reason === 'auth_invalid') return 'Session 失效'
  if (m.reason === 'network_error') return '网络抖动'
  return m.reason || '异常'
})

const masterSubtitle = computed(() => {
  const m = props.masterHealth
  if (!m) return '尚未拉取 / 设置 → 探测'
  // Round 11:subscription_grace 优先用 evidence.grace_until 显示倒计时
  if (m.reason === 'subscription_grace') {
    const evGrace = m.evidence?.grace_until
    const target = (typeof evGrace === 'number' && evGrace > 0) ? evGrace : props.minGraceUntil
    const r = formatGraceRemain(target)
    return r ? `grace 剩 ${r}` : '订阅 grace 期'
  }
  if (m.healthy === true || m.reason === 'active') return '订阅 active'
  if (m.reason === 'subscription_cancelled' && props.minGraceUntil) {
    const r = formatGraceRemain(props.minGraceUntil)
    return r ? `grace 剩 ${r}` : 'grace 已过期'
  }
  return m.evidence?.detail || ''
})
</script>