feat(app): 添加服务端和客户端更新功能以及系统状态监控
Some checks failed
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Has been cancelled
Build Multi-Platform Binaries / build-frontend (push) Has been cancelled

- 在App.vue中新增服务端更新模态框和相关功能
- 添加applyServerUpdate API调用和更新确认对话框
- 实现客户端版本信息显示和更新检测功能
- 添加系统状态监控包括CPU、内存和磁盘使用情况
- 新增getClientSystemStats API接口获取客户端系统统计信息
- 更新go.mod和go.sum文件添加必要的依赖包
- 优化UI界面样式和交互体验
This commit is contained in:
Flik
2026-01-22 21:32:03 +08:00
parent 7cddb7b3e7
commit 9f13b0d4e9
21 changed files with 731 additions and 232 deletions

View File

@@ -3,17 +3,23 @@ import { ref, onMounted, computed, watch } from 'vue'
import { RouterView, useRouter, useRoute } from 'vue-router'
import {
HomeOutline, DesktopOutline, SettingsOutline,
PersonCircleOutline, LogOutOutline, LogoGithub, ServerOutline, CheckmarkCircleOutline, ArrowUpCircleOutline
PersonCircleOutline, LogOutOutline, LogoGithub, ServerOutline, CheckmarkCircleOutline, ArrowUpCircleOutline, CloseOutline
} from '@vicons/ionicons5'
import { getServerStatus, getVersionInfo, checkServerUpdate, removeToken, getToken, type UpdateInfo } from './api'
import { getServerStatus, getVersionInfo, checkServerUpdate, applyServerUpdate, removeToken, getToken, type UpdateInfo } from './api'
import { useToast } from './composables/useToast'
import { useConfirm } from './composables/useConfirm'
const router = useRouter()
const route = useRoute()
const message = useToast()
const dialog = useConfirm()
const serverInfo = ref({ bind_addr: '', bind_port: 0 })
const clientCount = ref(0)
const version = ref('')
const showUserMenu = ref(false)
const updateInfo = ref<UpdateInfo | null>(null)
const showUpdateModal = ref(false)
const updatingServer = ref(false)
const isLoginPage = computed(() => route.path === '/login')
@@ -84,6 +90,48 @@ const logout = () => {
const toggleUserMenu = () => {
showUserMenu.value = !showUserMenu.value
}
const openUpdateModal = () => {
if (updateInfo.value) {
showUpdateModal.value = true
}
}
const formatBytes = (bytes: number): string => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
const handleApplyServerUpdate = () => {
if (!updateInfo.value?.download_url) {
message.error('没有可用的下载链接')
return
}
dialog.warning({
title: '确认更新服务端',
content: `即将更新服务端到 ${updateInfo.value.latest},更新后服务器将自动重启。确定要继续吗?`,
positiveText: '更新并重启',
negativeText: '取消',
onPositiveClick: async () => {
updatingServer.value = true
try {
await applyServerUpdate(updateInfo.value!.download_url)
message.success('更新已开始,服务器将在几秒后重启')
showUpdateModal.value = false
setTimeout(() => {
window.location.reload()
}, 5000)
} catch (e: any) {
message.error(e.response?.data || '更新失败')
updatingServer.value = false
}
}
})
}
</script>
<template>
@@ -130,7 +178,7 @@ const toggleUserMenu = () => {
<div v-if="version" class="version-info">
<ServerOutline class="version-icon" />
<span class="version">v{{ version }}</span>
<span v-if="updateInfo" class="update-status" :class="{ latest: !updateInfo.available, 'has-update': updateInfo.available }">
<span v-if="updateInfo" class="update-status" :class="{ latest: !updateInfo.available, 'has-update': updateInfo.available }" @click="openUpdateModal">
<template v-if="updateInfo.available">
<ArrowUpCircleOutline class="status-icon" />
<span>新版本 ({{ updateInfo.latest }})</span>
@@ -148,6 +196,53 @@ const toggleUserMenu = () => {
</a>
<span class="copyright">© 2024 Flik. MIT License</span>
</footer>
<!-- Update Modal -->
<div v-if="showUpdateModal" class="modal-overlay" @click.self="showUpdateModal = false">
<div class="update-modal">
<div class="modal-header">
<h3>系统更新</h3>
<button class="close-btn" @click="showUpdateModal = false">
<CloseOutline />
</button>
</div>
<div class="modal-body" v-if="updateInfo">
<div class="update-info-grid">
<div class="info-row">
<span class="info-label">当前版本</span>
<span class="info-value">{{ updateInfo.current }}</span>
</div>
<div class="info-row">
<span class="info-label">最新版本</span>
<span class="info-value highlight">{{ updateInfo.latest }}</span>
</div>
<div v-if="updateInfo.asset_name" class="info-row">
<span class="info-label">文件名</span>
<span class="info-value">{{ updateInfo.asset_name }}</span>
</div>
<div v-if="updateInfo.asset_size" class="info-row">
<span class="info-label">文件大小</span>
<span class="info-value">{{ formatBytes(updateInfo.asset_size) }}</span>
</div>
</div>
<div v-if="updateInfo.release_note" class="release-note">
<span class="note-label">更新日志</span>
<pre>{{ updateInfo.release_note }}</pre>
</div>
</div>
<div class="modal-footer">
<button class="modal-btn" @click="showUpdateModal = false">取消</button>
<button
v-if="updateInfo?.available && updateInfo?.download_url"
class="modal-btn primary"
:disabled="updatingServer"
@click="handleApplyServerUpdate"
>
{{ updatingServer ? '更新中...' : '立即更新' }}
</button>
</div>
</div>
</div>
</div>
<RouterView v-else />
</template>
@@ -327,6 +422,12 @@ const toggleUserMenu = () => {
font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.update-status:hover {
opacity: 0.8;
}
.update-status.latest {
@@ -381,4 +482,148 @@ const toggleUserMenu = () => {
display: none;
}
}
/* Update Modal */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.update-modal {
background: rgba(30, 27, 75, 0.95);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 16px;
width: 90%;
max-width: 480px;
max-height: 80vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.modal-header {
padding: 16px 20px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: white;
}
.close-btn {
background: none;
border: none;
color: rgba(255, 255, 255, 0.6);
cursor: pointer;
padding: 4px;
display: flex;
transition: color 0.2s;
}
.close-btn:hover {
color: white;
}
.modal-body {
padding: 20px;
overflow-y: auto;
flex: 1;
}
.update-info-grid {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 16px;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.info-label {
color: rgba(255, 255, 255, 0.6);
font-size: 13px;
}
.info-value {
color: white;
font-size: 13px;
font-weight: 500;
}
.info-value.highlight {
color: #34d399;
}
.release-note {
margin-top: 16px;
}
.note-label {
display: block;
font-size: 12px;
color: rgba(255, 255, 255, 0.5);
margin-bottom: 8px;
}
.release-note pre {
margin: 0;
white-space: pre-wrap;
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
background: rgba(0, 0, 0, 0.2);
padding: 12px;
border-radius: 8px;
max-height: 200px;
overflow-y: auto;
}
.modal-footer {
padding: 16px 20px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
justify-content: flex-end;
gap: 8px;
}
.modal-btn {
padding: 8px 16px;
border-radius: 8px;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.15);
color: white;
}
.modal-btn:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.2);
}
.modal-btn.primary {
background: linear-gradient(135deg, #60a5fa 0%, #a78bfa 100%);
border: none;
}
.modal-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>

View File

@@ -196,6 +196,19 @@ export interface TrafficRecord {
export const getTrafficStats = () => get<TrafficStats>('/traffic/stats')
export const getTrafficHourly = () => get<{ records: TrafficRecord[] }>('/traffic/hourly')
// 客户端系统状态
export interface SystemStats {
cpu_usage: number
memory_total: number
memory_used: number
memory_usage: number
disk_total: number
disk_used: number
disk_usage: number
}
export const getClientSystemStats = (clientId: string) => get<SystemStats>(`/client/${clientId}/system-stats`)
// 服务器配置
export interface ServerConfigInfo {
bind_addr: string

View File

@@ -76,6 +76,7 @@ export interface ClientDetail {
remote_addr?: string
os?: string
arch?: string
version?: string
}
// 服务器状态

View File

@@ -15,7 +15,7 @@ import {
getClient, updateClient, deleteClient, pushConfigToClient, disconnectClient, restartClient,
getClientPluginConfig, updateClientPluginConfig,
getStorePlugins, installStorePlugin, getRuleSchemas, startClientPlugin, restartClientPlugin, stopClientPlugin, deleteClientPlugin,
checkClientUpdate, applyClientUpdate, type UpdateInfo
checkClientUpdate, applyClientUpdate, getClientSystemStats, type UpdateInfo, type SystemStats
} from '../api'
import type { ProxyRule, ClientPlugin, ConfigField, StorePluginInfo, RuleSchemasMap } from '../types'
import LogViewer from '../components/LogViewer.vue'
@@ -37,12 +37,17 @@ const clientPlugins = ref<ClientPlugin[]>([])
const loading = ref(false)
const clientOs = ref('')
const clientArch = ref('')
const clientVersion = ref('')
// 客户端更新相关
const clientUpdate = ref<UpdateInfo | null>(null)
const checkingUpdate = ref(false)
const updatingClient = ref(false)
// 系统状态相关
const systemStats = ref<SystemStats | null>(null)
const loadingStats = ref(false)
// Rule Schemas
const pluginRuleSchemas = ref<RuleSchemasMap>({})
const loadRuleSchemas = async () => {
@@ -103,6 +108,13 @@ const loadClient = async () => {
clientPlugins.value = data.plugins || []
clientOs.value = data.os || ''
clientArch.value = data.arch || ''
clientVersion.value = data.version || ''
// 如果客户端在线且有平台信息,自动检测更新
if (data.online && data.os && data.arch) {
autoCheckClientUpdate()
loadSystemStats()
}
} catch (e) {
message.error('加载客户端信息失败')
console.error(e)
@@ -111,6 +123,39 @@ const loadClient = async () => {
}
}
// 自动检测客户端更新(静默)
const autoCheckClientUpdate = async () => {
try {
const { data } = await checkClientUpdate(clientOs.value, clientArch.value)
clientUpdate.value = data
} catch (e) {
console.error('Auto check update failed', e)
}
}
// 加载系统状态
const loadSystemStats = async () => {
if (!online.value) return
loadingStats.value = true
try {
const { data } = await getClientSystemStats(clientId)
systemStats.value = data
} catch (e) {
console.error('Failed to load system stats', e)
} finally {
loadingStats.value = false
}
}
// 格式化字节大小
const formatBytes = (bytes: number): string => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
// 客户端更新
const handleCheckClientUpdate = async () => {
if (!online.value) {
@@ -473,11 +518,11 @@ const handleDeletePlugin = (plugin: ClientPlugin) => {
<div class="page-header">
<div class="header-left">
<button class="back-btn" @click="router.push('/')">
<n-icon size="20"><ArrowBackOutline /></n-icon>
<ArrowBackOutline class="btn-icon-lg" />
</button>
<h1 class="page-title">{{ nickname || clientId }}</h1>
<button class="edit-btn" @click="openRenameModal">
<n-icon size="16"><CreateOutline /></n-icon>
<CreateOutline class="btn-icon" />
</button>
<span class="status-tag" :class="{ online }">
{{ online ? '在线' : '离线' }}
@@ -485,15 +530,15 @@ const handleDeletePlugin = (plugin: ClientPlugin) => {
</div>
<div class="header-actions">
<button v-if="online" class="glass-btn primary" @click="pushConfigToClient(clientId).then(() => message.success('已推送'))">
<n-icon size="16"><PushOutline /></n-icon>
<PushOutline class="btn-icon" />
<span>推送配置</span>
</button>
<button class="glass-btn" @click="showLogViewer=true">
<n-icon size="16"><DocumentTextOutline /></n-icon>
<DocumentTextOutline class="btn-icon" />
<span>日志</span>
</button>
<button class="glass-btn danger" @click="confirmDelete">
<n-icon size="16"><TrashOutline /></n-icon>
<TrashOutline class="btn-icon" />
<span>删除</span>
</button>
</div>
@@ -517,6 +562,15 @@ const handleDeletePlugin = (plugin: ClientPlugin) => {
<span class="stat-label">远程 IP</span>
<span class="stat-value">{{ remoteAddr || '-' }}</span>
</div>
<div class="stat-item">
<span class="stat-label">客户端版本</span>
<span class="stat-value">
{{ clientVersion || '-' }}
<span v-if="clientUpdate?.available" class="update-badge" @click="handleApplyClientUpdate">
可更新
</span>
</span>
</div>
<div class="stat-item">
<span class="stat-label">最后心跳</span>
<span class="stat-value">{{ lastPing ? new Date(lastPing).toLocaleTimeString() : '-' }}</span>
@@ -545,12 +599,57 @@ const handleDeletePlugin = (plugin: ClientPlugin) => {
</div>
</div>
<!-- System Stats Card -->
<div class="glass-card" v-if="online">
<div class="card-header">
<h3>系统状态</h3>
<button class="glass-btn tiny" :disabled="loadingStats" @click="loadSystemStats">
<RefreshOutline class="btn-icon-sm" />
刷新
</button>
</div>
<div class="card-body">
<div v-if="!systemStats" class="empty-hint">
{{ loadingStats ? '加载中...' : '点击刷新获取状态' }}
</div>
<template v-else>
<div class="system-stat-item">
<span class="system-stat-label">CPU</span>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: systemStats.cpu_usage + '%' }"></div>
</div>
<span class="system-stat-value">{{ systemStats.cpu_usage.toFixed(1) }}%</span>
</div>
<div class="system-stat-item">
<span class="system-stat-label">内存</span>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: systemStats.memory_usage + '%' }"></div>
</div>
<span class="system-stat-value">{{ systemStats.memory_usage.toFixed(1) }}%</span>
</div>
<div class="system-stat-detail">
{{ formatBytes(systemStats.memory_used) }} / {{ formatBytes(systemStats.memory_total) }}
</div>
<div class="system-stat-item">
<span class="system-stat-label">磁盘</span>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: systemStats.disk_usage + '%' }"></div>
</div>
<span class="system-stat-value">{{ systemStats.disk_usage.toFixed(1) }}%</span>
</div>
<div class="system-stat-detail">
{{ formatBytes(systemStats.disk_used) }} / {{ formatBytes(systemStats.disk_total) }}
</div>
</template>
</div>
</div>
<!-- Update Card -->
<div class="glass-card">
<div class="card-header">
<h3>客户端更新</h3>
<button class="glass-btn tiny" :disabled="!online || checkingUpdate" @click="handleCheckClientUpdate">
<n-icon size="14"><RefreshOutline /></n-icon>
<RefreshOutline class="btn-icon-sm" />
检查
</button>
</div>
@@ -563,7 +662,7 @@ const handleDeletePlugin = (plugin: ClientPlugin) => {
<div v-if="clientUpdate.download_url" class="update-available">
<p>发现新版本 {{ clientUpdate.latest }}</p>
<button class="glass-btn primary small" :disabled="updatingClient" @click="handleApplyClientUpdate">
<n-icon size="14"><CloudDownloadOutline /></n-icon>
<CloudDownloadOutline class="btn-icon-sm" />
更新
</button>
</div>
@@ -580,7 +679,7 @@ const handleDeletePlugin = (plugin: ClientPlugin) => {
<div class="card-header">
<h3>代理规则</h3>
<button class="glass-btn primary small" @click="openCreateRule">
<n-icon size="14"><AddOutline /></n-icon>
<AddOutline class="btn-icon-sm" />
添加规则
</button>
</div>
@@ -757,7 +856,7 @@ const handleDeletePlugin = (plugin: ClientPlugin) => {
<div v-for="plugin in storePlugins" :key="plugin.name" class="store-plugin-card">
<div class="store-plugin-header">
<span class="store-plugin-name">{{ plugin.name }}</span>
<n-tag size="small">v{{ plugin.version }}</n-tag>
<GlassTag>v{{ plugin.version }}</GlassTag>
</div>
<p class="store-plugin-desc">{{ plugin.description }}</p>
<button class="glass-btn primary small full" @click="handleInstallStorePlugin(plugin)">
@@ -1000,6 +1099,22 @@ const handleDeletePlugin = (plugin: ClientPlugin) => {
font-size: 12px;
}
.update-badge {
display: inline-block;
margin-left: 8px;
padding: 2px 8px;
font-size: 11px;
background: rgba(251, 191, 36, 0.2);
color: #fbbf24;
border-radius: 10px;
cursor: pointer;
transition: all 0.2s;
}
.update-badge:hover {
background: rgba(251, 191, 36, 0.3);
}
/* Mini Stats */
.stats-row {
display: flex;
@@ -1335,6 +1450,16 @@ const handleDeletePlugin = (plugin: ClientPlugin) => {
height: 14px;
}
.btn-icon-lg {
width: 20px;
height: 20px;
}
.btn-icon-sm {
width: 14px;
height: 14px;
}
.plugin-icon {
width: 18px;
height: 18px;
@@ -1345,4 +1470,49 @@ const handleDeletePlugin = (plugin: ClientPlugin) => {
width: 16px;
height: 16px;
}
/* System Stats */
.system-stat-item {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
}
.system-stat-label {
width: 40px;
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
}
.progress-bar {
flex: 1;
height: 8px;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #60a5fa, #a78bfa);
border-radius: 4px;
transition: width 0.3s ease;
}
.system-stat-value {
width: 50px;
text-align: right;
font-size: 12px;
color: white;
font-family: monospace;
}
.system-stat-detail {
font-size: 11px;
color: rgba(255, 255, 255, 0.4);
text-align: right;
margin-bottom: 12px;
margin-top: -4px;
}
</style>

View File

@@ -37,7 +37,24 @@ const loadTrafficStats = async () => {
const loadTrafficHourly = async () => {
try {
const { data } = await getTrafficHourly()
trafficHistory.value = data.records || []
const records = data.records || []
// 如果没有数据生成从当前时间开始的24小时空数据
if (records.length === 0) {
const now = new Date()
const currentHour = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours())
const emptyRecords: TrafficRecord[] = []
for (let i = 23; i >= 0; i--) {
const ts = new Date(currentHour.getTime() - i * 3600 * 1000)
emptyRecords.push({
timestamp: Math.floor(ts.getTime() / 1000),
inbound: 0,
outbound: 0
})
}
trafficHistory.value = emptyRecords
} else {
trafficHistory.value = records
}
} catch (e) {
console.error('Failed to load hourly traffic', e)
}
@@ -106,8 +123,8 @@ onMounted(() => {
<div class="dashboard-content">
<!-- Header -->
<div class="dashboard-header">
<h1 class="text-3xl font-bold text-white mb-2">Dashboard</h1>
<p class="text-white/70">Monitor your tunnel connections and traffic</p>
<h1 class="text-3xl font-bold text-white mb-2">仪表盘</h1>
<p class="text-white/70">监控隧道连接和流量状态</p>
</div>
<!-- Stats Grid -->
@@ -226,9 +243,6 @@ onMounted(() => {
</div>
</div>
</div>
<div class="chart-hint" v-if="trafficHistory.length === 0">
<span>暂无流量数据</span>
</div>
</div>
</div>
</div>
@@ -411,25 +425,29 @@ onMounted(() => {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
gap: 2px;
min-height: 48px;
justify-content: center;
}
.stat-label {
font-size: 13px;
color: rgba(255, 255, 255, 0.6);
font-weight: 500;
line-height: 1.2;
}
.stat-value {
font-size: 32px;
font-size: 28px;
font-weight: 700;
color: white;
line-height: 1.1;
line-height: 1.2;
}
.stat-unit {
font-size: 12px;
color: rgba(255, 255, 255, 0.5);
line-height: 1.2;
}
/* Client count special styling */

View File

@@ -1,23 +1,16 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { CloudDownloadOutline, RefreshOutline, ServerOutline, SettingsOutline, SaveOutline } from '@vicons/ionicons5'
import GlassTag from '../components/GlassTag.vue'
import { ServerOutline, SettingsOutline, SaveOutline } from '@vicons/ionicons5'
import { useToast } from '../composables/useToast'
import { useConfirm } from '../composables/useConfirm'
import {
getVersionInfo, checkServerUpdate, applyServerUpdate,
getServerConfig, updateServerConfig,
type UpdateInfo, type VersionInfo, type ServerConfigResponse
getVersionInfo, getServerConfig, updateServerConfig,
type VersionInfo, type ServerConfigResponse
} from '../api'
const message = useToast()
const dialog = useConfirm()
const versionInfo = ref<VersionInfo | null>(null)
const serverUpdate = ref<UpdateInfo | null>(null)
const loading = ref(true)
const checkingServer = ref(false)
const updatingServer = ref(false)
// 服务器配置
const serverConfig = ref<ServerConfigResponse | null>(null)
@@ -96,58 +89,6 @@ const handleSaveConfig = async () => {
}
}
const handleCheckServerUpdate = async () => {
checkingServer.value = true
try {
const { data } = await checkServerUpdate()
serverUpdate.value = data
if (data.available) {
message.success('发现新版本: ' + data.latest)
} else {
message.info('已是最新版本')
}
} catch (e: any) {
message.error(e.response?.data || '检查更新失败')
} finally {
checkingServer.value = false
}
}
const handleApplyServerUpdate = () => {
if (!serverUpdate.value?.download_url) {
message.error('没有可用的下载链接')
return
}
dialog.warning({
title: '确认更新服务端',
content: `即将更新服务端到 ${serverUpdate.value.latest},更新后服务器将自动重启。确定要继续吗?`,
positiveText: '更新并重启',
negativeText: '取消',
onPositiveClick: async () => {
updatingServer.value = true
try {
await applyServerUpdate(serverUpdate.value!.download_url)
message.success('更新已开始,服务器将在几秒后重启')
setTimeout(() => {
window.location.reload()
}, 5000)
} catch (e: any) {
message.error(e.response?.data || '更新失败')
updatingServer.value = false
}
}
})
}
const formatBytes = (bytes: number): string => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
onMounted(() => {
loadVersionInfo()
loadServerConfig()
@@ -301,50 +242,6 @@ onMounted(() => {
<div v-else class="empty-state">无法加载配置信息</div>
</div>
</div>
<!-- Server Update Card -->
<div class="glass-card">
<div class="card-header">
<h3>服务端更新</h3>
<button class="glass-btn small" :disabled="checkingServer" @click="handleCheckServerUpdate">
<RefreshOutline class="btn-icon" />
检查更新
</button>
</div>
<div class="card-body">
<div v-if="!serverUpdate" class="empty-state">
点击检查更新按钮查看是否有新版本
</div>
<template v-else>
<div v-if="serverUpdate.available" class="update-alert success">
发现新版本 {{ serverUpdate.latest }}当前版本 {{ serverUpdate.current }}
</div>
<div v-else class="update-alert info">
当前已是最新版本 {{ serverUpdate.current }}
</div>
<div v-if="serverUpdate.download_url" class="download-info">
下载文件: {{ serverUpdate.asset_name }}
<GlassTag style="margin-left: 8px;">{{ formatBytes(serverUpdate.asset_size) }}</GlassTag>
</div>
<div v-if="serverUpdate.release_note" class="release-note">
<span class="note-label">更新日志:</span>
<pre>{{ serverUpdate.release_note }}</pre>
</div>
<button
v-if="serverUpdate.available && serverUpdate.download_url"
class="glass-btn primary"
:disabled="updatingServer"
@click="handleApplyServerUpdate"
>
<CloudDownloadOutline class="btn-icon" />
下载并更新服务端
</button>
</template>
</div>
</div>
</div>
</div>
</template>