Skip to content

Commit 87daccd

Browse files
author
catlog22
committed
feat: 添加版本检查功能及 Header 布局修复
- SettingsPage 新增 VersionCheckSection(自动/手动检查更新) - 添加版本检查相关中英文 i18n 翻译 - 修复 Header 历史链接的 flex 布局对齐
1 parent f27e52a commit 87daccd

4 files changed

Lines changed: 212 additions & 2 deletions

File tree

ccw/frontend/src/components/layout/Header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export function Header({
8484
asChild
8585
className="gap-2"
8686
>
87-
<Link to="/history">
87+
<Link to="/history" className="inline-flex items-center gap-2">
8888
<Clock className="h-4 w-4" />
8989
<span className="hidden sm:inline">{formatMessage({ id: 'navigation.main.history' })}</span>
9090
</Link>

ccw/frontend/src/locales/en/settings.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,22 @@
114114
"on": "On",
115115
"off": "Off"
116116
},
117+
"versionCheck": {
118+
"title": "Version Update",
119+
"currentVersion": "Current Version",
120+
"latestVersion": "Latest Version",
121+
"checkNow": "Check Now",
122+
"checking": "Checking...",
123+
"autoCheck": "Auto-check for updates",
124+
"autoCheckDesc": "Automatically check for new versions every hour",
125+
"upToDate": "Up to date",
126+
"updateAvailable": "Update available",
127+
"updateCommand": "Update command",
128+
"viewRelease": "View Release",
129+
"lastChecked": "Last checked",
130+
"checkFailed": "Check failed",
131+
"never": "Never"
132+
},
117133
"about": {
118134
"title": "About",
119135
"version": "Version",

ccw/frontend/src/locales/zh/settings.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,22 @@
114114
"on": "开启",
115115
"off": "关闭"
116116
},
117+
"versionCheck": {
118+
"title": "版本更新",
119+
"currentVersion": "当前版本",
120+
"latestVersion": "最新版本",
121+
"checkNow": "立即检查",
122+
"checking": "检查中...",
123+
"autoCheck": "自动检查更新",
124+
"autoCheckDesc": "每小时自动检查是否有新版本",
125+
"upToDate": "已是最新版本",
126+
"updateAvailable": "有新版本可用",
127+
"updateCommand": "更新命令",
128+
"viewRelease": "查看更新",
129+
"lastChecked": "上次检查",
130+
"checkFailed": "检查失败",
131+
"never": "从未"
132+
},
117133
"about": {
118134
"title": "关于",
119135
"version": "版本",

ccw/frontend/src/pages/SettingsPage.tsx

Lines changed: 179 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// ========================================
44
// Application settings and configuration with CLI tools management
55

6-
import { useState, useCallback } from 'react';
6+
import { useState, useCallback, useEffect } from 'react';
77
import { useIntl } from 'react-intl';
88
import {
99
Settings,
@@ -727,6 +727,181 @@ function ResponseLanguageSection() {
727727
);
728728
}
729729

730+
// ========== Version Check Section ==========
731+
732+
interface VersionData {
733+
currentVersion: string;
734+
latestVersion: string;
735+
hasUpdate: boolean;
736+
packageName: string;
737+
updateCommand: string;
738+
checkedAt: string;
739+
}
740+
741+
function VersionCheckSection() {
742+
const { formatMessage } = useIntl();
743+
const [versionData, setVersionData] = useState<VersionData | null>(null);
744+
const [checking, setChecking] = useState(false);
745+
const [error, setError] = useState<string | null>(null);
746+
const [lastChecked, setLastChecked] = useState<Date | null>(null);
747+
const [autoCheck, setAutoCheck] = useState(() => {
748+
try {
749+
const saved = localStorage.getItem('ccw.autoUpdate');
750+
return saved === null ? true : JSON.parse(saved);
751+
} catch {
752+
return true;
753+
}
754+
});
755+
756+
const checkVersion = async (silent = false) => {
757+
if (!silent) setChecking(true);
758+
setError(null);
759+
try {
760+
const response = await fetch('/api/version-check');
761+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
762+
763+
const data: VersionData = await response.json();
764+
if (!data.currentVersion) throw new Error('Invalid response');
765+
766+
setVersionData(data);
767+
setLastChecked(new Date());
768+
} catch (err) {
769+
if (!silent) setError(err instanceof Error ? err.message : 'Unknown error');
770+
} finally {
771+
setChecking(false);
772+
}
773+
};
774+
775+
useEffect(() => {
776+
// Initial check
777+
checkVersion(true);
778+
779+
if (!autoCheck) return;
780+
const interval = setInterval(() => checkVersion(true), 60 * 60 * 1000);
781+
return () => clearInterval(interval);
782+
}, [autoCheck]);
783+
784+
const toggleAutoCheck = (enabled: boolean) => {
785+
setAutoCheck(enabled);
786+
localStorage.setItem('ccw.autoUpdate', JSON.stringify(enabled));
787+
};
788+
789+
return (
790+
<Card className="p-6">
791+
<div className="flex items-center justify-between mb-4">
792+
<h2 className="text-lg font-semibold text-foreground flex items-center gap-2">
793+
<ArrowUpCircle className="w-5 h-5" />
794+
{formatMessage({ id: 'settings.versionCheck.title' })}
795+
</h2>
796+
<Button
797+
variant="outline"
798+
size="sm"
799+
disabled={checking}
800+
onClick={() => checkVersion()}
801+
>
802+
<RefreshCw className={cn('w-3.5 h-3.5 mr-1.5', checking && 'animate-spin')} />
803+
{checking
804+
? formatMessage({ id: 'settings.versionCheck.checking' })
805+
: formatMessage({ id: 'settings.versionCheck.checkNow' })}
806+
</Button>
807+
</div>
808+
809+
<div className="space-y-4">
810+
{/* Version info */}
811+
<div className="rounded-lg border border-border p-4 space-y-3">
812+
<div className="flex items-center justify-between">
813+
<span className="text-sm text-muted-foreground">
814+
{formatMessage({ id: 'settings.versionCheck.currentVersion' })}
815+
</span>
816+
<Badge variant="secondary" className="font-mono text-xs">
817+
{versionData?.currentVersion ?? '...'}
818+
</Badge>
819+
</div>
820+
<div className="flex items-center justify-between">
821+
<span className="text-sm text-muted-foreground">
822+
{formatMessage({ id: 'settings.versionCheck.latestVersion' })}
823+
</span>
824+
<Badge
825+
variant={versionData?.updateAvailable ? 'default' : 'secondary'}
826+
className="font-mono text-xs"
827+
>
828+
{versionData?.latestVersion ?? '...'}
829+
</Badge>
830+
</div>
831+
832+
{/* Status */}
833+
{versionData && (
834+
<div className="flex items-center justify-between pt-2 border-t border-border">
835+
<span className="text-sm font-medium">
836+
{versionData.hasUpdate
837+
? formatMessage({ id: 'settings.versionCheck.updateAvailable' })
838+
: formatMessage({ id: 'settings.versionCheck.upToDate' })}
839+
</span>
840+
<span className={cn(
841+
'inline-block w-2.5 h-2.5 rounded-full',
842+
versionData.hasUpdate ? 'bg-orange-500' : 'bg-green-500'
843+
)} />
844+
</div>
845+
)}
846+
847+
{error && (
848+
<div className="flex items-center gap-2 pt-2 border-t border-border">
849+
<AlertTriangle className="w-4 h-4 text-destructive flex-shrink-0" />
850+
<span className="text-sm text-destructive">
851+
{formatMessage({ id: 'settings.versionCheck.checkFailed' })}: {error}
852+
</span>
853+
</div>
854+
)}
855+
</div>
856+
857+
{/* Update action */}
858+
{versionData?.hasUpdate && (
859+
<div className="rounded-lg border border-orange-500/30 bg-orange-500/5 p-4 space-y-3">
860+
<div>
861+
<p className="text-sm font-medium text-foreground mb-1">
862+
{formatMessage({ id: 'settings.versionCheck.updateCommand' })}
863+
</p>
864+
<code className="text-xs font-mono bg-muted px-3 py-1.5 rounded block">
865+
{versionData.updateCommand}
866+
</code>
867+
</div>
868+
<Button variant="outline" size="sm" asChild>
869+
<a
870+
href="https://github.com/dyw0830/ccw/releases"
871+
target="_blank"
872+
rel="noopener noreferrer"
873+
className="inline-flex items-center gap-1.5"
874+
>
875+
{formatMessage({ id: 'settings.versionCheck.viewRelease' })}
876+
</a>
877+
</Button>
878+
</div>
879+
)}
880+
881+
{/* Auto check toggle + last checked */}
882+
<div className="flex items-center justify-between">
883+
<label className="flex items-center gap-2 cursor-pointer">
884+
<input
885+
type="checkbox"
886+
checked={autoCheck}
887+
onChange={(e) => toggleAutoCheck(e.target.checked)}
888+
className="rounded border-input"
889+
/>
890+
<div>
891+
<span className="text-sm font-medium">{formatMessage({ id: 'settings.versionCheck.autoCheck' })}</span>
892+
<p className="text-xs text-muted-foreground">{formatMessage({ id: 'settings.versionCheck.autoCheckDesc' })}</p>
893+
</div>
894+
</label>
895+
<span className="text-xs text-muted-foreground">
896+
{formatMessage({ id: 'settings.versionCheck.lastChecked' })}:{' '}
897+
{lastChecked ? lastChecked.toLocaleTimeString() : formatMessage({ id: 'settings.versionCheck.never' })}
898+
</span>
899+
</div>
900+
</div>
901+
</Card>
902+
);
903+
}
904+
730905
// ========== System Status Section ==========
731906

732907
function SystemStatusSection() {
@@ -1118,6 +1293,9 @@ export function SettingsPage() {
11181293
{/* System Status */}
11191294
<SystemStatusSection />
11201295

1296+
{/* Version Check */}
1297+
<VersionCheckSection />
1298+
11211299
{/* CLI Tools Configuration */}
11221300
<Card className="p-6">
11231301
<h2 className="text-lg font-semibold text-foreground flex items-center gap-2 mb-4">

0 commit comments

Comments
 (0)