feat(updater): 优化关于页更新信息展示

- 关于页直接进入 GoNavi 内容详情,减少二次点击
- 将更新通道改为分段选择器并优化描述布局
- 保留后端发布发布时间元数据并在版本信息中展示
This commit is contained in:
Syngnat
2026-07-08 20:30:05 +08:00
parent 22fab86010
commit dca1e8a251
9 changed files with 238 additions and 78 deletions

View File

@@ -743,3 +743,63 @@ body[data-ui-version="legacy"] .ant-layout-sider {
min-width: 232px !important;
max-width: min(960px, calc(100vw - 360px)) !important;
}
body .gonavi-about-update-channel.ant-segmented.ant-segmented {
height: 42px !important;
padding: 4px !important;
border: 1px solid rgba(15, 23, 42, 0.10) !important;
border-radius: 12px !important;
background: rgba(15, 23, 42, 0.035) !important;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.72) !important;
}
body .gonavi-about-update-channel .ant-segmented-group {
gap: 3px;
}
body .gonavi-about-update-channel .ant-segmented-item {
border-radius: 9px !important;
}
body .gonavi-about-update-channel .ant-segmented-item-label {
min-height: 32px !important;
padding: 0 14px !important;
color: rgba(15, 23, 42, 0.62) !important;
font-size: 14px !important;
font-weight: 700 !important;
line-height: 32px !important;
}
body .gonavi-about-update-channel .ant-segmented-item-selected,
body .gonavi-about-update-channel .ant-segmented-thumb {
background: rgba(255, 255, 255, 0.98) !important;
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.12) !important;
}
body .gonavi-about-update-channel .ant-segmented-item-selected .ant-segmented-item-label {
color: #0f172a !important;
}
body .gonavi-about-update-channel.ant-segmented-disabled {
opacity: 0.58;
}
body[data-theme='dark'] .gonavi-about-update-channel.ant-segmented.ant-segmented {
border-color: rgba(148, 163, 184, 0.22) !important;
background: rgba(15, 23, 42, 0.55) !important;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08) !important;
}
body[data-theme='dark'] .gonavi-about-update-channel .ant-segmented-item-label {
color: rgba(226, 232, 240, 0.62) !important;
}
body[data-theme='dark'] .gonavi-about-update-channel .ant-segmented-item-selected,
body[data-theme='dark'] .gonavi-about-update-channel .ant-segmented-thumb {
background: rgba(30, 41, 59, 0.98) !important;
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.26) !important;
}
body[data-theme='dark'] .gonavi-about-update-channel .ant-segmented-item-selected .ant-segmented-item-label {
color: #f8fafc !important;
}

View File

