Skip to content

Commit b28b4a9

Browse files
committed
优化本地环境修复入口展示
- 将本地环境一键修复入口文案和后端修复能力名称统一为 一键修复。 - 在仪表盘本地环境详情中将已就绪依赖归入置顶折叠组,默认仅展开有问题的依赖项。 - 调整本地环境修复按钮为深绿底白字,并补充就绪折叠组样式以提升文本对比度。 - 补充前端本地环境页面测试、中文资源断言和本地依赖健康能力名称断言。
1 parent aa83abb commit b28b4a9

7 files changed

Lines changed: 232 additions & 61 deletions

File tree

resources/webui/upstream/source/src/i18n/locales/zh-CN.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,12 +201,15 @@
201201
"local_environment_repair_desktop_only": "修复入口仅在桌面模式可用。",
202202
"local_environment_repair": "修复",
203203
"local_environment_repair_unavailable": "当前修复能力不可用。",
204-
"local_environment_required_repair": "一键修复并安装最新 Codex",
204+
"local_environment_required_repair": "一键修复",
205205
"local_environment_required_repairing_button": "一键修复中",
206206
"local_environment_required_repair_unavailable": "请先完成检测,或修复 winget、PowerShell、npm 后再使用。",
207207
"local_environment_required_repair_loading": "正在读取本机修复能力。",
208208
"local_environment_required_repair_confirm_title": "确认一键修复本地环境",
209209
"local_environment_required_repair_confirm_message": "将修复必备环境并安装或升级最新 Codex;需要时会请求管理员授权,不会自动安装或更新 WSL。是否继续?",
210+
"local_environment_ready_group_title": "已就绪 {{count}} 项",
211+
"local_environment_ready_group_expand": "展开查看",
212+
"local_environment_ready_group_collapse": "收起",
210213
"local_environment_repair_confirm_title": "确认修复本地环境",
211214
"local_environment_repair_confirm_message": "将修复 {{name}};需要时会请求管理员授权。是否继续?",
212215
"local_environment_repairing_button": "修复中",

resources/webui/upstream/source/src/pages/DashboardPage.module.scss

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
@use 'sass:color';
21
@use '../styles/variables.scss' as *;
32

43
// ─── Container ──────────────────────────────────────────
@@ -444,18 +443,19 @@
444443
}
445444

