feat: 设置页新增「系统与更新」,按 GitHub 版本号检查更新

- 顶层 package.json 作为应用版本唯一来源(happy-export v1.0.0)
- backend/lib/updater.js:查 GitHub Release(优先)或 tag,与本机版本做 semver 比对
- backend/routes/system.js:GET /api/system/info、GET /api/system/check-update(30s 缓存)
- /api/health 版本号改为读真实版本
- 设置页新增第 4 个分类:当前版本 / 更新源仓库 / 检查更新(含更新说明折叠)
- 新增 RefreshIcon
This commit is contained in:
Drsakura
2026-09-10 16:23:42 +08:00
parent d8df2c3960
commit e4537d5bf0
7 changed files with 476 additions and 2 deletions
+169 -1
View File
@@ -1,5 +1,6 @@
import React, { useState, useEffect, useRef } from 'react'
import { MoonIcon, SunIcon, TuneIcon, CheckIcon, CloseIcon, ExchangeIcon, SparkIcon } from './Icons'
import axios from 'axios'
import { MoonIcon, SunIcon, TuneIcon, CheckIcon, CloseIcon, ExchangeIcon, SparkIcon, RefreshIcon } from './Icons'
function Settings({ isOpen, onClose, theme, onThemeChange, reducedMotion, onReducedMotionChange }) {
const [activeCategory, setActiveCategory] = useState('appearance')
@@ -56,6 +57,10 @@ function Settings({ isOpen, onClose, theme, onThemeChange, reducedMotion, onRedu
<span className="settings-nav-icon"><SparkIcon size={18} /></span>
<span>智能配置</span>
</button>
<button className={`settings-nav-item ${activeCategory === 'system-update' ? 'active' : ''}`} onClick={() => setActiveCategory('system-update')}>
<span className="settings-nav-icon"><RefreshIcon size={18} /></span>
<span>系统与更新</span>
</button>
</nav>
<div className="settings-sidebar-status"><TuneIcon size={15} /><span>专注业务,自在掌控<small>TRADE MANAGEMENT</small></span></div>
</aside>
@@ -64,6 +69,7 @@ function Settings({ isOpen, onClose, theme, onThemeChange, reducedMotion, onRedu
{activeCategory === 'appearance' && <AppearanceSettings theme={theme} onThemeChange={onThemeChange} reducedMotion={reducedMotion} onReducedMotionChange={onReducedMotionChange} />}
{activeCategory === 'import-export' && <ImportExportSettings />}
{activeCategory === 'ai-config' && <AIConfigSettings />}
{activeCategory === 'system-update' && <SystemUpdateSettings />}
</section>
</div>
</div>
@@ -127,4 +133,166 @@ function AIConfigSettings() {
)
}
/* ---------------------------------------------------------------------------
系统与更新
显示本机版本与运行环境,并从 GitHub 仓库检查是否有新版本。
版本号以顶层 package.json 为准;远端取 Release(优先)或 tag。
--------------------------------------------------------------------------- */
function SystemUpdateSettings() {
const [info, setInfo] = useState(null)
const [repo, setRepo] = useState('')
const [loadingInfo, setLoadingInfo] = useState(true)
const [checking, setChecking] = useState(false)
const [result, setResult] = useState(null)
const [error, setError] = useState('')
const [showNotes, setShowNotes] = useState(false)
useEffect(() => {
let alive = true
axios
.get('/api/system/info')
.then(({ data }) => {
if (!alive) return
setInfo(data)
setRepo(data.repo || '')
})
.catch((e) => {
if (alive) setError(e?.response?.data?.error || e.message)
})
.finally(() => {
if (alive) setLoadingInfo(false)
})
return () => {
alive = false
}
}, [])
const check = async () => {
setChecking(true)
setError('')
setResult(null)
setShowNotes(false)
try {
const { data } = await axios.get('/api/system/check-update', { params: { repo, force: 1 } })
setResult(data)
if (!data.ok) setError(data.error || '检查更新失败')
} catch (e) {
setError(e?.response?.data?.error || e.message)
} finally {
setChecking(false)
}
}
const fmtTime = (value) => {
if (!value) return ''
try {
return new Date(value).toLocaleString('zh-CN', { hour12: false })
} catch {
return String(value)
}
}
return (
<div className="settings-section">
<span className="section-label">[SYSTEM]</span>
<h3>系统与更新</h3>
<p className="settings-description">查看当前版本与运行环境,并从官方仓库检查是否有新版本。</p>
<div className="settings-group">
<h4>当前版本</h4>
<div className="setting-info">
<span>
<strong>{info ? `happy 出口通 v${info.version}` : '正在读取版本…'}</strong>
<small>{info ? `运行环境 Node ${info.node} · ${info.platform} ${info.arch}` : '读取本机信息中'}</small>
</span>
<span className="preference-badge">正式版</span>
</div>
{info?.startedAt && <p className="help-text">服务启动于 {fmtTime(info.startedAt)}</p>}
</div>
<div className="settings-group">
<h4>更新源</h4>
<div className="form-group">
<label>GitHub 仓库</label>
<input
type="text"
className="form-input"
value={repo}
placeholder="owner/repo"
onChange={(e) => setRepo(e.target.value)}
disabled={checking}
/>
</div>
<p className="help-text">默认抓取官方仓库,也支持填完整地址(https://github.com/owner/repo)。</p>
</div>
<div className="settings-group">
<h4>检查更新</h4>
<div className="settings-actions">
<button className="btn btn-primary" onClick={check} disabled={checking || loadingInfo}>
{checking ? '检查中…' : '检查更新'}
</button>
</div>
{!checking && result && (
<p className="help-text">
上次检查:{fmtTime(result.checkedAt)}
{result.cached ? '(结果来自缓存)' : ''}
</p>
)}
{checking && (
<div className="update-status is-checking">
<span className="spinner" />
<span>正在连接 GitHub,请稍候…</span>
</div>
)}
{!checking && error && (
<div className="update-status is-error">
<CloseIcon size={15} />
<span>{error}</span>
</div>
)}
{!checking && !error && result?.ok && result.hasUpdate && (
<>
<div className="update-status is-new">
<SparkIcon size={15} />
<span>
发现新版本 <b>v{result.latest}</b>,当前 v{result.current}
</span>
</div>
{result.publishedAt && <p className="help-text">发布于 {fmtTime(result.publishedAt)}</p>}
{(result.url || result.notes) && (
<div className="update-links">
{result.url && (
<a className="btn-link" href={result.url} target="_blank" rel="noreferrer">
在 GitHub 上查看
</a>
)}
{result.notes && (
<button className="btn-link" onClick={() => setShowNotes((v) => !v)}>
{showNotes ? '收起更新说明' : '查看更新说明'}
</button>
)}
</div>
)}
{showNotes && result.notes && <pre className="update-notes">{result.notes}</pre>}
</>
)}
{!checking && !error && result?.ok && !result.hasUpdate && (
<>
<div className="update-status is-ok">
<CheckIcon size={15} />
<span>已是最新版本(v{result.current})</span>
</div>
{result.note && <p className="help-text">{result.note}</p>}
</>
)}
</div>
</div>
)
}
export default Settings