@@ -87,6 +87,13 @@ describe('settings center layout', () => {
expect(appSource).toContain('renderSettingsCenterAboutPane()');
});
it('opens the about group directly instead of showing a one-item list', () => {
expect(appSource).toContain('const resolveSettingsCenterGroupInitialPane = (group: SettingsCenterGroupKey): SettingsCenterPaneState | null => (');
expect(appSource).toContain("group === 'about' ? { key: 'about-go-navi', group: 'about' } : null");
expect(appSource).toContain('setActiveSettingsCenterPane(resolveSettingsCenterGroupInitialPane(group));');
expect(appSource).toContain('setActiveSettingsCenterPane(resolveSettingsCenterGroupInitialPane(group.key));');
});
it('renders the settings center about page with the reference card layout', () => {
expect(appSource).toContain('const renderSettingsCenterAboutPane = () => {');
expect(appSource).toContain('const renderSettingsCenterAboutProjectEntry = ({');
@@ -94,6 +101,14 @@ describe('settings center layout', () => {
expect(appSource).toContain('width: 64');
expect(appSource).toContain('height: 64');
expect(appSource).toContain('minWidth: 260');
expect(appSource).toContain('const releaseTimeText = formatAboutReleaseTime(lastUpdateInfo?.releasePublishedAt);');
expect(appSource).toContain("[t('app.about.version.release_time'), releaseTimeText]");
expect(appSource).toContain("gridTemplateColumns: 'minmax(0, 1fr) minmax(220px, 260px)'");
expect(appSource).toContain('className="gonavi-about-update-channel"');
expect(appSource).toContain('<Segmented');
expect(appSource).toContain('maxWidth: 360');
expect(appSource).toContain("alignItems: 'start'");
expect(appSource).toContain("overflowWrap: 'anywhere'");
expect(appSource).toContain("t('app.about.version_update.title')");
expect(appSource).toContain("t('app.about.project.github.title')");
expect(appSource).toContain("t('app.about.project.issues.title')");

View File

@@ -226,6 +226,10 @@ type SettingsCenterPaneState = {
group: SettingsCenterGroupKey;
};
const resolveSettingsCenterGroupInitialPane = (group: SettingsCenterGroupKey): SettingsCenterPaneState | null => (
group === 'about' ? { key: 'about-go-navi', group: 'about' } : null
);
const DEFAULT_GLOBAL_PROXY_TEST_URL = 'https://api.github.com/';
type GlobalProxyTestResultState = {
@@ -271,6 +275,18 @@ const formatAboutCheckedAt = (value: Date): string => {
return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())} ${pad(value.getHours())}:${pad(value.getMinutes())}`;
};
const formatAboutReleaseTime = (value: string | undefined): string => {
const text = String(value || '').trim();
if (!text) {
return '-';
}
const date = new Date(text);
if (Number.isNaN(date.getTime())) {
return '-';
}
return formatAboutCheckedAt(date);
};
type ConnectionPackageDialogState = {
open: boolean;
mode: ConnectionPackageDialogMode;
@@ -2592,7 +2608,7 @@ function App() {
}, []);
const handleOpenSettingsModal = useCallback((group: SettingsCenterGroupKey = 'preferences') => {
setActiveSettingsCenterGroupKey(group);
setActiveSettingsCenterPane(null);
setActiveSettingsCenterPane(resolveSettingsCenterGroupInitialPane(group));
setIsSettingsModalOpen(true);
}, []);
const handleOpenSettingsCenterPane = useCallback((group: SettingsCenterGroupKey, key: SettingsCenterPaneKey) => {
@@ -3975,6 +3991,7 @@ function App() {
: (lastUpdateInfo ? t('app.about.hero.no_update') : t('app.about.update_status.not_checked'));
const currentVersionText = lastUpdateInfo?.currentVersion || aboutDisplayVersion;
const releaseNotesText = lastUpdateInfo?.releaseName || (hasUpdate ? t('app.about.release_notes.fallback') : '-');
const releaseTimeText = formatAboutReleaseTime(lastUpdateInfo?.releasePublishedAt);
const cardBorder = darkMode ? 'rgba(255,255,255,0.10)' : 'rgba(16,24,40,0.11)';
const cardBg = darkMode ? 'rgba(255,255,255,0.03)' : 'rgba(255,255,255,0.82)';
const mutedText = utilityMutedTextStyle.color;
@@ -3982,7 +3999,7 @@ function App() {
const versionRows = [
[t('app.about.version.current'), currentVersionText],
[t('app.about.version.latest'), latestVersionText],
[t('app.about.version.release_time'), '-'],
[t('app.about.version.release_time'), releaseTimeText],
[t('app.about.version.release_notes'), releaseNotesText],
];
@@ -4071,36 +4088,37 @@ function App() {
{t('app.about.version_update.title')}
</div>
<div style={{ borderTop: `1px solid ${dividerColor}`, padding: '18px 24px' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) 210px', alignItems: 'center', gap: 16 }}>
<div>
<div style={{ display: 'grid', gap: 8 }}>
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) minmax(220px, 260px)', alignItems: 'center', gap: 16 }}>
<div style={{ fontSize: 15, fontWeight: 600, color: overlayTheme.titleText }}>{t('app.about.field.update_channel')}</div>
<div style={{ ...utilityMutedTextStyle, marginTop: 6 }}>{t('app.about.version_update.channel_hint')}</div>
<Segmented
className="gonavi-about-update-channel"
value={updateChannel}
options={[
{ value: 'latest', label: t('app.about.update_channel.latest') },
{ value: 'dev', label: t('app.about.update_channel.dev') },
]}
onChange={(value) => {
void changeUpdateChannel(String(value));
}}
disabled={
isUpdateChannelLoading
|| isUpdateChannelSaving
|| updateDownloadProgress.status === 'start'
|| updateDownloadProgress.status === 'downloading'
}
block
style={{ width: '100%' }}
/>
</div>
<Select
value={updateChannel}
options={[
{ value: 'latest', label: t('app.about.update_channel.latest') },
{ value: 'dev', label: t('app.about.update_channel.dev') },
]}
onChange={(value) => {
void changeUpdateChannel(String(value));
}}
loading={isUpdateChannelLoading}
disabled={
isUpdateChannelLoading
|| isUpdateChannelSaving
|| updateDownloadProgress.status === 'start'
|| updateDownloadProgress.status === 'downloading'
}
style={{ width: '100%' }}
/>
<div style={{ ...utilityMutedTextStyle, lineHeight: 1.55, maxWidth: 360 }}>{t('app.about.version_update.channel_hint')}</div>
</div>
<div style={{ height: 1, background: dividerColor, margin: '18px 0' }} />
<div style={{ display: 'grid', gap: 13 }}>
{versionRows.map(([label, value]) => (
<div key={label} style={{ display: 'grid', gridTemplateColumns: '150px minmax(0, 1fr)', gap: 16, alignItems: 'baseline' }}>
<div style={{ fontWeight: 600, color: overlayTheme.titleText }}>{label}</div>
<div style={{ color: label === t('app.about.version.latest') && hasUpdate ? (darkMode ? '#86efac' : '#16a34a') : mutedText, fontWeight: label === t('app.about.version.latest') && hasUpdate ? 700 : 500 }}>
<div key={label} style={{ display: 'grid', gridTemplateColumns: '150px minmax(0, 1fr)', gap: 16, alignItems: 'start' }}>
<div style={{ fontWeight: 600, color: overlayTheme.titleText, lineHeight: 1.45 }}>{label}</div>
<div style={{ color: label === t('app.about.version.latest') && hasUpdate ? (darkMode ? '#86efac' : '#16a34a') : mutedText, fontWeight: label === t('app.about.version.latest') && hasUpdate ? 700 : 500, lineHeight: 1.45, minWidth: 0, overflowWrap: 'anywhere' }}>
{value}
</div>
</div>
@@ -6165,7 +6183,7 @@ function App() {
title={`${group.title} - ${group.description}`}
onClick={() => {
setActiveSettingsCenterGroupKey(group.key);
setActiveSettingsCenterPane(null);
setActiveSettingsCenterPane(resolveSettingsCenterGroupInitialPane(group.key));
}}
style={{
position: 'relative',

View File

@@ -221,6 +221,30 @@ describe('useAppUpdateManager', () => {
expect(hook?.lastUpdateInfo?.channel).toBe('dev');
});
it('keeps release metadata from the backend update response', async () => {
backendApp.CheckForUpdates.mockResolvedValue({
success: true,
data: {
hasUpdate: true,
currentVersion: '0.8.1',
latestVersion: '0.8.2',
releaseName: 'Dev Build (dev-22fab86)',
releasePublishedAt: '2026-07-08T11:15:00Z',
releaseNotesUrl: 'https://github.com/Syngnat/GoNavi/releases/tag/dev-latest',
},
});
renderHook();
await act(async () => {
await hook?.checkForUpdates(false);
});
expect(hook?.lastUpdateInfo?.releaseName).toBe('Dev Build (dev-22fab86)');
expect(hook?.lastUpdateInfo?.releasePublishedAt).toBe('2026-07-08T11:15:00Z');
expect(hook?.lastUpdateInfo?.releaseNotesUrl).toBe('https://github.com/Syngnat/GoNavi/releases/tag/dev-latest');
});
it('opens the downloaded update directory when a package is already downloaded', async () => {
backendApp.CheckForUpdates.mockResolvedValue({
success: true,

View File

@@ -13,6 +13,7 @@ export type UpdateInfo = {
currentVersion: string;
latestVersion: string;
releaseName?: string;
releasePublishedAt?: string;
releaseNotesUrl?: string;
assetName?: string;
assetUrl?: string;

View File

@@ -76,6 +76,9 @@ const importMain = async () => {
ExportConnectionsPackage: (options?: { includeSecrets?: boolean; filePassword?: string }) => Promise<{ success: boolean; message?: string }>;
ApplyDataRootDirectory: (path: string) => Promise<{ success: boolean; message?: string; data?: { path?: string } }>;
SaveQuery: (input: { id?: string; name?: string; sql?: string }) => Promise<{ name: string; sql: string }>;
CheckForUpdates: () => Promise<{ success: boolean; data?: Record<string, unknown> }>;
CheckForUpdatesSilently: () => Promise<{ success: boolean; data?: Record<string, unknown> }>;
SetUpdateChannel: (channel: string) => Promise<{ success: boolean; data?: { channel?: string } }>;
};
};
};
@@ -124,6 +127,34 @@ describe('main browser mock', () => {
});
}, 10000);
it('includes release metadata in browser mock update checks', async () => {
const app = await importMain();
await expect(app!.CheckForUpdates()).resolves.toMatchObject({
success: true,
data: {
channel: 'latest',
releaseName: 'Browser Mock Release',
releasePublishedAt: '2026-07-08T11:15:00Z',
releaseNotesUrl: 'https://github.com/Syngnat/GoNavi/releases/latest',
},
});
await expect(app!.SetUpdateChannel('dev')).resolves.toMatchObject({
success: true,
data: { channel: 'dev' },
});
await expect(app!.CheckForUpdatesSilently()).resolves.toMatchObject({
success: true,
data: {
channel: 'dev',
releaseName: 'Dev Build (dev-browser-mock)',
releasePublishedAt: '2026-07-08T11:15:00Z',
releaseNotesUrl: 'https://github.com/Syngnat/GoNavi/releases/tag/dev-latest',
},
});
});
it('rejects non-array payloads with the localized browser mock import limitation', async () => {
const app = await importMain();
const { t } = await import('./i18n');

View File

@@ -106,6 +106,18 @@ if (
let mockSkills: any[] = [];
let mockGlobalProxy: any = { enabled: false, type: 'socks5', host: '', port: 1080, user: '', password: '', hasPassword: false };
let mockUpdateChannel: 'latest' | 'dev' = 'latest';
const mockReleasePublishedAt = '2026-07-08T11:15:00Z';
const buildMockUpdateInfo = () => ({
hasUpdate: false,
channel: mockUpdateChannel,
currentVersion: '0.0.0',
latestVersion: mockUpdateChannel === 'dev' ? 'dev-browser-mock' : '0.0.0',
releaseName: mockUpdateChannel === 'dev' ? 'Dev Build (dev-browser-mock)' : 'Browser Mock Release',
releasePublishedAt: mockReleasePublishedAt,
releaseNotesUrl: mockUpdateChannel === 'dev'
? 'https://github.com/Syngnat/GoNavi/releases/tag/dev-latest'
: 'https://github.com/Syngnat/GoNavi/releases/latest',
});
let mockDataRootInfo: any = {
path: 'C:/mock/.gonavi',
defaultPath: 'C:/mock/.gonavi',
@@ -350,21 +362,11 @@ if (
GetDataRootDirectoryInfo: async () => ({ success: true, data: cloneBrowserMockValue(mockDataRootInfo) }),
CheckForUpdates: async () => ({
success: true,
data: {
hasUpdate: false,
channel: mockUpdateChannel,
currentVersion: '0.0.0',
latestVersion: mockUpdateChannel === 'dev' ? 'dev-browser-mock' : '0.0.0',
},
data: buildMockUpdateInfo(),
}),
CheckForUpdatesSilently: async () => ({
success: true,
data: {
hasUpdate: false,
channel: mockUpdateChannel,
currentVersion: '0.0.0',
latestVersion: mockUpdateChannel === 'dev' ? 'dev-browser-mock' : '0.0.0',
},
data: buildMockUpdateInfo(),
}),
GetUpdateChannel: async () => ({ success: true, data: { channel: mockUpdateChannel } }),
OpenDownloadedUpdateDirectory: async () => ({ success: false }),

View File

@@ -51,18 +51,19 @@ type updateState struct {
}
type UpdateInfo struct {
HasUpdate bool `json:"hasUpdate"`
Channel string `json:"channel"`
CurrentVersion string `json:"currentVersion"`
LatestVersion string `json:"latestVersion"`
ReleaseName string `json:"releaseName"`
ReleaseNotesURL string `json:"releaseNotesUrl"`
AssetName string `json:"assetName"`
AssetURL string `json:"assetUrl"`
AssetSize int64 `json:"assetSize"`
SHA256 string `json:"sha256"`
Downloaded bool `json:"downloaded"`
DownloadPath string `json:"downloadPath,omitempty"`
HasUpdate bool `json:"hasUpdate"`
Channel string `json:"channel"`
CurrentVersion string `json:"currentVersion"`
LatestVersion string `json:"latestVersion"`
ReleaseName string `json:"releaseName"`
ReleasePublishedAt string `json:"releasePublishedAt,omitempty"`
ReleaseNotesURL string `json:"releaseNotesUrl"`
AssetName string `json:"assetName"`
AssetURL string `json:"assetUrl"`
AssetSize int64 `json:"assetSize"`
SHA256 string `json:"sha256"`
Downloaded bool `json:"downloaded"`
DownloadPath string `json:"downloadPath,omitempty"`
}
type AppInfo struct {
@@ -108,11 +109,12 @@ type updatePathCandidate struct {
}
type githubRelease struct {
TagName string `json:"tag_name"`
Name string `json:"name"`
HTMLURL string `json:"html_url"`
Prerelease bool `json:"prerelease"`
Assets []githubAsset `json:"assets"`
TagName string `json:"tag_name"`
Name string `json:"name"`
HTMLURL string `json:"html_url"`
PublishedAt string `json:"published_at"`
Prerelease bool `json:"prerelease"`
Assets []githubAsset `json:"assets"`
}
type githubAsset struct {
@@ -458,12 +460,13 @@ func fetchLatestUpdateInfo(channel updateChannel) (UpdateInfo, error) {
}
if !hasUpdate {
return UpdateInfo{
HasUpdate: false,
Channel: string(channel),
CurrentVersion: currentVersion,
LatestVersion: latestVersion,
ReleaseName: release.Name,
ReleaseNotesURL: release.HTMLURL,
HasUpdate: false,
Channel: string(channel),
CurrentVersion: currentVersion,
LatestVersion: latestVersion,
ReleaseName: release.Name,
ReleasePublishedAt: strings.TrimSpace(release.PublishedAt),
ReleaseNotesURL: release.HTMLURL,
}, nil
}
@@ -492,16 +495,17 @@ func fetchLatestUpdateInfo(channel updateChannel) (UpdateInfo, error) {
return UpdateInfo{}, localizedUpdateError{key: "app.update.backend.error.sha256_missing_current_package"}
}
return UpdateInfo{
HasUpdate: hasUpdate,
Channel: string(channel),
CurrentVersion: currentVersion,
LatestVersion: latestVersion,
ReleaseName: release.Name,
ReleaseNotesURL: release.HTMLURL,
AssetName: asset.Name,
AssetURL: asset.BrowserDownloadURL,
AssetSize: asset.Size,
SHA256: sha256Value,
HasUpdate: hasUpdate,
Channel: string(channel),
CurrentVersion: currentVersion,
LatestVersion: latestVersion,
ReleaseName: release.Name,
ReleasePublishedAt: strings.TrimSpace(release.PublishedAt),
ReleaseNotesURL: release.HTMLURL,
AssetName: asset.Name,
AssetURL: asset.BrowserDownloadURL,
AssetSize: asset.Size,
SHA256: sha256Value,
}, nil
}

View File

@@ -30,9 +30,10 @@ func TestFetchLatestUpdateInfoSkipsChecksumWhenCurrentVersionIsAlreadyLatest(t *
restoreRelease := swapUpdateFetchLatestRelease(func() (*githubRelease, error) {
releaseCalled = true
return &githubRelease{
TagName: "v0.6.5",
Name: "v0.6.5",
HTMLURL: "https://github.com/Syngnat/GoNavi/releases/tag/v0.6.5",
TagName: "v0.6.5",
Name: "v0.6.5",
HTMLURL: "https://github.com/Syngnat/GoNavi/releases/tag/v0.6.5",
PublishedAt: "2026-07-08T11:15:00Z",
Assets: []githubAsset{
{
Name: assetName,
@@ -84,9 +85,10 @@ func TestFetchLatestUpdateInfoUsesAssetDigestWhenUpdateIsAvailable(t *testing.T)
restoreRelease := swapUpdateFetchLatestRelease(func() (*githubRelease, error) {
return &githubRelease{
TagName: "v0.6.5",
Name: "v0.6.5",
HTMLURL: "https://github.com/Syngnat/GoNavi/releases/tag/v0.6.5",
TagName: "v0.6.5",
Name: "v0.6.5",
HTMLURL: "https://github.com/Syngnat/GoNavi/releases/tag/v0.6.5",
PublishedAt: "2026-07-08T11:15:00Z",
Assets: []githubAsset{
{
Name: assetName,
@@ -119,6 +121,9 @@ func TestFetchLatestUpdateInfoUsesAssetDigestWhenUpdateIsAvailable(t *testing.T)
if info.SHA256 != strings.ToLower(digest) || info.AssetName != assetName {
t.Fatalf("unexpected update info: %#v", info)
}
if info.ReleasePublishedAt != "2026-07-08T11:15:00Z" {
t.Fatalf("expected release published time to be preserved, got %#v", info)
}
}
func TestFetchLatestUpdateInfoFallsBackToChecksumFileWhenAssetDigestMissing(t *testing.T) {