2026-05-13 22:21:07 +08:00
|
|
|
<template>
|
|
|
|
|
<Popover v-model:open="isOpen">
|
|
|
|
|
<PopoverTrigger as-child>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="flex h-9 w-9 items-center justify-center rounded-lg transition"
|
|
|
|
|
:class="buttonClass"
|
|
|
|
|
:title="buttonTitle"
|
2026-06-30 14:14:12 +08:00
|
|
|
:aria-label="$legacyT('版本信息')"
|
2026-05-13 22:21:07 +08:00
|
|
|
>
|
|
|
|
|
<Info
|
2026-05-22 15:15:17 +08:00
|
|
|
v-if="!isReconnecting"
|
2026-05-13 22:21:07 +08:00
|
|
|
class="h-4 w-4"
|
|
|
|
|
:class="loading ? 'animate-pulse' : ''"
|
|
|
|
|
/>
|
2026-05-22 15:15:17 +08:00
|
|
|
<RefreshCw
|
|
|
|
|
v-else
|
|
|
|
|
class="h-4 w-4 animate-spin"
|
|
|
|
|
/>
|
2026-05-13 22:21:07 +08:00
|
|
|
</button>
|
|
|
|
|
</PopoverTrigger>
|
|
|
|
|
|
|
|
|
|
<PopoverContent
|
|
|
|
|
align="end"
|
|
|
|
|
side="bottom"
|
|
|
|
|
:side-offset="8"
|
|
|
|
|
class="w-[22rem] max-w-[calc(100vw-1rem)] overflow-hidden rounded-xl border-border/60 bg-card/95 p-0 text-card-foreground shadow-xl shadow-black/5 backdrop-blur supports-[backdrop-filter]:bg-card/90"
|
|
|
|
|
>
|
|
|
|
|
<div class="text-left">
|
|
|
|
|
<div class="flex items-center justify-between gap-3 border-b border-border/60 bg-muted/30 px-3 py-2.5">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="text-xs font-semibold text-foreground">
|
2026-06-30 14:14:12 +08:00
|
|
|
{{ $legacyT('版本信息') }}
|
2026-05-13 22:21:07 +08:00
|
|
|
</div>
|
|
|
|
|
<div class="mt-0.5 text-[10px] uppercase tracking-[0.3em] text-muted-foreground">
|
|
|
|
|
System version
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<span
|
|
|
|
|
class="rounded-full border px-2 py-0.5 text-[10px] font-semibold"
|
|
|
|
|
:class="statusPillClass"
|
|
|
|
|
>
|
|
|
|
|
{{ statusLabel }}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="space-y-3 px-3 py-3">
|
|
|
|
|
<div class="rounded-lg border border-border/60 bg-muted/20 px-3 py-2.5">
|
|
|
|
|
<div>
|
|
|
|
|
<p class="text-xs text-muted-foreground">
|
2026-06-30 14:14:12 +08:00
|
|
|
{{ $legacyT('当前版本') }}
|
2026-05-13 22:21:07 +08:00
|
|
|
</p>
|
|
|
|
|
<p class="mt-1 break-all font-mono text-sm text-foreground">
|
|
|
|
|
{{ currentVersionLabel }}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
v-if="latestVersionLabel"
|
|
|
|
|
class="mt-2"
|
|
|
|
|
>
|
|
|
|
|
<p class="text-xs text-muted-foreground">
|
2026-06-30 14:14:12 +08:00
|
|
|
{{ $legacyT('最新版本') }}
|
2026-05-13 22:21:07 +08:00
|
|
|
</p>
|
|
|
|
|
<p class="mt-1 break-all font-mono text-sm text-foreground">
|
|
|
|
|
{{ latestVersionLabel }}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<p
|
|
|
|
|
v-if="status?.error"
|
|
|
|
|
class="text-xs text-muted-foreground"
|
|
|
|
|
>
|
2026-06-30 14:14:12 +08:00
|
|
|
{{ $legacyT('检查更新失败:') }}{{ $legacyT(status.error) }}
|
2026-05-13 22:21:07 +08:00
|
|
|
</p>
|
|
|
|
|
|
2026-05-22 15:15:17 +08:00
|
|
|
<p
|
|
|
|
|
v-if="status?.has_update && !canApplyUpdate"
|
|
|
|
|
class="text-xs text-muted-foreground"
|
|
|
|
|
>
|
|
|
|
|
{{ updateBlockerText }}
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<!-- Reconnecting / busy banner -->
|
|
|
|
|
<div
|
|
|
|
|
v-if="isReconnecting"
|
|
|
|
|
class="flex items-center justify-center gap-2 rounded-lg border border-primary/20 bg-primary/5 px-3 py-2 text-primary"
|
|
|
|
|
>
|
|
|
|
|
<RefreshCw class="h-3.5 w-3.5 animate-spin" />
|
2026-06-30 14:14:12 +08:00
|
|
|
<span class="text-xs font-medium">{{ $legacyT('服务重启中,请稍候...') }}</span>
|
2026-05-22 15:15:17 +08:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
v-else-if="isDownloadingUpdate"
|
|
|
|
|
class="rounded-lg border border-primary/20 bg-primary/5 px-3 py-2"
|
|
|
|
|
>
|
|
|
|
|
<div class="flex items-center justify-between gap-3 text-xs text-primary">
|
|
|
|
|
<span class="truncate">{{ downloadProgressText }}</span>
|
|
|
|
|
<span
|
|
|
|
|
v-if="downloadProgressPercent !== null"
|
|
|
|
|
class="shrink-0 font-mono"
|
|
|
|
|
>
|
|
|
|
|
{{ downloadProgressPercent }}%
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="mt-2 h-1.5 overflow-hidden rounded-full bg-primary/15">
|
|
|
|
|
<div
|
|
|
|
|
class="h-full rounded-full bg-primary transition-all duration-300"
|
|
|
|
|
:style="{ width: progressBarWidth }"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
v-else
|
|
|
|
|
class="flex items-center gap-2"
|
|
|
|
|
>
|
2026-05-13 22:21:07 +08:00
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
class="flex-1"
|
2026-05-22 15:15:17 +08:00
|
|
|
:disabled="isBusy || loading"
|
2026-05-13 22:21:07 +08:00
|
|
|
@click="handleRefresh"
|
|
|
|
|
>
|
|
|
|
|
<RefreshCw
|
|
|
|
|
class="mr-2 h-3.5 w-3.5"
|
|
|
|
|
:class="loading ? 'animate-spin' : ''"
|
|
|
|
|
/>
|
2026-06-30 14:14:12 +08:00
|
|
|
{{ $legacyT('重新检查') }}
|
2026-05-13 22:21:07 +08:00
|
|
|
</Button>
|
|
|
|
|
<Button
|
2026-05-22 15:15:17 +08:00
|
|
|
v-if="rollbackAvailable"
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
class="flex-1"
|
|
|
|
|
:disabled="isBusy"
|
|
|
|
|
@click="handleRollback"
|
|
|
|
|
>
|
|
|
|
|
{{ rollingBack ? '回滚中...' : '回滚' }}
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
v-if="status?.has_update && status.release_url && !rollbackAvailable"
|
2026-05-13 22:21:07 +08:00
|
|
|
size="sm"
|
|
|
|
|
class="flex-1"
|
|
|
|
|
@click="handleOpenRelease"
|
|
|
|
|
>
|
|
|
|
|
<ExternalLink class="mr-2 h-3.5 w-3.5" />
|
2026-05-22 15:15:17 +08:00
|
|
|
{{ releaseButtonLabel }}
|
2026-05-13 22:21:07 +08:00
|
|
|
</Button>
|
2026-05-20 22:02:47 +08:00
|
|
|
<Button
|
2026-05-22 15:15:17 +08:00
|
|
|
v-if="status?.has_update && canApplyUpdate"
|
2026-05-20 22:02:47 +08:00
|
|
|
size="sm"
|
|
|
|
|
class="flex-1"
|
2026-05-22 15:15:17 +08:00
|
|
|
:disabled="isBusy"
|
2026-05-20 22:02:47 +08:00
|
|
|
@click="handleApplyUpdate"
|
|
|
|
|
>
|
|
|
|
|
<RefreshCw
|
|
|
|
|
class="mr-2 h-3.5 w-3.5"
|
|
|
|
|
:class="updating ? 'animate-spin' : ''"
|
|
|
|
|
/>
|
|
|
|
|
{{ actionButtonLabel }}
|
|
|
|
|
</Button>
|
2026-05-13 22:21:07 +08:00
|
|
|
</div>
|
2026-05-22 15:15:17 +08:00
|
|
|
|
|
|
|
|
<!-- Releases List -->
|
|
|
|
|
<div v-if="!isReconnecting">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="flex w-full items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition"
|
|
|
|
|
@click="toggleReleases"
|
|
|
|
|
>
|
|
|
|
|
<ChevronRight
|
|
|
|
|
class="h-3 w-3 transition-transform"
|
|
|
|
|
:class="showReleases ? 'rotate-90' : ''"
|
|
|
|
|
/>
|
2026-06-30 14:14:12 +08:00
|
|
|
{{ $legacyT('历史版本') }}
|
2026-05-22 15:15:17 +08:00
|
|
|
<span
|
|
|
|
|
v-if="releases.length > 0"
|
|
|
|
|
class="text-[10px] text-muted-foreground/60"
|
|
|
|
|
>({{ releases.length }})</span>
|
|
|
|
|
<RefreshCw
|
|
|
|
|
v-if="loadingReleases"
|
|
|
|
|
class="ml-auto h-3 w-3 animate-spin text-muted-foreground"
|
|
|
|
|
/>
|
|
|
|
|
</button>
|
|
|
|
|
<div
|
|
|
|
|
v-if="showReleases"
|
|
|
|
|
class="mt-2 max-h-48 space-y-1 overflow-y-auto"
|
|
|
|
|
>
|
|
|
|
|
<p
|
|
|
|
|
v-if="releasesError"
|
|
|
|
|
class="text-xs text-muted-foreground"
|
|
|
|
|
>
|
|
|
|
|
{{ releasesError }}
|
|
|
|
|
</p>
|
|
|
|
|
<button
|
|
|
|
|
v-for="release in releases"
|
|
|
|
|
:key="release.version"
|
|
|
|
|
type="button"
|
|
|
|
|
class="flex w-full items-center justify-between rounded-md border border-border/40 px-2.5 py-1.5 text-left text-xs transition hover:border-primary/30 hover:bg-primary/5"
|
|
|
|
|
:class="release.is_current ? 'bg-primary/5 border-primary/20' : 'bg-muted/10'"
|
|
|
|
|
@click="openReleaseDetails(release)"
|
|
|
|
|
>
|
|
|
|
|
<div class="min-w-0 flex-1">
|
|
|
|
|
<div class="flex items-center gap-1.5">
|
|
|
|
|
<span class="break-all font-mono font-medium text-foreground">
|
|
|
|
|
{{ formatDisplayVersion(release.version) }}
|
|
|
|
|
</span>
|
|
|
|
|
<span
|
|
|
|
|
v-if="release.is_current"
|
|
|
|
|
class="shrink-0 rounded-full bg-primary/10 px-1.5 py-0.5 text-[10px] font-medium text-primary"
|
2026-06-30 14:14:12 +08:00
|
|
|
>{{ $legacyT('当前') }}</span>
|
2026-05-22 15:15:17 +08:00
|
|
|
<span
|
|
|
|
|
v-else-if="release.is_newer"
|
|
|
|
|
class="shrink-0 rounded-full bg-emerald-500/10 px-1.5 py-0.5 text-[10px] font-medium text-emerald-600 dark:text-emerald-400"
|
2026-06-30 14:14:12 +08:00
|
|
|
>{{ $legacyT('新') }}</span>
|
2026-05-22 15:15:17 +08:00
|
|
|
<span
|
|
|
|
|
v-if="release.is_newer && release.updatable === false"
|
|
|
|
|
class="shrink-0 rounded-full bg-amber-500/10 px-1.5 py-0.5 text-[10px] font-medium text-amber-600 dark:text-amber-400"
|
2026-06-30 14:14:12 +08:00
|
|
|
>{{ $legacyT('不可在线更新') }}</span>
|
2026-05-22 15:15:17 +08:00
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
v-if="release.published_at"
|
|
|
|
|
class="mt-0.5 text-[10px] text-muted-foreground"
|
|
|
|
|
>
|
|
|
|
|
{{ formatDate(release.published_at) }}
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
v-if="release.update_blocker"
|
|
|
|
|
class="mt-0.5 text-[10px] text-muted-foreground"
|
|
|
|
|
>
|
|
|
|
|
{{ release.update_blocker }}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="ml-2 shrink-0 text-[10px] font-medium text-muted-foreground">
|
2026-06-30 14:14:12 +08:00
|
|
|
{{ $legacyT('详情') }}
|
2026-05-22 15:15:17 +08:00
|
|
|
</span>
|
|
|
|
|
</button>
|
|
|
|
|
<p
|
|
|
|
|
v-if="!loadingReleases && releases.length === 0 && !releasesError"
|
|
|
|
|
class="py-2 text-center text-xs text-muted-foreground"
|
|
|
|
|
>
|
2026-06-30 14:14:12 +08:00
|
|
|
{{ $legacyT('暂无版本信息') }}
|
2026-05-22 15:15:17 +08:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-05-13 22:21:07 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
</Popover>
|
2026-05-22 15:15:17 +08:00
|
|
|
|
|
|
|
|
<Dialog
|
|
|
|
|
v-model="showReleaseDetails"
|
|
|
|
|
size="xl"
|
|
|
|
|
:title="selectedReleaseTitle"
|
|
|
|
|
:description="selectedReleaseDescription"
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
v-if="selectedRelease"
|
|
|
|
|
class="space-y-4"
|
|
|
|
|
>
|
|
|
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
|
|
|
<span class="rounded-full border border-border/60 bg-muted/20 px-2 py-0.5 font-mono text-[11px] text-foreground">
|
|
|
|
|
{{ formatDisplayVersion(selectedRelease.version) }}
|
|
|
|
|
</span>
|
|
|
|
|
<span
|
|
|
|
|
v-if="selectedRelease.is_current"
|
|
|
|
|
class="rounded-full bg-primary/10 px-2 py-0.5 text-[11px] font-medium text-primary"
|
|
|
|
|
>
|
2026-06-30 14:14:12 +08:00
|
|
|
{{ $legacyT('当前运行版本') }}
|
2026-05-22 15:15:17 +08:00
|
|
|
</span>
|
|
|
|
|
<span
|
|
|
|
|
v-else-if="selectedRelease.is_newer"
|
|
|
|
|
class="rounded-full bg-emerald-500/10 px-2 py-0.5 text-[11px] font-medium text-emerald-600 dark:text-emerald-400"
|
|
|
|
|
>
|
2026-06-30 14:14:12 +08:00
|
|
|
{{ $legacyT('可升级版本') }}
|
2026-05-22 15:15:17 +08:00
|
|
|
</span>
|
|
|
|
|
<span
|
|
|
|
|
v-else
|
|
|
|
|
class="rounded-full bg-muted px-2 py-0.5 text-[11px] font-medium text-muted-foreground"
|
|
|
|
|
>
|
2026-06-30 14:14:12 +08:00
|
|
|
{{ $legacyT('历史版本') }}
|
2026-05-22 15:15:17 +08:00
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<p
|
|
|
|
|
v-if="selectedReleaseHelpText"
|
|
|
|
|
class="text-xs text-muted-foreground"
|
|
|
|
|
>
|
|
|
|
|
{{ selectedReleaseHelpText }}
|
|
|
|
|
</p>
|
|
|
|
|
|
2026-07-04 05:24:40 +08:00
|
|
|
<!-- eslint-disable vue/no-v-html -->
|
2026-05-22 15:15:17 +08:00
|
|
|
<div
|
|
|
|
|
v-if="selectedReleaseDisplayNotes"
|
|
|
|
|
class="max-h-[26rem] overflow-y-auto rounded-xl border border-border/60 bg-muted/25 px-4 py-3 text-sm leading-6 text-foreground/90 shadow-inner shadow-black/[0.02] max-w-none prose prose-sm dark:prose-invert prose-headings:mb-2 prose-headings:mt-4 prose-headings:font-semibold prose-headings:text-foreground prose-h3:text-sm prose-p:my-2 prose-ul:my-2 prose-ul:list-disc prose-ul:pl-5 prose-li:my-1 prose-li:marker:text-primary prose-a:text-primary prose-strong:text-foreground prose-code:rounded prose-code:bg-muted prose-code:px-1 prose-code:py-0.5"
|
|
|
|
|
v-html="selectedReleaseNotesHtml"
|
|
|
|
|
/>
|
2026-07-04 05:24:40 +08:00
|
|
|
<!-- eslint-enable vue/no-v-html -->
|
2026-05-22 15:15:17 +08:00
|
|
|
<p
|
|
|
|
|
v-else
|
|
|
|
|
class="rounded-lg bg-muted/30 px-3 py-4 text-sm text-muted-foreground"
|
|
|
|
|
>
|
2026-06-30 14:14:12 +08:00
|
|
|
{{ $legacyT('这个版本没有附带更新说明。') }}
|
2026-05-22 15:15:17 +08:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
@click="showReleaseDetails = false"
|
|
|
|
|
>
|
2026-06-30 14:14:12 +08:00
|
|
|
{{ $legacyT('关闭') }}
|
2026-05-22 15:15:17 +08:00
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
v-if="selectedRelease?.release_url"
|
|
|
|
|
variant="outline"
|
|
|
|
|
@click="handleOpenSelectedReleasePage"
|
|
|
|
|
>
|
|
|
|
|
<ExternalLink class="mr-2 h-3.5 w-3.5" />
|
2026-06-30 14:14:12 +08:00
|
|
|
{{ $legacyT('查看标签页') }}
|
2026-05-22 15:15:17 +08:00
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
v-if="canUseSelectedRelease"
|
|
|
|
|
:disabled="isBusy"
|
|
|
|
|
@click="handleUseSelectedRelease"
|
|
|
|
|
>
|
|
|
|
|
<RefreshCw
|
|
|
|
|
class="mr-2 h-3.5 w-3.5"
|
|
|
|
|
:class="isBusy ? 'animate-spin' : ''"
|
|
|
|
|
/>
|
|
|
|
|
{{ selectedReleaseActionLabel }}
|
|
|
|
|
</Button>
|
|
|
|
|
</template>
|
|
|
|
|
</Dialog>
|
2026-05-13 22:21:07 +08:00
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { computed, ref } from 'vue'
|
2026-05-22 15:15:17 +08:00
|
|
|
import type { CheckUpdateResponse, ReleaseEntry } from '@/api/admin'
|
|
|
|
|
import { adminApi } from '@/api/admin'
|
|
|
|
|
import { Button, Dialog, Popover, PopoverContent, PopoverTrigger } from '@/components/ui'
|
|
|
|
|
import { normalizeReleaseNotesForDisplay } from '@/utils/releaseNotes'
|
2026-05-13 22:21:07 +08:00
|
|
|
import { formatDisplayVersion } from '@/utils/version'
|
|
|
|
|
import { describeUpdateStatus } from '@/utils/updateStatus'
|
2026-05-22 15:15:17 +08:00
|
|
|
import { sanitizeMarkdown } from '@/utils/sanitize'
|
2026-09-04 03:45:52 +08:00
|
|
|
import { safeExternalHttpsUrl } from '@/utils/navigationSecurity'
|
2026-06-30 14:14:12 +08:00
|
|
|
import { useI18n } from '@/i18n'
|
2026-05-22 15:15:17 +08:00
|
|
|
import { marked } from 'marked'
|
|
|
|
|
import { ChevronRight, ExternalLink, Info, RefreshCw } from 'lucide-vue-next'
|
|
|
|
|
|
2026-05-13 22:21:07 +08:00
|
|
|
const props = defineProps<{
|
|
|
|
|
status: CheckUpdateResponse | null
|
|
|
|
|
loading?: boolean
|
2026-05-20 22:02:47 +08:00
|
|
|
updating?: boolean
|
2026-05-22 15:15:17 +08:00
|
|
|
updatePhase?: 'download' | 'restart' | 'reconnecting'
|
|
|
|
|
updateSupported?: boolean
|
|
|
|
|
rollbackAvailable?: boolean
|
|
|
|
|
rollingBack?: boolean
|
|
|
|
|
downloadProgressText?: string | null
|
|
|
|
|
downloadProgressPercent?: number | null
|
2026-05-13 22:21:07 +08:00
|
|
|
}>()
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
refresh: []
|
|
|
|
|
openRelease: []
|
2026-05-20 22:02:47 +08:00
|
|
|
applyUpdate: []
|
2026-05-22 15:15:17 +08:00
|
|
|
previewRelease: [release: ReleaseEntry]
|
|
|
|
|
rollback: []
|
2026-05-13 22:21:07 +08:00
|
|
|
}>()
|
2026-06-30 14:14:12 +08:00
|
|
|
const { legacyT, locale } = useI18n()
|
2026-05-28 12:11:43 +08:00
|
|
|
const SOURCE_BUILD_UPDATE_HINT = '当前为源码构建,请使用 git pull 后重新编译。'
|
|
|
|
|
const SOURCE_BUILD_RELEASE_HINT = '当前为源码构建,请手动切换到对应标签后重新编译。'
|
2026-05-13 22:21:07 +08:00
|
|
|
|
|
|
|
|
const isOpen = ref(false)
|
2026-05-22 15:15:17 +08:00
|
|
|
const showReleases = ref(false)
|
|
|
|
|
const showReleaseDetails = ref(false)
|
|
|
|
|
const loadingReleases = ref(false)
|
|
|
|
|
const releases = ref<ReleaseEntry[]>([])
|
|
|
|
|
const releasesError = ref<string | null>(null)
|
|
|
|
|
const selectedRelease = ref<ReleaseEntry | null>(null)
|
|
|
|
|
let releasesFetched = false
|
2026-05-13 22:21:07 +08:00
|
|
|
|
|
|
|
|
const loading = computed(() => props.loading ?? false)
|
2026-05-20 22:02:47 +08:00
|
|
|
const updating = computed(() => props.updating ?? false)
|
|
|
|
|
const updatePhase = computed(() => props.updatePhase ?? 'download')
|
2026-05-22 15:15:17 +08:00
|
|
|
const updateSupported = computed(() => props.updateSupported ?? true)
|
|
|
|
|
const rollbackAvailable = computed(() => props.rollbackAvailable ?? false)
|
|
|
|
|
const rollingBack = computed(() => props.rollingBack ?? false)
|
|
|
|
|
const isReconnecting = computed(() => updatePhase.value === 'reconnecting')
|
|
|
|
|
const isDownloadingUpdate = computed(() => updating.value && updatePhase.value === 'download')
|
|
|
|
|
const isBusy = computed(() => updating.value || rollingBack.value || isReconnecting.value)
|
|
|
|
|
const canApplyUpdate = computed(() => updateSupported.value && props.status?.updatable !== false)
|
2026-06-30 14:14:12 +08:00
|
|
|
const downloadProgressText = computed(() => legacyT(props.downloadProgressText || '正在下载更新包...'))
|
2026-05-22 15:15:17 +08:00
|
|
|
const downloadProgressPercent = computed(() => {
|
|
|
|
|
const value = props.downloadProgressPercent
|
|
|
|
|
return typeof value === 'number' && Number.isFinite(value)
|
|
|
|
|
? Math.max(0, Math.min(100, Math.round(value)))
|
|
|
|
|
: null
|
|
|
|
|
})
|
|
|
|
|
const progressBarWidth = computed(() => {
|
|
|
|
|
return downloadProgressPercent.value === null ? '35%' : `${downloadProgressPercent.value}%`
|
|
|
|
|
})
|
|
|
|
|
const updateBlockerText = computed(() => {
|
|
|
|
|
if (!updateSupported.value) {
|
2026-06-30 14:14:12 +08:00
|
|
|
return legacyT(props.status?.update_blocker || SOURCE_BUILD_UPDATE_HINT)
|
2026-05-22 15:15:17 +08:00
|
|
|
}
|
2026-06-30 14:14:12 +08:00
|
|
|
return legacyT(props.status?.update_blocker || '当前版本暂不支持在线更新')
|
2026-05-22 15:15:17 +08:00
|
|
|
})
|
2026-06-30 14:14:12 +08:00
|
|
|
const releaseButtonLabel = computed(() => legacyT(updateSupported.value ? '查看更新' : '查看发布'))
|
2026-05-13 22:21:07 +08:00
|
|
|
const buttonClass = computed(() => {
|
|
|
|
|
const classes = []
|
|
|
|
|
|
2026-05-22 15:15:17 +08:00
|
|
|
if (isReconnecting.value) {
|
|
|
|
|
classes.push('bg-primary/10 text-primary animate-pulse')
|
|
|
|
|
return classes
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 22:21:07 +08:00
|
|
|
if (isOpen.value) {
|
|
|
|
|
classes.push('bg-muted/50')
|
|
|
|
|
} else {
|
|
|
|
|
classes.push('hover:bg-muted/50')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (props.status?.has_update) {
|
|
|
|
|
classes.push('text-primary')
|
|
|
|
|
} else if (isOpen.value) {
|
|
|
|
|
classes.push('text-foreground')
|
|
|
|
|
} else {
|
|
|
|
|
classes.push('text-muted-foreground hover:text-foreground')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return classes
|
|
|
|
|
})
|
2026-05-22 15:15:17 +08:00
|
|
|
const statusLabel = computed(() => {
|
2026-06-30 14:14:12 +08:00
|
|
|
if (isReconnecting.value) return legacyT('重启中')
|
|
|
|
|
if (rollingBack.value) return legacyT('回滚中')
|
|
|
|
|
if (updating.value) return legacyT('更新中')
|
|
|
|
|
return legacyT(describeUpdateStatus(props.status))
|
2026-05-22 15:15:17 +08:00
|
|
|
})
|
2026-05-13 22:21:07 +08:00
|
|
|
const currentVersionLabel = computed(() => {
|
|
|
|
|
return props.status?.current_version
|
|
|
|
|
? formatDisplayVersion(props.status.current_version)
|
2026-06-30 14:14:12 +08:00
|
|
|
: legacyT('加载中...')
|
2026-05-13 22:21:07 +08:00
|
|
|
})
|
|
|
|
|
const latestVersionLabel = computed(() => {
|
|
|
|
|
return props.status?.latest_version
|
|
|
|
|
? formatDisplayVersion(props.status.latest_version)
|
|
|
|
|
: ''
|
|
|
|
|
})
|
|
|
|
|
const statusPillClass = computed(() => {
|
2026-05-22 15:15:17 +08:00
|
|
|
if (isReconnecting.value || updating.value || rollingBack.value) {
|
|
|
|
|
return 'border-primary/20 bg-primary/10 text-primary'
|
|
|
|
|
}
|
2026-05-13 22:21:07 +08:00
|
|
|
if (!props.status) return 'border-border/60 bg-background/70 text-muted-foreground'
|
|
|
|
|
if (props.status.has_update) return 'border-primary/20 bg-primary/10 text-primary'
|
|
|
|
|
if (props.status.error) return 'border-destructive/20 bg-destructive/10 text-destructive'
|
|
|
|
|
return 'border-border/60 bg-background/70 text-muted-foreground'
|
|
|
|
|
})
|
|
|
|
|
const buttonTitle = computed(() => {
|
2026-06-30 14:14:12 +08:00
|
|
|
if (isReconnecting.value) return legacyT('服务重启中...')
|
|
|
|
|
if (!props.status) return legacyT('版本信息')
|
|
|
|
|
return `${legacyT('版本信息:')}${statusLabel.value}`
|
2026-05-13 22:21:07 +08:00
|
|
|
})
|
2026-05-20 22:02:47 +08:00
|
|
|
const actionButtonLabel = computed(() => {
|
|
|
|
|
if (updating.value) {
|
2026-06-30 14:14:12 +08:00
|
|
|
return legacyT(updatePhase.value === 'restart' ? '重启中...' : '下载中...')
|
2026-05-20 22:02:47 +08:00
|
|
|
}
|
2026-06-30 14:14:12 +08:00
|
|
|
return legacyT(updatePhase.value === 'restart' ? '立即重启' : '立即更新')
|
2026-05-20 22:02:47 +08:00
|
|
|
})
|
2026-05-22 15:15:17 +08:00
|
|
|
const selectedReleaseTitle = computed(() => {
|
|
|
|
|
return selectedRelease.value
|
2026-06-30 14:14:12 +08:00
|
|
|
? `${legacyT('版本详情')} · ${formatDisplayVersion(selectedRelease.value.version)}`
|
|
|
|
|
: legacyT('版本详情')
|
2026-05-22 15:15:17 +08:00
|
|
|
})
|
|
|
|
|
const selectedReleaseDescription = computed(() => {
|
|
|
|
|
return selectedRelease.value?.published_at
|
2026-06-30 14:14:12 +08:00
|
|
|
? `${legacyT('发布于')} ${formatDate(selectedRelease.value.published_at)}`
|
|
|
|
|
: legacyT('查看该版本的发布说明')
|
2026-05-22 15:15:17 +08:00
|
|
|
})
|
|
|
|
|
const canUseSelectedRelease = computed(() => {
|
|
|
|
|
return !!selectedRelease.value &&
|
|
|
|
|
!selectedRelease.value.is_current &&
|
|
|
|
|
selectedRelease.value.updatable !== false &&
|
|
|
|
|
updateSupported.value
|
|
|
|
|
})
|
|
|
|
|
const selectedReleaseActionLabel = computed(() => {
|
2026-06-30 14:14:12 +08:00
|
|
|
if (!selectedRelease.value) return legacyT('切换到此版本')
|
|
|
|
|
return legacyT(selectedRelease.value.is_newer ? '更新到此版本' : '切换到此版本')
|
2026-05-22 15:15:17 +08:00
|
|
|
})
|
|
|
|
|
const selectedReleaseHelpText = computed(() => {
|
|
|
|
|
if (!selectedRelease.value) return ''
|
2026-06-30 14:14:12 +08:00
|
|
|
if (selectedRelease.value.is_current) return legacyT('当前正在运行这个版本。')
|
2026-05-22 15:15:17 +08:00
|
|
|
if (!updateSupported.value) {
|
2026-06-30 14:14:12 +08:00
|
|
|
return legacyT(selectedRelease.value.update_blocker || SOURCE_BUILD_RELEASE_HINT)
|
2026-05-22 15:15:17 +08:00
|
|
|
}
|
2026-06-30 14:14:12 +08:00
|
|
|
if (selectedRelease.value.update_blocker) return legacyT(selectedRelease.value.update_blocker)
|
|
|
|
|
return legacyT(selectedRelease.value.is_newer
|
2026-05-22 15:15:17 +08:00
|
|
|
? '将这个版本作为在线更新目标。'
|
2026-06-30 14:14:12 +08:00
|
|
|
: '将切换到这个历史版本。')
|
2026-05-22 15:15:17 +08:00
|
|
|
})
|
|
|
|
|
const selectedReleaseDisplayNotes = computed(() => {
|
|
|
|
|
return normalizeReleaseNotesForDisplay(selectedRelease.value?.release_notes)
|
|
|
|
|
})
|
|
|
|
|
const selectedReleaseNotesHtml = computed(() => {
|
|
|
|
|
if (!selectedReleaseDisplayNotes.value) return ''
|
|
|
|
|
try {
|
|
|
|
|
const html = marked.parse(selectedReleaseDisplayNotes.value, {
|
|
|
|
|
async: false,
|
|
|
|
|
breaks: true
|
|
|
|
|
}) as string
|
|
|
|
|
return sanitizeMarkdown(html)
|
|
|
|
|
} catch {
|
|
|
|
|
return selectedReleaseDisplayNotes.value
|
|
|
|
|
.replace(/&/g, '&')
|
|
|
|
|
.replace(/</g, '<')
|
|
|
|
|
.replace(/>/g, '>')
|
|
|
|
|
.replace(/\n/g, '<br>')
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
function formatDate(dateStr: string): string {
|
|
|
|
|
try {
|
|
|
|
|
const date = new Date(dateStr)
|
2026-06-30 14:14:12 +08:00
|
|
|
return date.toLocaleDateString(locale.value, {
|
2026-05-22 15:15:17 +08:00
|
|
|
year: 'numeric',
|
|
|
|
|
month: 'short',
|
|
|
|
|
day: 'numeric'
|
|
|
|
|
})
|
|
|
|
|
} catch {
|
|
|
|
|
return dateStr
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function fetchReleases(force = false) {
|
|
|
|
|
if (releasesFetched && !force) return
|
|
|
|
|
loadingReleases.value = true
|
|
|
|
|
releasesError.value = null
|
|
|
|
|
try {
|
|
|
|
|
const data = await adminApi.getSystemReleases(force)
|
|
|
|
|
releases.value = data.releases
|
2026-06-30 14:14:12 +08:00
|
|
|
releasesError.value = data.error ? legacyT(data.error) : null
|
2026-05-22 15:15:17 +08:00
|
|
|
releasesFetched = true
|
|
|
|
|
} catch (err) {
|
2026-06-30 14:14:12 +08:00
|
|
|
releasesError.value = legacyT(err instanceof Error ? err.message : '获取版本列表失败')
|
2026-05-22 15:15:17 +08:00
|
|
|
} finally {
|
|
|
|
|
loadingReleases.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleReleases() {
|
|
|
|
|
showReleases.value = !showReleases.value
|
|
|
|
|
if (showReleases.value) {
|
|
|
|
|
fetchReleases()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openReleaseDetails(release: ReleaseEntry) {
|
|
|
|
|
selectedRelease.value = release
|
|
|
|
|
isOpen.value = false
|
|
|
|
|
showReleaseDetails.value = true
|
|
|
|
|
}
|
2026-05-13 22:21:07 +08:00
|
|
|
|
|
|
|
|
function handleRefresh() {
|
2026-05-22 15:15:17 +08:00
|
|
|
releasesFetched = false
|
|
|
|
|
if (showReleases.value) {
|
|
|
|
|
fetchReleases(true)
|
|
|
|
|
}
|
2026-05-13 22:21:07 +08:00
|
|
|
emit('refresh')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleOpenRelease() {
|
|
|
|
|
isOpen.value = false
|
|
|
|
|
emit('openRelease')
|
|
|
|
|
}
|
2026-05-20 22:02:47 +08:00
|
|
|
|
2026-05-22 15:15:17 +08:00
|
|
|
function handleOpenSelectedReleasePage() {
|
2026-09-04 03:45:52 +08:00
|
|
|
const releaseUrl = safeExternalHttpsUrl(selectedRelease.value?.release_url)
|
|
|
|
|
if (releaseUrl) {
|
|
|
|
|
window.open(releaseUrl, '_blank', 'noopener,noreferrer')
|
2026-05-22 15:15:17 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleUseSelectedRelease() {
|
|
|
|
|
if (!selectedRelease.value || !canUseSelectedRelease.value) return
|
|
|
|
|
showReleaseDetails.value = false
|
|
|
|
|
isOpen.value = false
|
|
|
|
|
emit('previewRelease', selectedRelease.value)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-20 22:02:47 +08:00
|
|
|
function handleApplyUpdate() {
|
|
|
|
|
emit('applyUpdate')
|
|
|
|
|
}
|
2026-05-22 15:15:17 +08:00
|
|
|
|
|
|
|
|
function handleRollback() {
|
|
|
|
|
emit('rollback')
|
|
|
|
|
}
|
2026-05-13 22:21:07 +08:00
|
|
|
</script>
|