feat: 重构侧边栏导航结构
- 更新 WorkspaceDestination 枚举,添加 skills/nodes/agents/clawHub/aiGateway - 调整侧边栏顺序:助手→任务→技能→节点→代理 | ClawHub→密钥→AI Gateway - 新建 SkillsPage 展示已安装技能列表 - ModulesPage 添加 initialTab 参数支持直接跳转 - 修复 ai_gateway_page 和 claw_hub_page 的编译错误 - 更新测试文件适配新的导航结构
This commit is contained in:
parent
4d2cb683f2
commit
23d75d45fa
@ -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,
|
||||
|
||||
@ -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)),
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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;
|
||||
|
||||
191
lib/features/skills/skills_page.dart
Normal file
191
lib/features/skills/skills_page.dart
Normal 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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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 密码保险箱,安全存储密钥、凭证与审计信息。',
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user