feat: 添加版本更新检查功能

- 后端新增 /api/admin/system/check-update 接口,从 GitHub Tags 获取最新版本
- 前端新增 UpdateDialog 组件,管理员登录后自动检查更新并弹窗提示
- 同一会话内只检查一次,点击"稍后提醒"后 24 小时内不再提示
- CI 和 deploy.sh 自动生成 _version.py 版本文件
This commit is contained in:
fawney19
2026-01-08 03:01:54 +08:00
parent d378630b38
commit f2e62dd197
8 changed files with 373 additions and 43 deletions

View File

@@ -159,6 +159,15 @@ export interface EmailTemplateResetResponse {
}
}
// 检查更新响应
export interface CheckUpdateResponse {
current_version: string
latest_version: string | null
has_update: boolean
release_url: string | null
error: string | null
}
// LDAP 配置响应
export interface LdapConfigResponse {
server_url: string | null
@@ -526,6 +535,14 @@ export const adminApi = {
return response.data
},
// 检查系统更新
async checkUpdate(): Promise<CheckUpdateResponse> {
const response = await apiClient.get<CheckUpdateResponse>(
'/api/admin/system/check-update'
)
return response.data
},
// LDAP 配置相关
// 获取 LDAP 配置
async getLdapConfig(): Promise<LdapConfigResponse> {

View File

@@ -0,0 +1,112 @@
<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 -->
<div class="flex items-center gap-3 mb-4">
<span class="px-3 py-1.5 rounded-lg bg-muted text-sm font-mono text-muted-foreground">
v{{ currentVersion }}
</span>
<svg
class="h-4 w-4 text-muted-foreground"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 7l5 5m0 0l-5 5m5-5H6"
/>
</svg>
<span class="px-3 py-1.5 rounded-lg bg-primary/10 text-sm font-mono font-medium text-primary">
v{{ latestVersion }}
</span>
</div>
<!-- Description -->
<p class="text-sm text-muted-foreground max-w-xs">
新版本已发布建议更新以获得最新功能和安全修复
</p>
</div>
<template #footer>
<div class="flex w-full gap-3">
<Button
variant="outline"
class="flex-1"
@click="handleLater"
>
稍后提醒
</Button>
<Button
class="flex-1"
@click="handleViewRelease"
>
查看更新
</Button>
</div>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { Dialog } from '@/components/ui'
import Button from '@/components/ui/button.vue'
import HeaderLogo from '@/components/HeaderLogo.vue'
const props = defineProps<{
modelValue: boolean
currentVersion: string
latestVersion: string
releaseUrl: string | null
}>()
const emit = defineEmits<{
'update:modelValue': [value: boolean]
}>()
const isOpen = ref(props.modelValue)
watch(() => props.modelValue, (val) => {
isOpen.value = val
})
watch(isOpen, (val) => {
emit('update:modelValue', val)
})
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
}
</script>

View File

@@ -295,6 +295,15 @@
</template>
<RouterView />
<!-- 更新提示弹窗 -->
<UpdateDialog
v-if="updateInfo"
v-model="showUpdateDialog"
:current-version="updateInfo.current_version"
:latest-version="updateInfo.latest_version || ''"
:release-url="updateInfo.release_url"
/>
</AppShell>
</template>
@@ -304,10 +313,12 @@ import { useRoute, useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { useDarkMode } from '@/composables/useDarkMode'
import { isDemoMode } from '@/config/demo'
import { adminApi, type CheckUpdateResponse } from '@/api/admin'
import Button from '@/components/ui/button.vue'
import AppShell from '@/components/layout/AppShell.vue'
import SidebarNav from '@/components/layout/SidebarNav.vue'
import HeaderLogo from '@/components/HeaderLogo.vue'
import UpdateDialog from '@/components/common/UpdateDialog.vue'
import {
Home,
Users,
@@ -345,17 +356,67 @@ const showAuthError = ref(false)
const mobileMenuOpen = ref(false)
let authCheckInterval: number | null = null
// 更新检查相关
const showUpdateDialog = ref(false)
const updateInfo = ref<CheckUpdateResponse | null>(null)
// 路由变化时自动关闭移动端菜单
watch(() => route.path, () => {
mobileMenuOpen.value = false
})
// 检查是否应该显示更新提示
function shouldShowUpdatePrompt(latestVersion: string): boolean {
const ignoreKey = 'aether_update_ignore'
const ignoreData = localStorage.getItem(ignoreKey)
if (!ignoreData) return true
try {
const { version, until } = JSON.parse(ignoreData)
// 如果忽略的是同一版本且未过期,则不显示
if (version === latestVersion && Date.now() < until) {
return false
}
} catch {
// 解析失败,显示提示
}
return true
}
// 检查更新
async function checkForUpdate() {
// 只有管理员才检查更新
if (authStore.user?.role !== 'admin') return
// 同一会话内只检查一次
const sessionKey = 'aether_update_checked'
if (sessionStorage.getItem(sessionKey)) return
sessionStorage.setItem(sessionKey, '1')
try {
const result = await adminApi.checkUpdate()
if (result.has_update && result.latest_version) {
if (shouldShowUpdatePrompt(result.latest_version)) {
updateInfo.value = result
showUpdateDialog.value = true
}
}
} catch {
// 静默失败,不影响用户体验
}
}
onMounted(() => {
authCheckInterval = setInterval(() => {
if (authStore.user && !authStore.token) {
showAuthError.value = true
}
}, 5000)
// 延迟检查更新,避免影响页面加载
setTimeout(() => {
checkForUpdate()
}, 2000)
})
onUnmounted(() => {