Files
Aether/frontend/src/components/common/UpdateDialog.vue
T

193 lines
5.0 KiB
Vue
Raw Normal View History

2026-01-08 03:01:54 +08:00
<template>
<Dialog
v-model="isOpen"
size="md"
title=""
>
<div class="flex flex-col items-center text-center py-2">
<!-- Logo -->
<HeaderLogo
size="h-16 w-16"
class-name="text-primary"
/>
<!-- Title -->
<h2 class="text-xl font-semibold text-foreground mt-4 mb-2">
发现新版本
</h2>
<!-- Version Info -->
2026-05-13 22:21:07 +08:00
<div class="mx-auto mb-2 w-full max-w-sm rounded-lg bg-muted/20 px-4 py-3 text-center">
<p class="text-xs text-muted-foreground">
最新版本
</p>
<p class="mt-1 break-all font-mono text-base font-semibold text-primary">
{{ formatDisplayVersion(latestVersion) }}
</p>
2026-01-08 03:01:54 +08:00
</div>
<!-- Release Notes -->
<div
v-if="releaseNotes"
2026-05-13 22:21:07 +08:00
class="w-full mt-3 mb-4"
>
2026-05-13 22:21:07 +08:00
<div
v-if="publishedAt"
class="text-left text-xs text-muted-foreground mb-2"
>
发布于 {{ formattedPublishedAt }}
</div>
<div class="text-left text-xs font-medium text-muted-foreground mb-2">
更新内容
</div>
<!-- eslint-disable vue/no-v-html -->
<div
class="w-full max-h-48 overflow-y-auto rounded-lg bg-muted/50 p-3 text-left text-sm text-foreground/80 prose prose-sm dark:prose-invert prose-p:my-1 prose-ul:my-1 prose-li:my-0"
v-html="renderedReleaseNotes"
/>
<!-- eslint-enable vue/no-v-html -->
</div>
<!-- Description (fallback when no release notes) -->
<p
v-else
2026-05-13 22:21:07 +08:00
class="text-sm text-muted-foreground max-w-xs mt-2 mb-4"
>
2026-01-08 03:01:54 +08:00
新版本已发布,建议更新以获得最新功能和安全修复
</p>
2026-05-20 22:02:47 +08:00
<p
v-if="updatePhase === 'restart'"
class="mt-1 text-xs text-primary"
>
更新包已下载,点击“立即重启”完成安装
</p>
2026-01-08 03:01:54 +08:00
</div>
<template #footer>
<div class="flex w-full gap-3">
<Button
variant="outline"
class="flex-1"
2026-05-20 22:02:47 +08:00
:disabled="updating"
2026-01-08 03:01:54 +08:00
@click="handleLater"
>
稍后提醒
</Button>
<Button
2026-05-20 22:02:47 +08:00
variant="outline"
2026-01-08 03:01:54 +08:00
class="flex-1"
2026-05-20 22:02:47 +08:00
:disabled="updating"
2026-01-08 03:01:54 +08:00
@click="handleViewRelease"
>
查看更新
</Button>
2026-05-20 22:02:47 +08:00
<Button
class="flex-1"
:disabled="updating"
@click="handleApplyUpdate"
>
{{ actionButtonLabel }}
</Button>
2026-01-08 03:01:54 +08:00
</div>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
2026-01-08 03:01:54 +08:00
import { Dialog } from '@/components/ui'
import Button from '@/components/ui/button.vue'
import HeaderLogo from '@/components/HeaderLogo.vue'
2026-05-13 22:21:07 +08:00
import { formatDisplayVersion } from '@/utils/version'
import { marked } from 'marked'
import DOMPurify from 'dompurify'
2026-01-08 03:01:54 +08:00
const props = defineProps<{
modelValue: boolean
currentVersion: string
latestVersion: string
releaseUrl: string | null
releaseNotes: string | null
publishedAt: string | null
2026-05-20 22:02:47 +08:00
updatePhase?: 'download' | 'restart'
updating?: boolean
2026-01-08 03:01:54 +08:00
}>()
const emit = defineEmits<{
'update:modelValue': [value: boolean]
2026-05-20 22:02:47 +08:00
applyUpdate: []
2026-01-08 03:01:54 +08:00
}>()
const isOpen = ref(props.modelValue)
2026-05-20 22:02:47 +08:00
const updating = computed(() => props.updating ?? false)
const updatePhase = computed(() => props.updatePhase ?? 'download')
const actionButtonLabel = computed(() => {
if (updating.value) {
return updatePhase.value === 'restart' ? '重启中...' : '下载中...'
}
return updatePhase.value === 'restart' ? '立即重启' : '立即更新'
})
2026-01-08 03:01:54 +08:00
watch(() => props.modelValue, (val) => {
isOpen.value = val
})
watch(isOpen, (val) => {
emit('update:modelValue', val)
})
// 格式化发布时间
const formattedPublishedAt = computed(() => {
if (!props.publishedAt) return ''
try {
const date = new Date(props.publishedAt)
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
} catch {
return props.publishedAt
}
})
// 渲染 Markdown 格式的 Release Notes(使用 DOMPurify 防止 XSS)
const renderedReleaseNotes = computed(() => {
if (!props.releaseNotes) return ''
try {
const html = marked.parse(props.releaseNotes, { async: false }) as string
return DOMPurify.sanitize(html)
} catch {
// 如果 markdown 解析失败,返回原始文本(转义 HTML)
return props.releaseNotes
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\n/g, '<br>')
}
})
2026-01-08 03:01:54 +08:00
function handleLater() {
// 记录忽略的版本,24小时内不再提醒
const ignoreKey = 'aether_update_ignore'
const ignoreData = {
version: props.latestVersion,
until: Date.now() + 24 * 60 * 60 * 1000 // 24小时
}
localStorage.setItem(ignoreKey, JSON.stringify(ignoreData))
isOpen.value = false
}
function handleViewRelease() {
if (props.releaseUrl) {
window.open(props.releaseUrl, '_blank')
}
isOpen.value = false
}
2026-05-20 22:02:47 +08:00
function handleApplyUpdate() {
emit('applyUpdate')
}
2026-01-08 03:01:54 +08:00
</script>