446445
.localEnvironmentRepairButton:global(.btn) {
447-
background: rgba($success-color, 0.12);
448-
border-color: rgba($success-color, 0.32);
449-
color: $success-color;
450-
box-shadow: 0 6px 16px rgba($success-color, 0.12);
446+
background: #047857;
447+
border-color: #047857;
448+
color: #fff;
449+
box-shadow: 0 6px 16px rgba(4, 120, 87, 0.18);
451450

452451
&:hover:not(:disabled) {
453-
background: rgba($success-color, 0.18);
454-
border-color: rgba($success-color, 0.48);
455-
color: color.adjust($success-color, $lightness: -4%);
452+
background: #065f46;
453+
border-color: #065f46;
454+
color: #fff;
456455
}
457456

458457
&:disabled {
458+
opacity: 1;
459459
background: var(--bg-tertiary);
460460
border-color: var(--border-color);
461461
color: var(--text-secondary);
@@ -504,6 +504,66 @@
504504
gap: $spacing-sm;
505505
}
506506

507+
.localReadyGroup {
508+
display: flex;
509+
flex-direction: column;
510+
gap: $spacing-sm;
511+
order: -1;
512+
}
513+
514+
.localReadyToggle {
515+
display: flex;
516+
align-items: center;
517+
justify-content: space-between;
518+
gap: $spacing-md;
519+
width: 100%;
520+
padding: $spacing-sm $spacing-md;
521+
border: 1px solid rgba($success-color, 0.34);
522+
border-radius: $radius-md;
523+
background: rgba($success-color, 0.1);
524+
color: var(--text-primary);
525+
cursor: pointer;
526+
transition:
527+
background $transition-fast,
528+
border-color $transition-fast;
529+
530+
&:hover {
531+
background: rgba($success-color, 0.16);
532+
border-color: rgba($success-color, 0.48);
533+
}
534+
535+
svg {
536+
flex-shrink: 0;
537+
color: #047857;
538+
}
539+
}
540+
541+
.localReadyToggleText {
542+
min-width: 0;
543+
display: flex;
544+
align-items: baseline;
545+
gap: 8px;
546+
flex-wrap: wrap;
547+
text-align: left;
548+
549+
strong {
550+
color: var(--text-primary);
551+
font-size: 13px;
552+
}
553+
554+
span {
555+
color: var(--text-secondary);
556+
font-size: 12px;
557+
font-weight: 600;
558+
}
559+
}
560+
561+
.localReadyGroupItems {
562+
display: flex;
563+
flex-direction: column;
564+
gap: $spacing-sm;
565+
}
566+
507567
.localDependencyItem {
508568
display: grid;
509569
grid-template-columns: minmax(0, 1fr) auto;

resources/webui/upstream/source/src/pages/DashboardPage.tsx

Lines changed: 88 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { useTranslation } from 'react-i18next';
44
import { Button } from '@/components/ui/Button';
55
import {
66
IconBot,
7+
IconChevronDown,
8+
IconChevronUp,
79
IconKey,
810
IconSatellite,
911
IconShield,
@@ -237,6 +239,7 @@ export function DashboardPage() {
237239

238240
const [loading, setLoading] = useState(true);
239241
const [localEnvironmentOpen, setLocalEnvironmentOpen] = useState(false);
242+
const [localReadyItemsOpen, setLocalReadyItemsOpen] = useState(false);
240243
const [localEnvironmentLoading, setLocalEnvironmentLoading] = useState(false);
241244
const [localEnvironmentError, setLocalEnvironmentError] = useState('');
242245
const [localEnvironmentSnapshot, setLocalEnvironmentSnapshot] =
@@ -339,6 +342,7 @@ export function DashboardPage() {
339342
if (!isDesktopMode()) {
340343
setLocalEnvironmentError('');
341344
setLocalEnvironmentSnapshot(null);
345+
setLocalReadyItemsOpen(false);
342346
return;
343347
}
344348

@@ -350,6 +354,7 @@ export function DashboardPage() {
350354
try {
351355
const snapshot = await request;
352356
setLocalEnvironmentSnapshot(snapshot);
357+
setLocalReadyItemsOpen(false);
353358
} catch (error) {
354359
const message = error instanceof Error ? error.message : t('common.unknown_error');
355360
setLocalEnvironmentError(message);
@@ -496,6 +501,7 @@ export function DashboardPage() {
496501
}));
497502
if (response.snapshot) {
498503
setLocalEnvironmentSnapshot(response.snapshot);
504+
setLocalReadyItemsOpen(false);
499505
} else {
500506
await fetchLocalEnvironment();
501507
}
@@ -619,6 +625,8 @@ export function DashboardPage() {
619625
const localEnvironmentItems = buildLocalEnvironmentDisplayItems(
620626
localEnvironmentSnapshot?.items ?? []
621627
);
628+
const localReadyItems = localEnvironmentItems.filter((item) => item.status === 'ready');
629+
const localIssueItems = localEnvironmentItems.filter((item) => item.status !== 'ready');
622630
const localRequiredReady = localRequiredItems.filter((item) => item.status === 'ready').length;
623631
const localOptionalUnavailable =
624632
localEnvironmentSnapshot?.items.filter((item) => item.status === 'optionalUnavailable').length ?? 0;
@@ -726,6 +734,53 @@ export function DashboardPage() {
726734
</div>
727735
);
728736

737+
const renderLocalDependencyItem = (item: LocalDependencyItem) => {
738+
const isRepairingItem =
739+
repairingTarget?.itemId === item.id &&
740+
repairingTarget.actionId === item.repairActionId;
741+
const itemRepairProgress =
742+
item.id === localRepairProgressItemId &&
743+
item.repairActionId &&
744+
localRepairProgress?.actionId === item.repairActionId
745+
? localRepairProgress
746+
: null;
747+
748+
return (
749+
<div key={item.id} className={styles.localDependencyItem}>
750+
<div className={styles.localDependencyMain}>
751+
<div className={styles.localDependencyTitleRow}>
752+
<strong>{item.name}</strong>
753+
<span className={`${styles.localStatusBadge} ${getLocalStatusClass(item.status)}`}>
754+
{getLocalStatusLabel(item.status)}
755+
</span>
756+
</div>
757+
<div className={styles.localDependencyMeta}>
758+
{item.version && <span>{item.version}</span>}
759+
{item.path && <span>{item.path}</span>}
760+
</div>
761+
<p>{item.detail}</p>
762+
{item.recommendation && <small>{item.recommendation}</small>}
763+
</div>
764+
{item.repairActionId && localEnvironmentDesktopMode && (
765+
<Button
766+
type="button"
767+
variant="secondary"
768+
size="sm"
769+
className={styles.localEnvironmentRepairButton}
770+
onClick={() => handleRepairLocalDependency(item)}
771+
loading={isRepairingItem}
772+
disabled={repairingTarget !== null}
773+
>
774+
{isRepairingItem
775+
? t('dashboard.local_environment_repairing_button')
776+
: t('dashboard.local_environment_repair')}
777+
</Button>
778+
)}
779+
{itemRepairProgress && renderLocalRepairProgress(itemRepairProgress)}
780+
</div>
781+
);
782+
};
783+
729784
return (
730785
<div className={styles.dashboard}>
731786
{/* Decorative background orbs */}
@@ -905,55 +960,40 @@ export function DashboardPage() {
905960
)}
906961
{localEnvironmentSnapshot && (
907962
<div className={styles.localEnvironmentItems}>
908-
{localEnvironmentItems.map((item) => {
909-
const isRepairingItem =
910-
repairingTarget?.itemId === item.id &&
911-
repairingTarget.actionId === item.repairActionId;
912-
const itemRepairProgress =
913-
item.id === localRepairProgressItemId &&
914-
item.repairActionId &&
915-
localRepairProgress?.actionId === item.repairActionId
916-
? localRepairProgress
917-
: null;
918-
return (
919-
<div key={item.id} className={styles.localDependencyItem}>
920-
<div className={styles.localDependencyMain}>
921-
<div className={styles.localDependencyTitleRow}>
922-
<strong>{item.name}</strong>
923-
<span
924-
className={`${styles.localStatusBadge} ${getLocalStatusClass(item.status)}`}
925-
>
926-
{getLocalStatusLabel(item.status)}
927-
</span>
928-
</div>
929-
<div className={styles.localDependencyMeta}>
930-
{item.version && <span>{item.version}</span>}
931-
{item.path && <span>{item.path}</span>}
932-
</div>
933-
<p>{item.detail}</p>
934-
{item.recommendation && <small>{item.recommendation}</small>}
935-
</div>
936-
{item.repairActionId && localEnvironmentDesktopMode && (
937-
<Button
938-
type="button"
939-
variant="secondary"
940-
size="sm"
941-
className={styles.localEnvironmentRepairButton}
942-
onClick={() => handleRepairLocalDependency(item)}
943-
loading={isRepairingItem}
944-
disabled={repairingTarget !== null}
945-
>
946-
{isRepairingItem
947-
? t('dashboard.local_environment_repairing_button')
948-
: t('dashboard.local_environment_repair')}
949-
</Button>
963+
{localReadyItems.length > 0 && (
964+
<div className={styles.localReadyGroup}>
965+
<button
966+
type="button"
967+
className={styles.localReadyToggle}
968+
aria-expanded={localReadyItemsOpen}
969+
onClick={() => setLocalReadyItemsOpen((open) => !open)}
970+
>
971+
<span className={styles.localReadyToggleText}>
972+
<strong>
973+
{t('dashboard.local_environment_ready_group_title', {
974+
count: localReadyItems.length,
975+
})}
976+
</strong>
977+
<span>
978+
{localReadyItemsOpen
979+
? t('dashboard.local_environment_ready_group_collapse')
980+
: t('dashboard.local_environment_ready_group_expand')}
981+
</span>
982+
</span>
983+
{localReadyItemsOpen ? (
984+
<IconChevronUp size={16} />
985+
) : (
986+
<IconChevronDown size={16} />
950987
)}
951-
{itemRepairProgress && (
952-
renderLocalRepairProgress(itemRepairProgress)
953-
)}
954-
</div>
955-
);
956-
})}
988+
</button>
989+
{localReadyItemsOpen && (
990+
<div className={styles.localReadyGroupItems}>
991+
{localReadyItems.map(renderLocalDependencyItem)}
992+
</div>
993+
)}
994+
</div>
995+
)}
996+
{localIssueItems.map(renderLocalDependencyItem)}
957997
</div>
958998
)}
959999
</div>

resources/webui/upstream/source/tests/pages/DashboardPage.localEnvironment.test.tsx

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ const snapshot: LocalDependencySnapshot = {
110110
repairCapabilities: [
111111
{
112112
actionId: 'repair-required-env-install-latest-codex',
113-
name: '一键修复并安装最新 Codex',
113+
name: '一键修复',
114114
isAvailable: true,
115115
requiresElevation: true,
116116
isOptional: false,
@@ -239,6 +239,68 @@ describe('DashboardPage local environment loading', () => {
239239
).toContain('localEnvironmentRepairButton');
240240
});
241241

242+
it('places ready local environment items first but keeps them collapsed by default', async () => {
243+
bridgeMocks.requestLocalDependencySnapshot.mockResolvedValue({
244+
...snapshot,
245+
summary: '本地环境混合状态。',
246+
items: [
247+
{
248+
id: 'powershell',
249+
name: 'PowerShell',
250+
status: 'ready',
251+
severity: 'required',
252+
version: '7.5.0',
253+
path: 'C:\\Program Files\\PowerShell\\7\\pwsh.exe',
254+
detail: 'PowerShell 已就绪。',
255+
recommendation: '',
256+
repairActionId: null,
257+
},
258+
{
259+
id: 'codex-cli',
260+
name: 'Codex CLI',
261+
status: 'missing',
262+
severity: 'required',
263+
version: null,
264+
path: null,
265+
detail: '未找到 Codex CLI。',
266+
recommendation: '安装最新 Codex。',
267+
repairActionId: 'install-codex-cli',
268+
},
269+
],
270+
repairCapabilities: [
271+
snapshot.repairCapabilities[0],
272+
{
273+
actionId: 'install-codex-cli',
274+
name: '安装 Codex CLI',
275+
isAvailable: true,
276+
requiresElevation: false,
277+
isOptional: false,
278+
detail: '',
279+
},
280+
],
281+
});
282+
renderDashboard();
283+
284+
fireEvent.click(getLocalEnvironmentButton());
285+
await screen.findByText('本地环境混合状态。');
286+
287+
const readyToggle = screen.getByRole('button', {
288+
name: /dashboard\.local_environment_ready_group_title/,
289+
});
290+
const codexRow = getDependencyRow('Codex CLI');
291+
292+
expect(screen.queryByText('PowerShell')).not.toBeInTheDocument();
293+
expect(readyToggle.getAttribute('aria-expanded')).toBe('false');
294+
expect(
295+
readyToggle.compareDocumentPosition(codexRow) & Node.DOCUMENT_POSITION_FOLLOWING
296+
).toBeTruthy();
297+
298+
fireEvent.click(readyToggle);
299+
300+
expect(readyToggle.getAttribute('aria-expanded')).toBe('true');
301+
expect(getDependencyRow('PowerShell')).toBeInTheDocument();
302+
});
303+
242304
it('clears a previous bridge error before retrying', async () => {
243305
bridgeMocks.requestLocalDependencySnapshot
244306
.mockRejectedValueOnce(new Error('旧错误'))
@@ -447,7 +509,7 @@ describe('DashboardPage local environment loading', () => {
447509
repairCapabilities: [
448510
{
449511
actionId: 'repair-required-env-install-latest-codex',
450-
name: '一键修复并安装最新 Codex',
512+
name: '一键修复',
451513
isAvailable: false,
452514
requiresElevation: true,
453515
isOptional: false,

0 commit comments

Comments
 (0)