SystemOverviewPanel.vue 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. <script setup lang="ts">
  2. import { computed, ref, watch } from 'vue'
  3. import AppIcon from '@/components/common/AppIcon.vue'
  4. import { listUsers, type ManagedUser } from '@/api/admin'
  5. import { getDuplexVoiceSettings, type DuplexVoiceSettings } from '@/api/agentManagement'
  6. import { pairedDevices, type PairedDevicesResponse } from '@/api/pairing'
  7. import { getPushSystemStatus, type PushSystemStatus } from '@/api/push'
  8. import { systemUpdateStatus, type SystemUpdateStatus } from '@/api/systemUpdate'
  9. type Destination = 'system-users' | 'system-connection' | 'system-push' | 'system-nodes' | 'system-voice' | 'system-update'
  10. const props = withDefaults(defineProps<{
  11. active?: boolean
  12. upstreamReady?: boolean
  13. upstreamError?: string
  14. }>(), {
  15. active: true,
  16. upstreamReady: false,
  17. upstreamError: '',
  18. })
  19. const emit = defineEmits<{ navigate: [destination: Destination] }>()
  20. const users = ref<ManagedUser[]>()
  21. const push = ref<PushSystemStatus>()
  22. const devices = ref<PairedDevicesResponse>()
  23. const voice = ref<DuplexVoiceSettings>()
  24. const update = ref<SystemUpdateStatus>()
  25. const checking = ref(false)
  26. const error = ref('')
  27. const userStatus = computed(() => users.value
  28. ? `${users.value.filter(user => user.role === 'admin').length} 位管理员 · ${users.value.length} 位用户`
  29. : '尚未读取用户状态')
  30. const pushStatus = computed(() => {
  31. if (!push.value) return '尚未读取推送状态'
  32. const apns = push.value.configured && push.value.healthy ? 'APNs 已启用' : 'APNs 未就绪'
  33. const fcm = push.value.providers?.fcm
  34. return `${apns} · ${fcm?.configured && fcm.healthy ? 'FCM 已启用' : 'FCM 未配置'}`
  35. })
  36. const deviceStatus = computed(() => devices.value ? `${devices.value.devices.length} 台已授权` : '尚未读取设备状态')
  37. const voiceStatus = computed(() => voice.value ? `已配置 ${voice.value.voices.length} 个音色` : '尚未读取语音状态')
  38. const updateStatus = computed(() => {
  39. if (!update.value) return '尚未读取版本状态'
  40. return `Web ${update.value.current.webVersion} · 可独立升级与回滚`
  41. })
  42. async function refresh() {
  43. checking.value = true
  44. error.value = ''
  45. const results = await Promise.allSettled([
  46. listUsers(),
  47. getPushSystemStatus(),
  48. pairedDevices(),
  49. getDuplexVoiceSettings(),
  50. // Publish local update status immediately, without waiting for 9119 panels.
  51. systemUpdateStatus().then(value => { update.value = value; return value }),
  52. ])
  53. if (results[0]?.status === 'fulfilled') users.value = results[0].value
  54. if (results[1]?.status === 'fulfilled') push.value = results[1].value
  55. if (results[2]?.status === 'fulfilled') devices.value = results[2].value
  56. if (results[3]?.status === 'fulfilled') voice.value = results[3].value
  57. if (results[4]?.status === 'fulfilled') update.value = results[4].value
  58. const failed = results.filter(result => result.status === 'rejected').length
  59. if (failed) error.value = failed === results.length ? '无法读取系统状态' : `${failed} 项状态暂时无法读取,可进入对应页面重试。`
  60. checking.value = false
  61. }
  62. watch(() => props.active, active => { if (active) void refresh() }, { immediate: true })
  63. </script>
  64. <template>
  65. <section class="system-overview-panel" aria-label="系统概览">
  66. <div class="overview-toolbar">
  67. <p>影响整个夭夭安装及所有 Agent,仅管理员可见。</p>
  68. <button type="button" :disabled="checking" @click="refresh"><AppIcon name="refresh" :size="16" />{{ checking ? '检查中…' : '检查系统状态' }}</button>
  69. </div>
  70. <p v-if="error" class="overview-warning" role="status">{{ error }}</p>
  71. <div class="overview-list">
  72. <button type="button" @click="emit('navigate', 'system-users')">
  73. <AppIcon name="users" :size="22" />
  74. <span><strong>用户与权限</strong><small>{{ userStatus }}</small></span>
  75. <AppIcon class="chevron" name="chevron-left" :size="18" />
  76. </button>
  77. <button type="button" @click="emit('navigate', 'system-connection')">
  78. <AppIcon name="link" :size="22" />
  79. <span><strong>Hermes 连接</strong><small :class="{ success: upstreamReady }">{{ upstreamReady ? '9119 连接正常' : upstreamError || '尚未验证上游连接' }}</small></span>
  80. <AppIcon class="chevron" name="chevron-left" :size="18" />
  81. </button>
  82. <button type="button" @click="emit('navigate', 'system-push')">
  83. <AppIcon name="bell" :size="22" />
  84. <span><strong>消息推送</strong><small>{{ pushStatus }}</small></span>
  85. <AppIcon class="chevron" name="chevron-left" :size="18" />
  86. </button>
  87. <button type="button" @click="emit('navigate', 'system-nodes')">
  88. <AppIcon name="panel" :size="22" />
  89. <span><strong>节点与设备</strong><small>{{ deviceStatus }}</small></span>
  90. <AppIcon class="chevron" name="chevron-left" :size="18" />
  91. </button>
  92. <button type="button" @click="emit('navigate', 'system-voice')">
  93. <AppIcon name="audio" :size="22" />
  94. <span><strong>双流语音 <em>全局</em></strong><small>{{ voiceStatus }}</small></span>
  95. <AppIcon class="chevron" name="chevron-left" :size="18" />
  96. </button>
  97. <button type="button" @click="emit('navigate', 'system-update')">
  98. <AppIcon name="refresh" :size="22" />
  99. <span><strong>更新与回滚</strong><small>{{ updateStatus }} <b v-if="update?.latest && !update.updateAvailable">已是最新版本</b></small></span>
  100. <AppIcon class="chevron" name="chevron-left" :size="18" />
  101. </button>
  102. </div>
  103. <p class="ownership-note">8800 通过官方 API 管理 Hermes 9119;外部文件与进程不在此处控制。</p>
  104. </section>
  105. </template>
  106. <style scoped>
  107. .system-overview-panel { display: grid; gap: 16px; }
  108. .overview-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 18px; }
  109. .overview-toolbar p { margin: 0; color: var(--text-secondary); font-size: 14px; }
  110. .overview-toolbar button { display: inline-flex; min-height: 40px; flex: 0 0 auto; align-items: center; gap: 7px; padding: 0 14px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface-raised); color: var(--text-primary); cursor: pointer; font: 600 13px var(--font-ui); }
  111. .overview-toolbar button:disabled { cursor: wait; opacity: .55; }
  112. .overview-warning { margin: 0; padding: 10px 12px; border-radius: 9px; background: color-mix(in srgb, var(--warning, #bd7611) 10%, transparent); color: var(--text-secondary); font-size: 12px; }
  113. .overview-list { border-top: 1px solid var(--line); }
  114. .overview-list > button { display: grid; width: 100%; min-height: 76px; grid-template-columns: 28px minmax(0, 1fr) 22px; align-items: center; gap: 15px; padding: 12px 8px; border: 0; border-bottom: 1px solid var(--line); border-radius: 9px; background: transparent; color: var(--text-primary); cursor: pointer; text-align: left; }
  115. .overview-list > button:hover { background: var(--surface-soft); }
  116. .overview-list > button:focus-visible { outline: 0; box-shadow: inset 0 0 0 2px color-mix(in srgb, #7c4dff 72%, var(--line-strong)); }
  117. .overview-list > button > span { display: grid; min-width: 0; gap: 6px; }
  118. .overview-list strong { display: flex; align-items: center; gap: 8px; font-size: 15px; }
  119. .overview-list small { color: var(--text-muted); font-size: 13px; }
  120. .overview-list small.success,.overview-list small b { color: var(--success, #21845b); font-weight: 600; }
  121. .overview-list em { padding: 2px 7px; border: 1px solid var(--line-strong); border-radius: 999px; color: var(--text-muted); font: normal 11px var(--font-ui); }
  122. .chevron { transform: rotate(180deg); color: var(--text-muted); }
  123. .ownership-note { margin: 2px 0 0; color: var(--text-muted); font-size: 12px; line-height: 1.6; }
  124. @media (max-width: 700px) {
  125. .overview-toolbar { align-items: stretch; flex-direction: column; }
  126. .overview-toolbar button { justify-content: center; }
  127. .overview-list > button { min-height: 72px; padding-inline: 4px; }
  128. }
  129. </style>