Skip to content

Commit e2f46f6

Browse files
committed
feat: redesign settings release panel
1 parent 806130f commit e2f46f6

9 files changed

Lines changed: 190 additions & 20 deletions

File tree

docs-linhay/memory/2026-05-25.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# 2026-05-25
2+
3+
- Settings Release Panel 重设计:`frontend/src/features/settings/components/SettingsReleasePanel.tsx` 改为信息矩阵 + 右侧操作区,并为当前版本、最新 release、GetTokens Git Hash、CLIProxyAPI Git Hash 增加 GitHub 外链入口。URL 构造逻辑收敛到 `settingsRelease.ts`,占位 hash(`DEV```)不显示外链;浏览器预览下外链走 `window.open`,Wails 桌面下走 `BrowserOpenURL`。验证:`npm run test:unit``npm run typecheck``npm run build` 通过;Playwright 预览截图落位 `docs-linhay/screenshots/20260525/settings/20260525-settings-release-panel-after-v02.png`。375px 预览暴露的是既有固定侧边栏桌面壳宽度问题,不作为本次 release panel 阻塞。
71.1 KB
Loading

frontend/src/features/settings/SettingsFeature.tsx

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@ import { useI18n } from '../../context/I18nContext';
1919
import { useTextScale } from '../../context/TextScaleContext';
2020
import { useTheme } from '../../context/ThemeContext';
2121
import { buildGitHashLabel, formatBuildGitHash } from './settingsBuildMetadata';
22-
import { mapCheckedRelease } from './settingsRelease';
22+
import {
23+
buildGetTokensReleaseURL,
24+
buildGitHubCommitURL,
25+
cliProxyApiGitHubRepositoryURL,
26+
getTokensGitHubRepositoryURL,
27+
mapCheckedRelease,
28+
} from './settingsRelease';
2329
import {
2430
localProjectedUsageRefreshIntervalOptions,
2531
parseLocalProjectedUsageRefreshIntervalMinutes,
@@ -31,7 +37,7 @@ import {
3137
} from './settingsTextScale';
3238
import { getSettingsSectionBadge, type SettingsSectionID } from './settingsLayout';
3339
import { toErrorMessage } from '../../utils/error';
34-
import { hasWailsAppBindings } from '../../utils/previewMode';
40+
import { hasWailsAppBindings, hasWailsRuntime } from '../../utils/previewMode';
3541
import { formatAppVersion } from '../../utils/version';
3642
import type { LocaleCode, ReleaseInfo, SegmentedOption, SidecarStatus, ThemeMode } from '../../types';
3743

@@ -108,6 +114,10 @@ export default function SettingsFeature({
108114
const currentVersionLabel = formatAppVersion(version);
109115
const latestReleaseLabel = availableRelease ? formatAppVersion(availableRelease.version) : '—';
110116
const cliProxyApiGitHashLabel = formatBuildGitHash(sidecarStatus.gitHash);
117+
const currentReleaseGitHubURL = buildGetTokensReleaseURL(currentVersionLabel);
118+
const latestReleaseGitHubURL = availableRelease?.releaseUrl ?? '';
119+
const gitHashGitHubURL = buildGitHubCommitURL(getTokensGitHubRepositoryURL, buildGitHashLabel);
120+
const cliProxyApiGitHashGitHubURL = buildGitHubCommitURL(cliProxyApiGitHubRepositoryURL, cliProxyApiGitHashLabel);
111121
const textScaleOptions: ReadonlyArray<SegmentedOption<typeof textScale>> = [
112122
{ id: 'default', label: t('settings.text_scale_default') },
113123
{ id: 'large', label: t('settings.text_scale_large') },
@@ -278,7 +288,7 @@ export default function SettingsFeature({
278288
setIsOpeningRelease(true);
279289
setUpdateMessage('');
280290
try {
281-
BrowserOpenURL(availableRelease.releaseUrl);
291+
openExternalURL(availableRelease.releaseUrl);
282292
setUpdateMessage(t('settings.update_redirected'));
283293
} catch (error) {
284294
setUpdateMessage(`${t('settings.update_error')}: ${toErrorMessage(error)}`);
@@ -288,6 +298,25 @@ export default function SettingsFeature({
288298
setIsOpeningRelease(false);
289299
}
290300

301+
function handleOpenGitHubURL(url: string) {
302+
openExternalURL(url);
303+
}
304+
305+
function openExternalURL(url: string) {
306+
if (!url) {
307+
return;
308+
}
309+
310+
if (hasWailsRuntime()) {
311+
BrowserOpenURL(url);
312+
return;
313+
}
314+
315+
if (typeof window !== 'undefined') {
316+
window.open(url, '_blank', 'noopener,noreferrer');
317+
}
318+
}
319+
291320
async function handleLocalUsageIntervalChange(value: LocalProjectedUsageRefreshIntervalID) {
292321
setLocalUsageInterval(value);
293322
setIsSavingLocalUsageSettings(true);
@@ -709,6 +738,7 @@ export default function SettingsFeature({
709738
cliProxyApiGitHashLabel={cliProxyApiGitHashLabel}
710739
latestReleaseTitle={t('settings.latest_release')}
711740
latestReleaseLabel={latestReleaseLabel}
741+
latestReleaseGitHubURL={latestReleaseGitHubURL}
712742
updateAssetTitle={t('settings.update_asset')}
713743
updateAssetName={availableRelease?.assetName || '—'}
714744
updateChannelTitle={t('settings.update_channel')}
@@ -719,6 +749,11 @@ export default function SettingsFeature({
719749
? 'settings.update_channel_hint_auto'
720750
: 'settings.update_channel_hint_manual'
721751
)}
752+
currentReleaseGitHubURL={currentReleaseGitHubURL}
753+
gitHashGitHubURL={gitHashGitHubURL}
754+
cliProxyApiGitHashGitHubURL={cliProxyApiGitHashGitHubURL}
755+
openGitHubLabel={t('settings.open_github')}
756+
onOpenGitHubURL={handleOpenGitHubURL}
722757
updateMessage={updateMessage}
723758
checkUpdateLabel={t('settings.check_update')}
724759
checkingUpdateLabel={t('settings.checking_update')}

frontend/src/features/settings/components/SettingsReleasePanel.stories.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,16 @@ const baseProps = {
2626
cliProxyApiGitHashLabel: '7f1c2d9',
2727
latestReleaseTitle: '最新版本',
2828
latestReleaseLabel: '0.2.2',
29+
latestReleaseGitHubURL: 'https://github.com/AxApp/GetTokens/releases/tag/v0.2.2',
2930
updateAssetTitle: '更新包',
3031
updateAssetName: 'GetTokens_macOS_AppleSilicon.dmg',
3132
updateChannelTitle: '更新通道',
3233
updateChannelHint: '自动下载并应用当前架构的 macOS 更新包。',
34+
currentReleaseGitHubURL: 'https://github.com/AxApp/GetTokens/releases/tag/v0.2.1',
35+
gitHashGitHubURL: 'https://github.com/AxApp/GetTokens/commit/960ebd9fd83f',
36+
cliProxyApiGitHashGitHubURL: 'https://github.com/AxApp/CLIProxyAPI/commit/7f1c2d9',
37+
openGitHubLabel: '打开 GitHub',
38+
onOpenGitHubURL: () => undefined,
3339
updateMessage: '',
3440
checkUpdateLabel: '检查更新',
3541
checkingUpdateLabel: '检查中',

frontend/src/features/settings/components/SettingsReleasePanel.tsx

Lines changed: 80 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ReactNode } from 'react';
2+
import { ExternalLink } from 'lucide-react';
23

34
const fieldMetaStyle = { fontSize: 'var(--gt-settings-meta-size, 8px)' } as const;
45
const bodyTextStyle = { fontSize: 'var(--gt-settings-body-size, 9px)' } as const;
@@ -15,10 +16,16 @@ interface SettingsReleasePanelProps {
1516
cliProxyApiGitHashLabel: string;
1617
latestReleaseTitle: string;
1718
latestReleaseLabel: string;
19+
latestReleaseGitHubURL: string;
1820
updateAssetTitle: string;
1921
updateAssetName: string;
2022
updateChannelTitle: string;
2123
updateChannelHint: ReactNode;
24+
currentReleaseGitHubURL: string;
25+
gitHashGitHubURL: string;
26+
cliProxyApiGitHashGitHubURL: string;
27+
openGitHubLabel: string;
28+
onOpenGitHubURL: (url: string) => void;
2229
updateMessage: string;
2330
checkUpdateLabel: string;
2431
checkingUpdateLabel: string;
@@ -42,10 +49,16 @@ export default function SettingsReleasePanel({
4249
cliProxyApiGitHashLabel,
4350
latestReleaseTitle,
4451
latestReleaseLabel,
52+
latestReleaseGitHubURL,
4553
updateAssetTitle,
4654
updateAssetName,
4755
updateChannelTitle,
4856
updateChannelHint,
57+
currentReleaseGitHubURL,
58+
gitHashGitHubURL,
59+
cliProxyApiGitHashGitHubURL,
60+
openGitHubLabel,
61+
onOpenGitHubURL,
4962
updateMessage,
5063
checkUpdateLabel,
5164
checkingUpdateLabel,
@@ -59,26 +72,54 @@ export default function SettingsReleasePanel({
5972
}: SettingsReleasePanelProps) {
6073
return (
6174
<div
62-
className="card-swiss bg-[var(--bg-surface)] !p-5"
75+
className="card-swiss overflow-hidden bg-[var(--bg-surface)] !p-0"
6376
data-design-system-component="true"
6477
data-design-system-component-name="SettingsReleasePanel"
6578
data-design-system-git-hash={gitHashLabel}
6679
>
67-
<div className="grid gap-5 md:grid-cols-[1.1fr_0.9fr]">
68-
<div className="space-y-4">
69-
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
70-
<SettingsReleaseValue title={currentVersionTitle} value={currentVersionLabel} />
80+
<div className="grid md:grid-cols-[minmax(0,1fr)_17rem]">
81+
<div className="min-w-0">
82+
<div className="grid grid-cols-1 border-b border-dashed border-[var(--border-color)] sm:grid-cols-2 lg:grid-cols-4">
83+
<SettingsReleaseValue
84+
title={currentVersionTitle}
85+
value={currentVersionLabel}
86+
actionURL={currentReleaseGitHubURL}
87+
actionLabel={openGitHubLabel}
88+
onOpenURL={onOpenGitHubURL}
89+
strong
90+
/>
7191
<SettingsReleaseValue title={releaseLabelTitle} value={releaseLabel} />
72-
<SettingsReleaseValue title={gitHashTitle} value={gitHashLabel} mono />
73-
<SettingsReleaseValue title={cliProxyApiGitHashTitle} value={cliProxyApiGitHashLabel} mono />
92+
<SettingsReleaseValue
93+
title={gitHashTitle}
94+
value={gitHashLabel}
95+
mono
96+
actionURL={gitHashGitHubURL}
97+
actionLabel={openGitHubLabel}
98+
onOpenURL={onOpenGitHubURL}
99+
/>
100+
<SettingsReleaseValue
101+
title={cliProxyApiGitHashTitle}
102+
value={cliProxyApiGitHashLabel}
103+
mono
104+
actionURL={cliProxyApiGitHashGitHubURL}
105+
actionLabel={openGitHubLabel}
106+
onOpenURL={onOpenGitHubURL}
107+
/>
74108
</div>
75109

76-
<div className="grid grid-cols-1 gap-4 border-t border-dashed border-[var(--border-color)] pt-4 sm:grid-cols-2">
77-
<SettingsReleaseValue title={latestReleaseTitle} value={latestReleaseLabel} />
110+
<div className="grid grid-cols-1 border-b border-dashed border-[var(--border-color)] sm:grid-cols-2">
111+
<SettingsReleaseValue
112+
title={latestReleaseTitle}
113+
value={latestReleaseLabel}
114+
actionURL={latestReleaseGitHubURL}
115+
actionLabel={openGitHubLabel}
116+
onOpenURL={onOpenGitHubURL}
117+
strong
118+
/>
78119
<SettingsReleaseValue title={updateAssetTitle} value={updateAssetName} mono body />
79120
</div>
80121

81-
<div className="border-t border-dashed border-[var(--border-color)] pt-4">
122+
<div className="px-4 py-4">
82123
<div className="font-bold uppercase tracking-widest text-[var(--text-muted)]" style={fieldMetaStyle}>
83124
{updateChannelTitle}
84125
</div>
@@ -96,7 +137,7 @@ export default function SettingsReleasePanel({
96137
</div>
97138
</div>
98139

99-
<div className="space-y-3 border-t border-dashed border-[var(--border-color)] pt-4 md:border-l md:border-t-0 md:pl-5 md:pt-0">
140+
<div className="grid content-start gap-3 border-t border-dashed border-[var(--border-color)] bg-[var(--bg-main)] p-4 md:border-l md:border-t-0">
100141
<button className="btn-swiss w-full" onClick={onCheckUpdate} disabled={isCheckingUpdate}>
101142
{isCheckingUpdate ? checkingUpdateLabel : checkUpdateLabel}
102143
</button>
@@ -119,22 +160,45 @@ function SettingsReleaseValue({
119160
value,
120161
mono = false,
121162
body = false,
163+
strong = false,
164+
actionURL = '',
165+
actionLabel = '',
166+
onOpenURL,
122167
}: {
123168
title: string;
124169
value: string;
125170
mono?: boolean;
126171
body?: boolean;
172+
strong?: boolean;
173+
actionURL?: string;
174+
actionLabel?: string;
175+
onOpenURL?: (url: string) => void;
127176
}) {
128177
const valueClassName = mono
129178
? 'break-all font-mono font-bold text-[var(--text-primary)]'
130-
: 'font-black uppercase italic text-[var(--text-primary)]';
179+
: strong
180+
? 'font-black uppercase italic text-[var(--text-primary)]'
181+
: 'font-bold uppercase text-[var(--text-primary)]';
131182

132183
return (
133-
<div className="min-w-0 space-y-1">
134-
<div className="font-bold uppercase tracking-widest text-[var(--text-muted)]" style={fieldMetaStyle}>
135-
{title}
184+
<div className="min-w-0 border-b border-dashed border-[var(--border-color)] px-4 py-3 last:border-b-0 sm:border-b-0 sm:border-r sm:last:border-r-0 lg:border-r lg:[&:nth-child(4n)]:border-r-0">
185+
<div className="flex min-h-6 items-center justify-between gap-2">
186+
<div className="font-bold uppercase tracking-widest text-[var(--text-muted)]" style={fieldMetaStyle}>
187+
{title}
188+
</div>
189+
{actionURL && onOpenURL ? (
190+
<button
191+
type="button"
192+
className="inline-flex h-6 w-6 shrink-0 items-center justify-center border border-[var(--border-color)] bg-[var(--bg-main)] text-[var(--text-primary)] transition active:scale-95 hover:bg-[var(--bg-surface)]"
193+
aria-label={`${actionLabel}: ${value}`}
194+
title={actionLabel}
195+
onClick={() => onOpenURL(actionURL)}
196+
>
197+
<ExternalLink className="h-3.5 w-3.5" aria-hidden="true" />
198+
</button>
199+
) : null}
136200
</div>
137-
<div className={valueClassName} style={body ? bodyTextStyle : valueTextStyle}>
201+
<div className={`mt-1 ${valueClassName}`} style={body ? bodyTextStyle : valueTextStyle}>
138202
{value}
139203
</div>
140204
</div>

frontend/src/features/settings/settingsRelease.test.mjs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import test from 'node:test';
22
import assert from 'node:assert/strict';
33

4-
import { mapCheckedRelease } from './settingsRelease.ts';
4+
import {
5+
buildGitHubCommitURL,
6+
buildGetTokensReleaseURL,
7+
mapCheckedRelease,
8+
} from './settingsRelease.ts';
59

610
test('mapCheckedRelease keeps release page url for manual update flow', () => {
711
const release = mapCheckedRelease({
@@ -23,3 +27,32 @@ test('mapCheckedRelease returns null when updater has no newer release', () => {
2327
assert.equal(mapCheckedRelease(null), null);
2428
assert.equal(mapCheckedRelease(undefined), null);
2529
});
30+
31+
test('buildGitHubCommitURL links valid hashes to the selected repository', () => {
32+
assert.equal(
33+
buildGitHubCommitURL('https://github.com/AxApp/GetTokens', '960ebd9fd83f'),
34+
'https://github.com/AxApp/GetTokens/commit/960ebd9fd83f',
35+
);
36+
assert.equal(
37+
buildGitHubCommitURL('https://github.com/AxApp/CLIProxyAPI', ' e5bcdfb6 '),
38+
'https://github.com/AxApp/CLIProxyAPI/commit/e5bcdfb6',
39+
);
40+
});
41+
42+
test('buildGitHubCommitURL ignores placeholder build hashes', () => {
43+
assert.equal(buildGitHubCommitURL('https://github.com/AxApp/GetTokens', 'DEV'), '');
44+
assert.equal(buildGitHubCommitURL('https://github.com/AxApp/GetTokens', '—'), '');
45+
assert.equal(buildGitHubCommitURL('https://github.com/AxApp/GetTokens', ''), '');
46+
});
47+
48+
test('buildGetTokensReleaseURL prefers checked release url and falls back to current version tag', () => {
49+
assert.equal(
50+
buildGetTokensReleaseURL('1.0.23', 'https://github.com/AxApp/GetTokens/releases/tag/v1.0.24'),
51+
'https://github.com/AxApp/GetTokens/releases/tag/v1.0.24',
52+
);
53+
assert.equal(
54+
buildGetTokensReleaseURL('1.0.23'),
55+
'https://github.com/AxApp/GetTokens/releases/tag/v1.0.23',
56+
);
57+
assert.equal(buildGetTokensReleaseURL('DEV'), '');
58+
});

frontend/src/features/settings/settingsRelease.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import type { ReleaseInfo } from '../../types';
22

3+
export const getTokensGitHubRepositoryURL = 'https://github.com/AxApp/GetTokens';
4+
export const cliProxyApiGitHubRepositoryURL = 'https://github.com/AxApp/CLIProxyAPI';
5+
36
export function mapCheckedRelease(result: ReleaseInfo | null | undefined): ReleaseInfo | null {
47
if (!result) {
58
return null;
@@ -12,3 +15,27 @@ export function mapCheckedRelease(result: ReleaseInfo | null | undefined): Relea
1215
releaseNote: result.releaseNote,
1316
};
1417
}
18+
19+
export function buildGitHubCommitURL(repositoryURL: string, gitHash: string): string {
20+
const normalizedHash = gitHash.trim();
21+
if (!normalizedHash || normalizedHash === 'DEV' || normalizedHash === '—') {
22+
return '';
23+
}
24+
25+
return `${repositoryURL.replace(/\/$/, '')}/commit/${encodeURIComponent(normalizedHash)}`;
26+
}
27+
28+
export function buildGetTokensReleaseURL(currentVersion: string, checkedReleaseURL = ''): string {
29+
const normalizedCheckedReleaseURL = checkedReleaseURL.trim();
30+
if (normalizedCheckedReleaseURL) {
31+
return normalizedCheckedReleaseURL;
32+
}
33+
34+
const normalizedVersion = currentVersion.trim();
35+
if (!normalizedVersion || normalizedVersion === 'DEV' || normalizedVersion === '—') {
36+
return '';
37+
}
38+
39+
const tag = normalizedVersion.startsWith('v') ? normalizedVersion : `v${normalizedVersion}`;
40+
return `${getTokensGitHubRepositoryURL}/releases/tag/${encodeURIComponent(tag)}`;
41+
}

frontend/src/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1333,6 +1333,7 @@
13331333
"apply_update": "Apply Update & Quit",
13341334
"applying_update": "Applying...",
13351335
"apply_update_hint": "The app will quit after a successful update. Relaunch to enter the new version.",
1336+
"open_github": "Open GitHub",
13361337
"open_release_page": "Open Release Page",
13371338
"opening_release_page": "Opening...",
13381339
"manual_update_hint": "Signed macOS releases avoid in-place bundle patching; install the update from the release page instead.",

frontend/src/locales/zh.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1333,6 +1333,7 @@
13331333
"apply_update": "应用更新并退出",
13341334
"applying_update": "升级中...",
13351335
"apply_update_hint": "应用成功后程序会退出;重新启动后进入新版本。",
1336+
"open_github": "打开 GitHub",
13361337
"open_release_page": "打开发布页安装",
13371338
"opening_release_page": "打开中...",
13381339
"manual_update_hint": "macOS 已签名发布包不做 bundle 内原地替换,改为跳转发布页安装。",

0 commit comments

Comments
 (0)