DesktopUpdateEntry.vue 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
  1. <script setup lang="ts">
  2. import { computed } from 'vue'
  3. import type { DesktopUpdateState } from '@shared/desktopUpdate'
  4. import AppIcon from '@/components/common/AppIcon.vue'
  5. const props = defineProps<{ state?: DesktopUpdateState; pending?: boolean; error?: string; menu?: boolean; compact?: boolean }>()
  6. const emit = defineEmits<{ activate: [] }>()
  7. const busy = computed(() => Boolean(props.pending || ['checking', 'downloading', 'verifying', 'preparing', 'installing'].includes(props.state?.phase ?? '')))
  8. const progress = computed(() => props.state?.phase === 'downloading' && props.state.total > 0
  9. ? Math.max(0, Math.min(100, Math.floor(props.state.received / props.state.total * 100))) : undefined)
  10. const label = computed(() => {
  11. const state = props.state
  12. if (state?.phase === 'ready') return state.installMode === 'restart' ? '重新启动' : '打开安装包'
  13. if (state?.phase === 'installing') return '正在重启…'
  14. if (state?.phase === 'downloading') return progress.value === undefined ? '正在下载…' : `正在下载 · ${progress.value}%`
  15. if (['verifying', 'preparing'].includes(state?.phase ?? '')) return '正在准备…'
  16. if (state?.phase === 'checking' || props.pending) return '正在检测…'
  17. if (state?.retryable === false) return '更新未完成'
  18. if (state?.available) return '可更新'
  19. if (state?.phase === 'failed' || props.error) return '重试更新'
  20. if (state?.phase === 'checked') return '已是最新版本'
  21. return '检测更新'
  22. })
  23. const detail = computed(() => props.error || props.state?.error || (props.state?.phase === 'ready'
  24. ? '更新已下载,点击重新启动完成安装' : props.state?.available ? `App ${props.state.latestVersion ?? ''} 可更新,点击下载` : props.state?.message))
  25. </script>
  26. <template>
  27. <div class="desktop-update-entry" :class="{ 'desktop-update-entry--compact': compact }">
  28. <button type="button" :role="menu ? 'menuitem' : undefined" :title="detail" :aria-label="label" :aria-busy="busy" :disabled="busy || state?.retryable === false" @click="emit('activate')">
  29. <span v-if="busy" class="desktop-update-ring" :class="{ 'desktop-update-ring--spinning': progress === undefined }" role="progressbar" :aria-label="label" :aria-valuemin="0" :aria-valuemax="100" :aria-valuenow="progress">
  30. <svg viewBox="0 0 24 24" aria-hidden="true"><circle class="desktop-update-ring__track" cx="12" cy="12" r="9" /><circle class="desktop-update-ring__value" cx="12" cy="12" r="9" pathLength="100" :stroke-dasharray="`${progress ?? 25} 100`" /></svg>
  31. </span>
  32. <AppIcon v-else :name="state?.phase === 'ready' ? 'refresh' : state?.available ? 'download' : 'refresh'" :size="18" />
  33. <span class="desktop-update-entry__label" aria-live="polite">{{ label }}</span>
  34. <span v-if="state?.available && !busy" class="desktop-update-dot" aria-hidden="true" />
  35. </button>
  36. <p v-if="error || state?.error" class="desktop-update-error" role="alert">{{ error || state?.error }}</p>
  37. </div>
  38. </template>
  39. <style scoped>
  40. .desktop-update-entry{width:100%;min-width:0;color:var(--text-primary)}
  41. .desktop-update-entry button{display:flex;align-items:center;gap:9px;width:100%;min-height:36px;padding:7px 10px;border:0;border-radius:7px;background:transparent;color:inherit;font:13px var(--font-ui);text-align:left;cursor:pointer}
  42. .desktop-update-entry button:hover{background:var(--surface-hover)}
  43. .desktop-update-entry button:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
  44. .desktop-update-entry button:disabled{cursor:default}
  45. .desktop-update-entry__label{flex:1;white-space:nowrap}
  46. .desktop-update-dot{width:6px;height:6px;border-radius:50%;background:var(--accent)}
  47. .desktop-update-ring{display:inline-flex;flex:0 0 18px;width:18px;height:18px;color:var(--accent)}
  48. .desktop-update-ring svg{width:100%;height:100%;transform:rotate(-90deg)}
  49. .desktop-update-ring circle{fill:none;stroke:currentColor;stroke-width:2.5}
  50. .desktop-update-ring__track{opacity:.18}.desktop-update-ring__value{stroke-linecap:round}
  51. .desktop-update-ring--spinning svg{animation:desktop-update-spin 1s linear infinite}
  52. .desktop-update-error{margin:4px 10px;color:var(--danger);font-size:12px;line-height:1.5;overflow-wrap:anywhere}
  53. .desktop-update-entry--compact button{justify-content:center;padding:9px}
  54. .desktop-update-entry--compact .desktop-update-entry__label,.desktop-update-entry--compact .desktop-update-dot,.desktop-update-entry--compact .desktop-update-error{display:none}
  55. @keyframes desktop-update-spin{to{transform:rotate(270deg)}}
  56. @media(prefers-reduced-motion:reduce){.desktop-update-ring--spinning svg{animation:none}}
  57. </style>