UpdateCheckDialog.vue 4.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061
  1. <script setup lang="ts">
  2. import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
  3. import { checkSystemUpdate, systemUpdateStatus, type SystemUpdateStatus } from '@/api/systemUpdate'
  4. import AppIcon from '@/components/common/AppIcon.vue'
  5. import StandaloneDialog from '@/components/common/StandaloneDialog.vue'
  6. const emit = defineEmits<{ close: []; manage: [] }>()
  7. const status = ref<SystemUpdateStatus>()
  8. const checking = ref(true)
  9. const checked = ref(false)
  10. const error = ref('')
  11. let disposed = false
  12. const message = computed(() => checking.value ? '正在检测更新…'
  13. : error.value ? '检测失败,请重试'
  14. : status.value?.updateAvailable ? `发现新版本 ${status.value.latest?.webVersion}` : '当前已是最新版本')
  15. const releasePage = computed(() => {
  16. try {
  17. const url = new URL(status.value?.releasePageUrl || '')
  18. return url.protocol === 'https:' ? url.href : undefined
  19. } catch { return undefined }
  20. })
  21. async function check() {
  22. checking.value = true
  23. checked.value = false
  24. error.value = ''
  25. try {
  26. const next = await checkSystemUpdate()
  27. if (disposed) return
  28. status.value = next
  29. checked.value = true
  30. } catch (cause) {
  31. if (!disposed) error.value = cause instanceof Error ? cause.message : '无法连接更新服务'
  32. } finally { if (!disposed) checking.value = false }
  33. }
  34. onMounted(async () => {
  35. // Keep the installed version visible even when the remote check fails.
  36. try { const current = await systemUpdateStatus(); if (!disposed) status.value = current } catch { /* The check below reports actionable errors. */ }
  37. if (!disposed) await check()
  38. })
  39. onBeforeUnmount(() => { disposed = true })
  40. </script>
  41. <template>
  42. <StandaloneDialog title="检测更新" compact @close="emit('close')">
  43. <section class="update-check" :aria-busy="checking">
  44. <div class="update-check__icon" :class="{ spinning: checking }"><AppIcon :name="checking ? 'refresh' : error ? 'alert' : status?.updateAvailable ? 'download' : 'check'" :size="28" /></div>
  45. <p class="update-check__status" role="status" aria-live="polite">{{ message }}</p>
  46. <dl aria-label="Web 版本信息"><div><dt>当前 Web 版本</dt><dd>{{ status?.current.webVersion || '—' }}</dd></div><div><dt>最新版本</dt><dd>{{ checking ? '检测中…' : !checked ? '未获取' : status?.latest?.webVersion || status?.current.webVersion }}</dd></div></dl>
  47. <p v-if="error" class="update-check__error" role="alert">{{ error }}</p>
  48. <p v-if="checked && status?.updateAvailable && !status.supported" class="update-check__note">{{ status.unsupportedReason || '请通过部署环境更新 Web。' }}</p>
  49. <a v-if="checked && releasePage" :href="releasePage" target="_blank" rel="noopener noreferrer">查看发布说明<AppIcon name="external" :size="14" /></a>
  50. <div class="update-check__actions">
  51. <button type="button" :disabled="checking" @click="check">{{ checking ? '正在检测…' : error ? '重试' : '重新检测' }}</button>
  52. <button v-if="checked && status?.updateAvailable && status.supported" class="primary" type="button" @click="emit('manage')">前往更新</button>
  53. </div>
  54. </section>
  55. </StandaloneDialog>
  56. </template>
  57. <style scoped>
  58. .update-check{display:grid;gap:18px;text-align:center}.update-check__icon{display:grid;place-items:center;color:var(--accent)}.update-check__status{margin:0;font-size:16px;font-weight:600;line-height:1.5}dl{display:grid;gap:12px;margin:0;padding:16px 0;border-block:1px solid var(--line);font-size:13px}dl div{display:flex;justify-content:space-between;gap:12px}dt{color:var(--text-secondary)}dd{margin:0;font-variant-numeric:tabular-nums}.update-check__error,.update-check__note{margin:0;font-size:13px;line-height:1.6;overflow-wrap:anywhere}.update-check__error{color:var(--danger)}.update-check__note{color:var(--text-secondary)}a{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:44px;color:var(--accent);font-size:13px;text-decoration:none}a:hover{text-decoration:underline}.update-check__actions{display:flex;gap:10px;flex-wrap:wrap}button{flex:1;min-height:44px;padding:8px 14px;border:1px solid var(--line);border-radius:9px;background:var(--surface-raised);color:var(--text-primary);font:500 13px var(--font-ui);cursor:pointer}button:hover{background:var(--surface-hover)}button.primary{background:var(--accent);border-color:var(--accent);color:var(--text-on-solid)}button:disabled{opacity:.6;cursor:wait}button:focus-visible,a:focus-visible{outline:2px solid var(--accent);outline-offset:3px}.spinning{animation:update-spin 1s linear infinite}@keyframes update-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.spinning{animation:none}}
  59. </style>