feat: 重构侧边栏导航结构

- 更新 WorkspaceDestination 枚举,添加 skills/nodes/agents/clawHub/aiGateway
- 调整侧边栏顺序:助手→任务→技能→节点→代理 | ClawHub→密钥→AI Gateway
- 新建 SkillsPage 展示已安装技能列表
- ModulesPage 添加 initialTab 参数支持直接跳转
- 修复 ai_gateway_page 和 claw_hub_page 的编译错误
- 更新测试文件适配新的导航结构
This commit is contained in:
Haitao Pan 2026-03-13 21:19:43 +08:00
parent 4d2cb683f2
commit 23d75d45fa
8 changed files with 302 additions and 54 deletions

View File

@ -1,14 +1,15 @@
import 'package:flutter/material.dart';
import '../features/account/account_page.dart';
import '../features/ai_gateway/ai_gateway_page.dart';
import '../features/assistant/assistant_page.dart';
import '../features/claw_hub/claw_hub_page.dart';
import '../features/mobile/ios_mobile_shell.dart';
import '../features/modules/modules_page.dart';
import '../features/secrets/secrets_page.dart';
import '../features/settings/settings_page.dart';
import '../features/tasks/tasks_page.dart';
import '../features/account/account_page.dart';
import '../features/ai_gateway/ai_gateway_page.dart';
import '../features/assistant/assistant_page.dart';
import '../features/claw_hub/claw_hub_page.dart';
import '../features/mobile/ios_mobile_shell.dart';
import '../features/modules/modules_page.dart';
import '../features/secrets/secrets_page.dart';
import '../features/settings/settings_page.dart';
import '../features/skills/skills_page.dart';
import '../features/tasks/tasks_page.dart';
import '../i18n/app_language.dart';
import '../models/app_models.dart';
import '../theme/app_palette.dart';
@ -33,7 +34,7 @@ class _AppShellState extends State<AppShell> {
static const _mobileDestinations = [
WorkspaceDestination.assistant,
WorkspaceDestination.tasks,
WorkspaceDestination.modules,
WorkspaceDestination.skills,
WorkspaceDestination.secrets,
WorkspaceDestination.settings,
];
@ -325,10 +326,20 @@ class _AppShellState extends State<AppShell> {
controller: widget.controller,
onOpenDetail: onOpenDetail,
),
WorkspaceDestination.modules => ModulesPage(
WorkspaceDestination.skills => SkillsPage(
controller: widget.controller,
onOpenDetail: onOpenDetail,
),
WorkspaceDestination.nodes => ModulesPage(
controller: widget.controller,
onOpenDetail: onOpenDetail,
initialTab: ModulesTab.nodes,
),
WorkspaceDestination.agents => ModulesPage(
controller: widget.controller,
onOpenDetail: onOpenDetail,
initialTab: ModulesTab.agents,
),
WorkspaceDestination.clawHub => ClawHubPage(
controller: widget.controller,
onOpenDetail: onOpenDetail,

View File

@ -1,13 +1,16 @@
import 'package:flutter/material.dart';
import '../../app/app_controller.dart';
import '../../i18n/app_language.dart';
import '../../models/app_models.dart';
import '../../widgets/metric_card.dart';
import '../../widgets/section_header.dart';
import '../../widgets/section_tabs.dart';
import '../../widgets/surface_card.dart';
import '../../widgets/top_bar.dart';
import 'package:flutter/material.dart';
import '../../app/app_controller.dart';
import '../../i18n/app_language.dart';
import '../../models/app_models.dart';
import '../../runtime/runtime_models.dart';
import '../../theme/app_palette.dart';
import '../../theme/app_theme.dart';
import '../../widgets/metric_card.dart';
import '../../widgets/section_header.dart';
import '../../widgets/section_tabs.dart';
import '../../widgets/surface_card.dart';
import '../../widgets/top_bar.dart';
class AiGatewayPage extends StatefulWidget {
const AiGatewayPage({
@ -77,11 +80,12 @@ class _AiGatewayPageState extends State<AiGatewayPage> {
children: metrics.map((m) => MetricCard(metric: m)).toList(),
),
const SizedBox(height: 24),
SectionTabs<AiGatewayTab>(
tabs: AiGatewayTab.values,
selected: _tab,
onSelect: (t) => setState(() => _tab = t),
labelFor: (t) => t.label,
SectionTabs(
items: AiGatewayTab.values.map((t) => t.label).toList(),
value: _tab.label,
onChanged: (label) => setState(
() => _tab = AiGatewayTab.values.firstWhere((t) => t.label == label),
),
),
const SizedBox(height: 16),
_buildTabContent(context, _tab, controller),
@ -201,7 +205,7 @@ class _AiGatewayPageState extends State<AiGatewayPage> {
children: [
Row(
children: [
Icon(Icons.endpoint_rounded, color: palette.accent, size: 20),
Icon(Icons.device_hub_rounded, color: palette.accent, size: 20),
const SizedBox(width: 8),
Text(
appText('端点配置', 'Endpoint Configuration'),
@ -238,7 +242,7 @@ class _AiGatewayPageState extends State<AiGatewayPage> {
return switch (status) {
RuntimeConnectionStatus.connected => const StatusInfo('Connected', StatusTone.success),
RuntimeConnectionStatus.connecting => const StatusInfo('Connecting', StatusTone.accent),
RuntimeConnectionStatus.disconnected => const StatusInfo('Disconnected', StatusTone.neutral),
RuntimeConnectionStatus.offline => const StatusInfo('Offline', StatusTone.neutral),
RuntimeConnectionStatus.error => const StatusInfo('Error', StatusTone.danger),
};
}
@ -362,7 +366,7 @@ class _EndpointCard extends StatelessWidget {
child: ListTile(
onTap: onTap,
leading: Icon(
Icons.endpoint_rounded,
Icons.device_hub_rounded,
color: isConnected ? palette.accent : palette.textMuted,
),
title: Text(name, style: TextStyle(color: palette.textPrimary)),

View File

@ -1,11 +1,13 @@
import 'package:flutter/material.dart';
import '../../app/app_controller.dart';
import '../../i18n/app_language.dart';
import '../../models/app_models.dart';
import '../../widgets/section_header.dart';
import '../../widgets/surface_card.dart';
import '../../widgets/top_bar.dart';
import 'package:flutter/material.dart';
import '../../app/app_controller.dart';
import '../../i18n/app_language.dart';
import '../../models/app_models.dart';
import '../../theme/app_palette.dart';
import '../../theme/app_theme.dart';
import '../../widgets/section_header.dart';
import '../../widgets/surface_card.dart';
import '../../widgets/top_bar.dart';
class ClawHubPage extends StatefulWidget {
const ClawHubPage({
@ -191,7 +193,7 @@ class _ClawHubPageState extends State<ClawHubPage> {
const SizedBox(height: 24),
SectionHeader(
title: appText('终端', 'Terminal'),
icon: Icons.terminal_rounded,
subtitle: appText('执行终端命令', 'Execute terminal commands'),
),
const SizedBox(height: 12),
SurfaceCard(
@ -327,7 +329,7 @@ class _ClawHubPageState extends State<ClawHubPage> {
const SizedBox(height: 24),
SectionHeader(
title: appText('快速操作', 'Quick Actions'),
icon: Icons.bolt_rounded,
subtitle: appText('常用操作快捷入口', 'Quick access to common actions'),
),
const SizedBox(height: 12),
Wrap(

View File

@ -13,23 +13,33 @@ import '../../widgets/status_badge.dart';
import '../../widgets/surface_card.dart';
import '../../widgets/top_bar.dart';
class ModulesPage extends StatefulWidget {
class ModulesPage extends StatefulWidget {
const ModulesPage({
super.key,
required this.controller,
required this.onOpenDetail,
this.initialTab,
});
final AppController controller;
final ValueChanged<DetailPanelData> onOpenDetail;
final ModulesTab? initialTab;
@override
State<ModulesPage> createState() => _ModulesPageState();
}
class _ModulesPageState extends State<ModulesPage> {
class _ModulesPageState extends State<ModulesPage> {
ModulesTab _tab = ModulesTab.gateway;
@override
void initState() {
super.initState();
if (widget.initialTab != null) {
_tab = widget.initialTab!;
}
}
@override
Widget build(BuildContext context) {
final controller = widget.controller;

View File

@ -0,0 +1,191 @@
import 'package:flutter/material.dart';
import '../../app/app_controller.dart';
import '../../i18n/app_language.dart';
import '../../models/app_models.dart';
import '../../runtime/runtime_models.dart';
import '../../widgets/status_badge.dart';
import '../../widgets/surface_card.dart';
import '../../widgets/top_bar.dart';
class SkillsPage extends StatelessWidget {
const SkillsPage({
super.key,
required this.controller,
required this.onOpenDetail,
});
final AppController controller;
final ValueChanged<DetailPanelData> onOpenDetail;
@override
Widget build(BuildContext context) {
final items = controller.skills;
return AnimatedBuilder(
animation: controller,
builder: (context, _) {
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(32, 32, 32, 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TopBar(
title: appText('技能', 'Skills'),
subtitle: appText(
'管理已安装的技能包,查看技能状态与依赖。',
'Manage installed skill packages, view status and dependencies.',
),
trailing: Wrap(
spacing: 12,
runSpacing: 12,
children: [
SizedBox(
width: 220,
child: TextField(
decoration: InputDecoration(
hintText: appText('搜索技能', 'Search skills'),
prefixIcon: Icon(Icons.search_rounded),
),
),
),
IconButton(
onPressed: () async {
await controller.skillsController.refresh(
agentId: controller.selectedAgentId.isEmpty
? null
: controller.selectedAgentId,
);
},
icon: const Icon(Icons.refresh_rounded),
),
],
),
),
const SizedBox(height: 24),
if (items.isEmpty)
SurfaceCard(
child: Text(
controller.connection.status ==
RuntimeConnectionStatus.connected
? appText(
'当前网关或代理没有加载技能。',
'No skills loaded for the active gateway / agent.',
)
: appText(
'连接 Gateway 后可加载技能。',
'Connect a gateway to load skills.',
),
),
)
else
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: items
.map(
(skill) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: SurfaceCard(
onTap: () => onOpenDetail(
DetailPanelData(
title: skill.name,
subtitle: appText('技能', 'Skill'),
icon: Icons.extension_rounded,
status: skill.disabled
? StatusInfo(
appText('已禁用', 'Disabled'),
StatusTone.warning,
)
: StatusInfo(
appText('已启用', 'Enabled'),
StatusTone.success,
),
description: skill.description,
meta: [skill.source, skill.skillKey],
actions: [appText('刷新', 'Refresh')],
sections: [
DetailSection(
title: appText('依赖要求', 'Requirements'),
items: [
DetailItem(
label: appText(
'缺失二进制', 'Missing bins'),
value: skill.missingBins.isEmpty
? appText('', 'None')
: skill.missingBins.join(', '),
),
DetailItem(
label: appText(
'缺失环境变量', 'Missing env'),
value: skill.missingEnv.isEmpty
? appText('', 'None')
: skill.missingEnv.join(', '),
),
DetailItem(
label: appText('缺失配置', 'Missing config'),
value: skill.missingConfig.isEmpty
? appText('', 'None')
: skill.missingConfig.join(', '),
),
],
),
],
),
),
child: Row(
children: [
Expanded(
flex: 4,
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
skill.name,
style: Theme.of(context)
.textTheme
.titleMedium,
),
const SizedBox(height: 6),
Text(
skill.description,
style:
Theme.of(context).textTheme.bodySmall,
),
],
),
),
Expanded(
flex: 2,
child: StatusBadge(
status: skill.disabled
? StatusInfo(
appText('已禁用', 'Disabled'),
StatusTone.warning,
)
: StatusInfo(
appText('已启用', 'Enabled'),
StatusTone.success,
),
),
),
Expanded(flex: 2, child: Text(skill.source)),
Expanded(
flex: 2,
child: Text(skill.primaryEnv ?? 'workspace'),
),
const Icon(Icons.chevron_right_rounded),
],
),
),
),
)
.toList(),
),
],
),
);
},
);
}
}

View File

@ -5,8 +5,12 @@ import '../i18n/app_language.dart';
enum WorkspaceDestination {
assistant,
tasks,
modules,
skills,
nodes,
agents,
clawHub,
secrets,
aiGateway,
settings,
account,
}
@ -15,8 +19,12 @@ extension WorkspaceDestinationCopy on WorkspaceDestination {
String get label => switch (this) {
WorkspaceDestination.assistant => appText('助手', 'Assistant'),
WorkspaceDestination.tasks => appText('任务', 'Tasks'),
WorkspaceDestination.modules => appText('模块', 'Modules'),
WorkspaceDestination.skills => appText('技能', 'Skills'),
WorkspaceDestination.nodes => appText('节点', 'Nodes'),
WorkspaceDestination.agents => appText('代理', 'Agents'),
WorkspaceDestination.clawHub => 'ClawHub',
WorkspaceDestination.secrets => appText('密钥', 'Secrets'),
WorkspaceDestination.aiGateway => 'AI Gateway',
WorkspaceDestination.settings => appText('设置', 'Settings'),
WorkspaceDestination.account => appText('账号', 'Account'),
};
@ -24,8 +32,12 @@ extension WorkspaceDestinationCopy on WorkspaceDestination {
IconData get icon => switch (this) {
WorkspaceDestination.assistant => Icons.chat_bubble_outline_rounded,
WorkspaceDestination.tasks => Icons.layers_rounded,
WorkspaceDestination.modules => Icons.extension_rounded,
WorkspaceDestination.skills => Icons.auto_awesome_rounded,
WorkspaceDestination.nodes => Icons.developer_board_rounded,
WorkspaceDestination.agents => Icons.hub_rounded,
WorkspaceDestination.clawHub => Icons.extension_rounded,
WorkspaceDestination.secrets => Icons.key_rounded,
WorkspaceDestination.aiGateway => Icons.smart_toy_rounded,
WorkspaceDestination.settings => Icons.tune_rounded,
WorkspaceDestination.account => Icons.account_circle_rounded,
};
@ -39,9 +51,21 @@ extension WorkspaceDestinationCopy on WorkspaceDestination {
'任务队列、运行态、失败项和调度历史的统一视图。',
'Unified view for queue, running, failed, and history.',
),
WorkspaceDestination.modules => appText(
'平台能力中心,管理 Gateway、Nodes、Agents、Skills 与 Connectors。',
'Capability center for gateway, nodes, agents, skills, and connectors.',
WorkspaceDestination.skills => appText(
'管理技能包与能力扩展,浏览和安装 ClawHub 技能。',
'Manage skill packages and extensions, browse and install from ClawHub.',
),
WorkspaceDestination.nodes => appText(
'管理边缘节点与实例,监控运行状态与负载。',
'Manage edge nodes and instances, monitor status and load.',
),
WorkspaceDestination.agents => appText(
'管理代理实例,配置行为与能力。',
'Manage agent instances, configure behaviors and capabilities.',
),
WorkspaceDestination.clawHub => appText(
'浏览和安装技能包、代理模板与连接器。',
'Browse and install skill packages, agent templates and connectors.',
),
WorkspaceDestination.secrets => appText(
'Vault 密码保险箱,安全存储密钥、凭证与审计信息。',

View File

@ -40,7 +40,9 @@ class SidebarNavigation extends StatelessWidget {
static const _mainSections = <WorkspaceDestination>[
WorkspaceDestination.assistant,
WorkspaceDestination.tasks,
WorkspaceDestination.modules,
WorkspaceDestination.skills,
WorkspaceDestination.nodes,
WorkspaceDestination.agents,
WorkspaceDestination.clawHub,
WorkspaceDestination.secrets,
WorkspaceDestination.aiGateway,
@ -253,9 +255,11 @@ class _SidebarNavItemState extends State<SidebarNavItem> {
return switch (section) {
WorkspaceDestination.assistant => Icons.auto_awesome_rounded,
WorkspaceDestination.tasks => Icons.task_alt_rounded,
WorkspaceDestination.modules => Icons.extension_rounded,
WorkspaceDestination.clawHub => Icons.hub_rounded,
WorkspaceDestination.secrets => Icons.security_rounded,
WorkspaceDestination.skills => Icons.auto_awesome_rounded,
WorkspaceDestination.nodes => Icons.developer_board_rounded,
WorkspaceDestination.agents => Icons.hub_rounded,
WorkspaceDestination.clawHub => Icons.extension_rounded,
WorkspaceDestination.secrets => Icons.key_rounded,
WorkspaceDestination.aiGateway => Icons.smart_toy_rounded,
WorkspaceDestination.settings => Icons.tune_rounded,
WorkspaceDestination.account => Icons.account_circle_rounded,
@ -266,9 +270,11 @@ class _SidebarNavItemState extends State<SidebarNavItem> {
return switch (section) {
WorkspaceDestination.assistant => appText('助手', 'Assistant'),
WorkspaceDestination.tasks => appText('任务', 'Tasks'),
WorkspaceDestination.modules => appText('模块', 'Modules'),
WorkspaceDestination.skills => appText('技能', 'Skills'),
WorkspaceDestination.nodes => appText('节点', 'Nodes'),
WorkspaceDestination.agents => appText('代理', 'Agents'),
WorkspaceDestination.clawHub => 'ClawHub',
WorkspaceDestination.secrets => appText('密钥 / Vault', 'Secrets / Vault'),
WorkspaceDestination.secrets => appText('密钥', 'Secrets'),
WorkspaceDestination.aiGateway => 'AI Gateway',
WorkspaceDestination.settings => appText('设置', 'Settings'),
WorkspaceDestination.account => appText('账户', 'Account'),

View File

@ -9,7 +9,7 @@ void main() {
'ModulesPage switches connectors tab and routes module actions to settings',
(WidgetTester tester) async {
final controller = await createTestController(tester);
controller.navigateTo(WorkspaceDestination.modules);
controller.navigateTo(WorkspaceDestination.skills);
await pumpPage(
tester,