mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-05-19 23:30:53 +08:00
功能: 系统更新检查(GitHub Release + Docker)
后端: - 新增 GET /api/system/update-check,从 GitHub Releases API 获取最新版本 - 自动比较当前版本与最新版本,匹配当前平台的下载链接 - 返回版本号、更新说明、下载链接、Docker 镜像信息 前端(系统设置页重构): - 新增"检查更新"按钮,点击后展示更新结果 - 有新版本时显示版本号、更新说明、下载按钮、Docker 更新命令 - 新增磁盘状态卡片(总空间/已用/可用/使用率) - 运行模式用彩色 Tag 区分(生产/开发)
This commit is contained in:
@@ -1,88 +1,137 @@
|
||||
import { Card, Descriptions, Grid, PageHeader, Space, Typography } from '@arco-design/web-react'
|
||||
import { Badge, Button, Card, Descriptions, Grid, Link, PageHeader, Space, Tag, Typography } from '@arco-design/web-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { fetchSystemInfo, type SystemInfo } from '../../services/system'
|
||||
import { fetchSystemInfo, checkUpdate, type SystemInfo, type UpdateCheckResult } from '../../services/system'
|
||||
import { resolveErrorMessage } from '../../utils/error'
|
||||
import { formatDuration } from '../../utils/format'
|
||||
|
||||
const { Row, Col } = Grid
|
||||
|
||||
const deploySteps = [
|
||||
'1. 构建前端:cd web && npm run build',
|
||||
'2. 编译后端:cd server && go build -o backupx ./cmd/backupx',
|
||||
'3. 部署静态资源与二进制,并按 deploy/ 目录提供的配置接入 Nginx 与 systemd',
|
||||
'4. 首次启动后访问 Web 控制台,完成管理员初始化与存储目标配置',
|
||||
]
|
||||
function formatBytes(bytes: number | undefined): string {
|
||||
if (!bytes || bytes <= 0) return '-'
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
let i = 0
|
||||
let size = bytes
|
||||
while (size >= 1024 && i < units.length - 1) {
|
||||
size /= 1024
|
||||
i++
|
||||
}
|
||||
return `${size.toFixed(1)} ${units[i]}`
|
||||
}
|
||||
|
||||
export function SettingsPage() {
|
||||
const [info, setInfo] = useState<SystemInfo | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
const [updateResult, setUpdateResult] = useState<UpdateCheckResult | null>(null)
|
||||
const [checking, setChecking] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
void (async () => {
|
||||
try {
|
||||
const result = await fetchSystemInfo()
|
||||
if (active) {
|
||||
setInfo(result)
|
||||
setError('')
|
||||
}
|
||||
if (active) { setInfo(result); setError('') }
|
||||
} catch (loadError) {
|
||||
if (active) {
|
||||
setError(resolveErrorMessage(loadError, '加载系统设置失败'))
|
||||
}
|
||||
if (active) setError(resolveErrorMessage(loadError, '加载系统信息失败'))
|
||||
} finally {
|
||||
if (active) {
|
||||
setLoading(false)
|
||||
}
|
||||
if (active) setLoading(false)
|
||||
}
|
||||
})()
|
||||
return () => {
|
||||
active = false
|
||||
}
|
||||
return () => { active = false }
|
||||
}, [])
|
||||
|
||||
async function handleCheckUpdate() {
|
||||
setChecking(true)
|
||||
try {
|
||||
const result = await checkUpdate()
|
||||
setUpdateResult(result)
|
||||
} catch (e) {
|
||||
setUpdateResult({ currentVersion: info?.version || '-', latestVersion: '-', hasUpdate: false, error: resolveErrorMessage(e, '检查更新失败') })
|
||||
} finally {
|
||||
setChecking(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
<PageHeader
|
||||
style={{ paddingBottom: 16 }}
|
||||
title="系统设置"
|
||||
subTitle="展示当前运行信息、部署入口和交付所需的基础操作说明"
|
||||
>
|
||||
<PageHeader style={{ paddingBottom: 16 }} title="系统设置" subTitle="运行信息、磁盘状态与版本更新">
|
||||
{error ? <Typography.Text type="error">{error}</Typography.Text> : null}
|
||||
</PageHeader>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Card loading={loading} title="运行信息">
|
||||
<Descriptions
|
||||
column={1}
|
||||
border
|
||||
data={[
|
||||
{ label: '版本', value: info?.version ?? '-' },
|
||||
{ label: '运行模式', value: info?.mode ?? '-' },
|
||||
{ label: '运行时长', value: formatDuration(info?.uptimeSeconds) },
|
||||
{ label: '启动时间', value: info?.startedAt ?? '-' },
|
||||
{ label: '数据库路径', value: info?.databasePath ?? '-' },
|
||||
]}
|
||||
/>
|
||||
<Descriptions column={1} border data={[
|
||||
{ label: '版本', value: <Space>{info?.version ?? '-'}<Button size="mini" type="text" loading={checking} onClick={handleCheckUpdate}>检查更新</Button></Space> },
|
||||
{ label: '运行模式', value: info?.mode === 'release' ? <Tag color="green">生产</Tag> : <Tag color="orange">{info?.mode ?? '-'}</Tag> },
|
||||
{ label: '运行时长', value: formatDuration(info?.uptimeSeconds) },
|
||||
{ label: '启动时间', value: info?.startedAt ?? '-' },
|
||||
{ label: '数据库路径', value: <Typography.Text copyable>{info?.databasePath ?? '-'}</Typography.Text> },
|
||||
]} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card title="部署资产">
|
||||
<Space direction="vertical" size="medium" style={{ width: '100%' }}>
|
||||
<Typography.Text>`deploy/nginx.conf`:静态资源托管与 `/api` 反向代理示例。</Typography.Text>
|
||||
<Typography.Text>`deploy/backupx.service`:systemd 服务单元,负责守护 API 进程。</Typography.Text>
|
||||
<Typography.Text>`deploy/install.sh`:一键安装示例脚本,用于创建目录、复制文件并启动服务。</Typography.Text>
|
||||
<Typography.Text>`README.md`:包含完整部署与使用文档。</Typography.Text>
|
||||
</Space>
|
||||
<Card loading={loading} title="磁盘状态">
|
||||
<Descriptions column={1} border data={[
|
||||
{ label: '总空间', value: formatBytes(info?.diskTotal) },
|
||||
{ label: '已用空间', value: formatBytes(info?.diskUsed) },
|
||||
{ label: '可用空间', value: formatBytes(info?.diskFree) },
|
||||
{ label: '使用率', value: info?.diskTotal ? `${((info.diskUsed / info.diskTotal) * 100).toFixed(1)}%` : '-' },
|
||||
]} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Card title="部署步骤">
|
||||
<div className="code-block">{deploySteps.join('\n')}</div>
|
||||
</Card>
|
||||
{/* 更新检查结果 */}
|
||||
{updateResult && (
|
||||
<Card title="版本更新">
|
||||
{updateResult.error ? (
|
||||
<Typography.Text type="warning">{updateResult.error}</Typography.Text>
|
||||
) : updateResult.hasUpdate ? (
|
||||
<Space direction="vertical" size="medium" style={{ width: '100%' }}>
|
||||
<Space>
|
||||
<Badge status="processing" />
|
||||
<Typography.Text style={{ fontWeight: 600 }}>
|
||||
有新版本可用:{updateResult.latestVersion}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary">(当前:{updateResult.currentVersion})</Typography.Text>
|
||||
</Space>
|
||||
{updateResult.publishedAt && (
|
||||
<Typography.Text type="secondary">发布时间:{new Date(updateResult.publishedAt).toLocaleString()}</Typography.Text>
|
||||
)}
|
||||
{updateResult.releaseNotes && (
|
||||
<Card size="small" title="更新说明" style={{ maxHeight: 200, overflow: 'auto' }}>
|
||||
<Typography.Paragraph style={{ whiteSpace: 'pre-wrap', marginBottom: 0 }}>{updateResult.releaseNotes}</Typography.Paragraph>
|
||||
</Card>
|
||||
)}
|
||||
<Space>
|
||||
{updateResult.downloadUrl && (
|
||||
<Link href={updateResult.downloadUrl} target="_blank">
|
||||
<Button type="primary">下载二进制包</Button>
|
||||
</Link>
|
||||
)}
|
||||
{updateResult.releaseUrl && (
|
||||
<Link href={updateResult.releaseUrl} target="_blank">
|
||||
<Button type="outline">查看 Release 详情</Button>
|
||||
</Link>
|
||||
)}
|
||||
</Space>
|
||||
{updateResult.dockerImage && (
|
||||
<Card size="small" title="Docker 更新命令">
|
||||
<Typography.Paragraph copyable code style={{ marginBottom: 0 }}>
|
||||
{`docker pull ${updateResult.dockerImage}:${updateResult.latestVersion} && docker compose up -d`}
|
||||
</Typography.Paragraph>
|
||||
</Card>
|
||||
)}
|
||||
</Space>
|
||||
) : (
|
||||
<Space>
|
||||
<Badge status="success" />
|
||||
<Typography.Text>当前已是最新版本 ({updateResult.currentVersion})</Typography.Text>
|
||||
</Space>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,11 +11,28 @@ export interface SystemInfo {
|
||||
diskUsed: number
|
||||
}
|
||||
|
||||
export interface UpdateCheckResult {
|
||||
currentVersion: string
|
||||
latestVersion: string
|
||||
hasUpdate: boolean
|
||||
releaseUrl?: string
|
||||
releaseNotes?: string
|
||||
publishedAt?: string
|
||||
downloadUrl?: string
|
||||
dockerImage?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export async function fetchSystemInfo() {
|
||||
const response = await http.get<{ code: string; message: string; data: SystemInfo }>('/system/info')
|
||||
return response.data.data
|
||||
}
|
||||
|
||||
export async function checkUpdate() {
|
||||
const response = await http.get<{ code: string; message: string; data: UpdateCheckResult }>('/system/update-check')
|
||||
return response.data.data
|
||||
}
|
||||
|
||||
export async function fetchSettings() {
|
||||
const response = await http.get<{ code: string; message: string; data: Record<string, string> }>('/settings')
|
||||
return response.data.data
|
||||
|
||||
Reference in New Issue
Block a user