diff --git a/it0_app/lib/core/router/app_router.dart b/it0_app/lib/core/router/app_router.dart index 4b5b5b7..a801dff 100644 --- a/it0_app/lib/core/router/app_router.dart +++ b/it0_app/lib/core/router/app_router.dart @@ -75,20 +75,51 @@ class ScaffoldWithNav extends ConsumerWidget { final Widget child; const ScaffoldWithNav({super.key, required this.child}); + static const _routes = [ + '/dashboard', + '/chat', + '/tasks', + '/alerts', + '/settings', + ]; + + int _selectedIndex(BuildContext context) { + final location = GoRouterState.of(context).uri.path; + for (int i = 0; i < _routes.length; i++) { + if (location.startsWith(_routes[i])) return i; + } + return 0; + } + @override Widget build(BuildContext context, WidgetRef ref) { final unreadCount = ref.watch(unreadNotificationCountProvider); + final currentIndex = _selectedIndex(context); return Scaffold( body: child, bottomNavigationBar: NavigationBar( + selectedIndex: currentIndex, destinations: [ const NavigationDestination( - icon: Icon(Icons.dashboard), label: '仪表盘'), - const NavigationDestination(icon: Icon(Icons.chat), label: '对话'), - const NavigationDestination(icon: Icon(Icons.task), label: '任务'), + icon: Icon(Icons.dashboard_outlined), + selectedIcon: Icon(Icons.dashboard), + label: '仪表盘'), + const NavigationDestination( + icon: Icon(Icons.chat_outlined), + selectedIcon: Icon(Icons.chat), + label: '对话'), + const NavigationDestination( + icon: Icon(Icons.task_outlined), + selectedIcon: Icon(Icons.task), + label: '任务'), NavigationDestination( icon: Badge( + isLabelVisible: unreadCount > 0, + label: Text('$unreadCount'), + child: const Icon(Icons.notifications_outlined), + ), + selectedIcon: Badge( isLabelVisible: unreadCount > 0, label: Text('$unreadCount'), child: const Icon(Icons.notifications), @@ -96,17 +127,14 @@ class ScaffoldWithNav extends ConsumerWidget { label: '告警', ), const NavigationDestination( - icon: Icon(Icons.settings), label: '设置'), + icon: Icon(Icons.settings_outlined), + selectedIcon: Icon(Icons.settings), + label: '设置'), ], onDestinationSelected: (index) { - final routes = [ - '/dashboard', - '/chat', - '/tasks', - '/alerts', - '/settings' - ]; - GoRouter.of(context).go(routes[index]); + if (index != currentIndex) { + GoRouter.of(context).go(_routes[index]); + } }, ), ); diff --git a/it0_app/lib/features/alerts/presentation/pages/alerts_page.dart b/it0_app/lib/features/alerts/presentation/pages/alerts_page.dart index 79f702a..e363fb2 100644 --- a/it0_app/lib/features/alerts/presentation/pages/alerts_page.dart +++ b/it0_app/lib/features/alerts/presentation/pages/alerts_page.dart @@ -60,7 +60,7 @@ class _AlertsPageState extends ConsumerState { return Scaffold( appBar: AppBar( - title: const Text('Alert Center'), + title: const Text('告警中心'), actions: [ IconButton( icon: const Icon(Icons.refresh), @@ -82,8 +82,8 @@ class _AlertsPageState extends ConsumerState { if (filtered.isEmpty) { return const EmptyState( icon: Icons.notifications_off_outlined, - title: 'No alerts', - subtitle: 'All clear — no matching alerts found', + title: '暂无告警', + subtitle: '一切正常 — 没有匹配的告警', ); } return ListView.builder( @@ -110,7 +110,7 @@ class _AlertsPageState extends ConsumerState { color: AppColors.error, size: 48), const SizedBox(height: 12), const Text( - 'Failed to load alerts', + '加载告警失败', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600), ), @@ -126,7 +126,7 @@ class _AlertsPageState extends ConsumerState { onPressed: () => ref.invalidate(alertEventsProvider), icon: const Icon(Icons.refresh), - label: const Text('Retry'), + label: const Text('重试'), ), ], ), @@ -157,7 +157,7 @@ class _AlertsPageState extends ConsumerState { return Padding( padding: const EdgeInsets.only(right: 8), child: FilterChip( - label: Text(s == 'all' ? 'All severity' : s), + label: Text(s == 'all' ? '全部级别' : s), selected: selected, selectedColor: _severityChipColor(s).withOpacity(0.25), checkmarkColor: _severityChipColor(s), @@ -191,7 +191,7 @@ class _AlertsPageState extends ConsumerState { return Padding( padding: const EdgeInsets.only(right: 8), child: FilterChip( - label: Text(s == 'all' ? 'All status' : s), + label: Text(s == 'all' ? '全部状态' : s), selected: selected, selectedColor: AppColors.primary.withOpacity(0.25), checkmarkColor: AppColors.primary, @@ -251,7 +251,7 @@ class _AlertsPageState extends ConsumerState { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text('Alert acknowledged'), + content: Text('告警已确认'), backgroundColor: AppColors.success, ), ); @@ -260,7 +260,7 @@ class _AlertsPageState extends ConsumerState { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Failed to acknowledge: $e'), + content: Text('确认失败: $e'), backgroundColor: AppColors.error, ), ); @@ -376,7 +376,7 @@ class _AlertCard extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.end, children: [ Text( - 'Acknowledge', + '确认', style: TextStyle( color: AppColors.info, fontWeight: FontWeight.w600, diff --git a/it0_app/lib/features/approvals/presentation/pages/approvals_page.dart b/it0_app/lib/features/approvals/presentation/pages/approvals_page.dart index dda0d49..ab024c6 100644 --- a/it0_app/lib/features/approvals/presentation/pages/approvals_page.dart +++ b/it0_app/lib/features/approvals/presentation/pages/approvals_page.dart @@ -77,7 +77,7 @@ class _ApprovalsPageState extends ConsumerState { return Scaffold( appBar: AppBar( - title: const Text('Approval Center'), + title: const Text('审批中心'), actions: [ IconButton( icon: const Icon(Icons.refresh), @@ -94,17 +94,17 @@ class _ApprovalsPageState extends ConsumerState { segments: const [ ButtonSegment( value: _ApprovalFilter.all, - label: Text('All'), + label: Text('全部'), icon: Icon(Icons.list, size: 18), ), ButtonSegment( value: _ApprovalFilter.pending, - label: Text('Pending'), + label: Text('待审批'), icon: Icon(Icons.hourglass_top, size: 18), ), ButtonSegment( value: _ApprovalFilter.expired, - label: Text('Expired'), + label: Text('已过期'), icon: Icon(Icons.timer_off, size: 18), ), ], @@ -138,11 +138,11 @@ class _ApprovalsPageState extends ConsumerState { child: EmptyState( icon: Icons.approval_outlined, title: _filter == _ApprovalFilter.all - ? 'No approvals' - : 'No ${_filter.name} approvals', + ? '暂无审批' + : '没有${_filter.name}审批记录', subtitle: _filter == _ApprovalFilter.all - ? 'Approval requests will appear here' - : 'Try changing the filter', + ? '审批请求将显示在此处' + : '尝试切换筛选条件', ), ), ], @@ -171,7 +171,7 @@ class _ApprovalsPageState extends ConsumerState { color: AppColors.error, size: 48), const SizedBox(height: 12), const Text( - 'Failed to load approvals', + '加载审批失败', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600), ), @@ -186,7 +186,7 @@ class _ApprovalsPageState extends ConsumerState { OutlinedButton.icon( onPressed: () => ref.invalidate(approvalsProvider), icon: const Icon(Icons.refresh), - label: const Text('Retry'), + label: const Text('重试'), ), ], ), @@ -287,13 +287,13 @@ class _ApprovalCardState extends State<_ApprovalCard> { // ---- Countdown display --------------------------------------------------- String get _countdownLabel { - if (_remaining == Duration.zero) return 'Expired'; + if (_remaining == Duration.zero) return '已过期'; final hours = _remaining.inHours; final minutes = _remaining.inMinutes.remainder(60); final seconds = _remaining.inSeconds.remainder(60); - if (hours > 0) return '${hours}h ${minutes}m remaining'; - if (minutes > 0) return '${minutes}m ${seconds}s remaining'; - return '${seconds}s remaining'; + if (hours > 0) return '剩余 ${hours}小时${minutes}分'; + if (minutes > 0) return '剩余 ${minutes}分${seconds}秒'; + return '剩余 ${seconds}秒'; } Color get _countdownColor { @@ -319,8 +319,8 @@ class _ApprovalCardState extends State<_ApprovalCard> { SnackBar( content: Text( action == 'approve' - ? 'Approval granted successfully' - : 'Approval rejected', + ? '审批已通过' + : '审批已拒绝', ), backgroundColor: action == 'approve' ? AppColors.success : AppColors.error, @@ -332,7 +332,7 @@ class _ApprovalCardState extends State<_ApprovalCard> { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Failed to $action: $e'), + content: Text('操作失败: $e'), backgroundColor: AppColors.error, ), ); @@ -434,7 +434,7 @@ class _ApprovalCardState extends State<_ApprovalCard> { strokeWidth: 2, color: AppColors.error), ) : const Icon(Icons.close, size: 18), - label: const Text('Reject'), + label: const Text('拒绝'), style: OutlinedButton.styleFrom( foregroundColor: AppColors.error, side: const BorderSide(color: AppColors.error), @@ -454,7 +454,7 @@ class _ApprovalCardState extends State<_ApprovalCard> { strokeWidth: 2, color: Colors.white), ) : const Icon(Icons.check, size: 18), - label: const Text('Approve'), + label: const Text('通过'), style: FilledButton.styleFrom( backgroundColor: AppColors.success, foregroundColor: Colors.white, diff --git a/it0_app/lib/features/chat/presentation/pages/chat_page.dart b/it0_app/lib/features/chat/presentation/pages/chat_page.dart index 47894cf..2237b11 100644 --- a/it0_app/lib/features/chat/presentation/pages/chat_page.dart +++ b/it0_app/lib/features/chat/presentation/pages/chat_page.dart @@ -103,7 +103,7 @@ class ChatNotifier extends StateNotifier { final taskId = data['taskId'] as String? ?? data['task_id'] as String?; if (sessionId == null) { - state = state.copyWith(isStreaming: false, error: 'No sessionId returned'); + state = state.copyWith(isStreaming: false, error: '未返回 sessionId'); return; } @@ -129,7 +129,7 @@ class ChatNotifier extends StateNotifier { } else if (event == 'error') { state = state.copyWith( isStreaming: false, - error: msg['message'] as String? ?? 'Stream error', + error: msg['message'] as String? ?? '流式传输错误', ); _wsSubscription?.cancel(); } @@ -342,12 +342,12 @@ class _ChatPageState extends ConsumerState with SingleTickerProviderSt return Scaffold( appBar: AppBar( - title: const Text('AI Agent'), + title: const Text('AI 对话'), actions: [ if (chatState.messages.isNotEmpty) IconButton( icon: const Icon(Icons.delete_outline), - tooltip: 'Clear chat', + tooltip: '清空对话', onPressed: () => ref.read(chatMessagesProvider.notifier).clearChat(), ), // Voice input button (TODO 40) @@ -389,7 +389,7 @@ class _ChatPageState extends ConsumerState with SingleTickerProviderSt TextButton( onPressed: () => ref.read(chatMessagesProvider.notifier).clearChat(), - child: const Text('Dismiss'), + child: const Text('关闭'), ), ], ), @@ -404,12 +404,12 @@ class _ChatPageState extends ConsumerState with SingleTickerProviderSt Icon(Icons.smart_toy_outlined, size: 64, color: AppColors.textMuted), const SizedBox(height: 16), Text( - 'Start a conversation with iAgent', + '开始与 iAgent 对话', style: TextStyle(color: AppColors.textSecondary, fontSize: 16), ), const SizedBox(height: 8), Text( - 'Hold the mic button for voice input', + '长按麦克风按钮进行语音输入', style: TextStyle(color: AppColors.textMuted, fontSize: 13), ), ], @@ -454,7 +454,7 @@ class _ChatPageState extends ConsumerState with SingleTickerProviderSt ), const SizedBox(width: 8), Text( - _isListening ? 'Listening...' : 'Transcribing...', + _isListening ? '正在聆听...' : '正在转写...', style: TextStyle( color: _isListening ? AppColors.error : AppColors.primary, ), @@ -463,7 +463,7 @@ class _ChatPageState extends ConsumerState with SingleTickerProviderSt if (_isListening) TextButton( onPressed: () => _stopListening(), - child: const Text('Cancel'), + child: const Text('取消'), ), ], ), @@ -483,7 +483,7 @@ class _ChatPageState extends ConsumerState with SingleTickerProviderSt child: TextField( controller: _messageController, decoration: const InputDecoration( - hintText: 'Ask iAgent...', + hintText: '向 iAgent 提问...', border: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(24)), ), @@ -593,7 +593,7 @@ class _ToolUseCard extends StatelessWidget { Icon(Icons.code, size: 16, color: AppColors.primary), const SizedBox(width: 6), Text( - 'Tool Execution', + '工具执行', style: TextStyle( color: AppColors.primary, fontSize: 12, diff --git a/it0_app/lib/features/dashboard/presentation/pages/dashboard_page.dart b/it0_app/lib/features/dashboard/presentation/pages/dashboard_page.dart index 99f6cc6..8c0c3f1 100644 --- a/it0_app/lib/features/dashboard/presentation/pages/dashboard_page.dart +++ b/it0_app/lib/features/dashboard/presentation/pages/dashboard_page.dart @@ -137,7 +137,7 @@ class DashboardPage extends ConsumerWidget { return Scaffold( appBar: AppBar( - title: const Text('Dashboard'), + title: const Text('仪表盘'), actions: [ IconButton( icon: const Icon(Icons.refresh), @@ -160,7 +160,7 @@ class DashboardPage extends ConsumerWidget { children: [ // ------------- Summary cards row --------------------------------- const Text( - 'Overview', + '概览', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), const SizedBox(height: 12), @@ -179,7 +179,7 @@ class DashboardPage extends ConsumerWidget { const SizedBox(height: 24), // ------------- Recent operations -------------------------------- const Text( - 'Recent Operations', + '最近操作', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), ), const SizedBox(height: 12), @@ -197,14 +197,14 @@ class DashboardPage extends ConsumerWidget { data: (summary) => _SummaryCard( icon: Icons.dns, iconColor: AppColors.success, - title: 'Servers', + title: '服务器', mainValue: '${summary.online}/${summary.total}', - subtitle: 'online', + subtitle: '在线', details: [ if (summary.warning > 0) - _DetailRow(label: 'Warning', count: summary.warning, color: AppColors.warning), + _DetailRow(label: '告警', count: summary.warning, color: AppColors.warning), if (summary.offline > 0) - _DetailRow(label: 'Offline', count: summary.offline, color: AppColors.error), + _DetailRow(label: '离线', count: summary.offline, color: AppColors.error), ], ), loading: () => const _SummaryCardLoading(), @@ -217,16 +217,16 @@ class DashboardPage extends ConsumerWidget { data: (summary) => _SummaryCard( icon: Icons.warning_amber_rounded, iconColor: summary.critical > 0 ? AppColors.error : AppColors.warning, - title: 'Alerts', + title: '告警', mainValue: '${summary.total}', - subtitle: 'active', + subtitle: '活跃', details: [ if (summary.critical > 0) - _DetailRow(label: 'Critical', count: summary.critical, color: AppColors.error), + _DetailRow(label: '严重', count: summary.critical, color: AppColors.error), if (summary.warning > 0) - _DetailRow(label: 'Warning', count: summary.warning, color: AppColors.warning), + _DetailRow(label: '警告', count: summary.warning, color: AppColors.warning), if (summary.info > 0) - _DetailRow(label: 'Info', count: summary.info, color: AppColors.info), + _DetailRow(label: '信息', count: summary.info, color: AppColors.info), ], ), loading: () => const _SummaryCardLoading(), @@ -242,11 +242,11 @@ class DashboardPage extends ConsumerWidget { return _SummaryCard( icon: Icons.assignment, iconColor: AppColors.info, - title: 'Tasks', + title: '任务', mainValue: '$running', - subtitle: 'running', + subtitle: '运行中', details: [ - _DetailRow(label: 'Recent', count: tasks.length, color: AppColors.textSecondary), + _DetailRow(label: '最近', count: tasks.length, color: AppColors.textSecondary), ], ); }, @@ -263,8 +263,8 @@ class DashboardPage extends ConsumerWidget { if (tasks.isEmpty) { return const EmptyState( icon: Icons.assignment_outlined, - title: 'No recent operations', - subtitle: 'Operations tasks will appear here', + title: '暂无操作记录', + subtitle: '操作任务将显示在此处', ); } return Column( @@ -297,7 +297,7 @@ class DashboardPage extends ConsumerWidget { error: (e, _) => Center( child: Padding( padding: const EdgeInsets.all(32), - child: Text('Failed to load: $e', style: const TextStyle(color: AppColors.error)), + child: Text('加载失败: $e', style: const TextStyle(color: AppColors.error)), ), ), ); diff --git a/it0_app/lib/features/servers/presentation/pages/servers_page.dart b/it0_app/lib/features/servers/presentation/pages/servers_page.dart index 446fd6b..2c52944 100644 --- a/it0_app/lib/features/servers/presentation/pages/servers_page.dart +++ b/it0_app/lib/features/servers/presentation/pages/servers_page.dart @@ -47,7 +47,7 @@ class _ServersPageState extends ConsumerState { return Scaffold( appBar: AppBar( - title: const Text('Servers'), + title: const Text('服务器'), actions: [ IconButton( icon: const Icon(Icons.refresh), @@ -69,8 +69,8 @@ class _ServersPageState extends ConsumerState { if (filtered.isEmpty) { return const EmptyState( icon: Icons.dns_outlined, - title: 'No servers found', - subtitle: 'No servers match the current filter', + title: '未找到服务器', + subtitle: '没有匹配当前筛选条件的服务器', ); } return ListView.builder( @@ -97,7 +97,7 @@ class _ServersPageState extends ConsumerState { color: AppColors.error, size: 48), const SizedBox(height: 12), const Text( - 'Failed to load servers', + '加载服务器失败', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600), ), @@ -112,7 +112,7 @@ class _ServersPageState extends ConsumerState { OutlinedButton.icon( onPressed: () => ref.invalidate(serversProvider), icon: const Icon(Icons.refresh), - label: const Text('Retry'), + label: const Text('重试'), ), ], ), @@ -138,7 +138,7 @@ class _ServersPageState extends ConsumerState { return Padding( padding: const EdgeInsets.only(right: 8), child: FilterChip( - label: Text(env == 'all' ? 'All' : env), + label: Text(env == 'all' ? '全部' : env), selected: selected, onSelected: (_) => setState(() => _envFilter = env), selectedColor: _envColor(env).withOpacity(0.25), @@ -254,17 +254,17 @@ class _ServersPageState extends ConsumerState { const SizedBox(height: 20), // Detail rows - _DetailRow(label: 'IP Address', value: ip), - if (os.isNotEmpty) _DetailRow(label: 'OS', value: os), + _DetailRow(label: 'IP 地址', value: ip), + if (os.isNotEmpty) _DetailRow(label: '操作系统', value: os), if (cpu.isNotEmpty) _DetailRow(label: 'CPU', value: cpu), if (memory.isNotEmpty) - _DetailRow(label: 'Memory', value: memory), + _DetailRow(label: '内存', value: memory), if (region.isNotEmpty) - _DetailRow(label: 'Region', value: region), + _DetailRow(label: '区域', value: region), if (provider.isNotEmpty) - _DetailRow(label: 'Provider', value: provider), + _DetailRow(label: '云厂商', value: provider), if (createdAt.isNotEmpty) - _DetailRow(label: 'Created', value: createdAt), + _DetailRow(label: '创建时间', value: createdAt), ], ), ), diff --git a/it0_app/lib/features/settings/presentation/pages/settings_page.dart b/it0_app/lib/features/settings/presentation/pages/settings_page.dart index cb701d5..291cf5b 100644 --- a/it0_app/lib/features/settings/presentation/pages/settings_page.dart +++ b/it0_app/lib/features/settings/presentation/pages/settings_page.dart @@ -26,186 +26,288 @@ class _SettingsPageState extends ConsumerState { final settings = ref.watch(settingsProvider); final profile = ref.watch(accountProfileProvider); final theme = Theme.of(context); - final cardColor = theme.cardTheme.color ?? AppColors.surface; + final isDark = theme.brightness == Brightness.dark; + final cardColor = isDark ? AppColors.surface : Colors.white; + final subtitleColor = isDark ? AppColors.textSecondary : Colors.grey[600]; return Scaffold( - appBar: AppBar( - title: const Text('设置'), - ), + appBar: AppBar(title: const Text('设置')), body: ListView( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), children: [ - // --- Profile Section --- - _SectionHeader(title: '个人信息'), - const SizedBox(height: 8), - Card( - color: cardColor, - child: Column( - children: [ - ListTile( - leading: CircleAvatar( - backgroundColor: theme.colorScheme.primary, - child: Text( - profile.displayName.isNotEmpty - ? profile.displayName[0].toUpperCase() - : '?', - style: const TextStyle( - color: Colors.white, fontWeight: FontWeight.bold), - ), - ), - title: Text( - profile.displayName.isNotEmpty - ? profile.displayName - : '加载中...', - ), - subtitle: Text(profile.email), - trailing: IconButton( - icon: const Icon(Icons.edit), - onPressed: () => _showEditNameDialog(profile.displayName), - ), - ), - const Divider(height: 1), - ListTile( - leading: const Icon(Icons.lock_outline), - title: const Text('修改密码'), - trailing: const Icon(Icons.chevron_right), - onTap: _showChangePasswordSheet, - ), - ], - ), - ), - const SizedBox(height: 20), + // ===== Profile Card (large, like WeChat/Feishu) ===== + _buildProfileCard(profile, theme, cardColor, subtitleColor), + const SizedBox(height: 24), - // --- Appearance Section --- - _SectionHeader(title: '外观'), - const SizedBox(height: 8), - Card( - color: cardColor, - child: Padding( - padding: const EdgeInsets.all(16), - child: SegmentedButton( - segments: const [ - ButtonSegment( - value: ThemeMode.dark, - label: Text('深色'), - icon: Icon(Icons.dark_mode)), - ButtonSegment( - value: ThemeMode.light, - label: Text('浅色'), - icon: Icon(Icons.light_mode)), - ButtonSegment( - value: ThemeMode.system, - label: Text('跟随系统'), - icon: Icon(Icons.settings_brightness)), - ], - selected: {settings.themeMode}, - onSelectionChanged: (modes) { - ref - .read(settingsProvider.notifier) - .setThemeMode(modes.first); - }, + // ===== General Group ===== + _SettingsGroup( + cardColor: cardColor, + children: [ + _SettingsRow( + icon: Icons.palette_outlined, + iconBg: const Color(0xFF6366F1), + title: '外观主题', + trailing: _ThemeLabel(mode: settings.themeMode), + onTap: () => _showThemePicker(settings.themeMode), ), - ), + _SettingsRow( + icon: Icons.language, + iconBg: const Color(0xFF3B82F6), + title: '语言', + trailing: Text('简体中文', + style: TextStyle(color: subtitleColor, fontSize: 14)), + ), + ], ), - const SizedBox(height: 20), + const SizedBox(height: 24), - // --- Notifications Section --- - _SectionHeader(title: '通知'), - const SizedBox(height: 8), - Card( - color: cardColor, - child: Column( - children: [ - SwitchListTile( - secondary: const Icon(Icons.notifications_outlined), - title: const Text('推送通知'), - value: settings.notificationsEnabled, - onChanged: (v) { - ref - .read(settingsProvider.notifier) - .setNotificationsEnabled(v); - }, - ), - const Divider(height: 1), - SwitchListTile( - secondary: const Icon(Icons.volume_up_outlined), - title: const Text('提示音'), - value: settings.soundEnabled, - onChanged: settings.notificationsEnabled - ? (v) => ref - .read(settingsProvider.notifier) - .setSoundEnabled(v) - : null, - ), - const Divider(height: 1), - SwitchListTile( - secondary: const Icon(Icons.vibration), - title: const Text('震动反馈'), - value: settings.hapticFeedback, - onChanged: settings.notificationsEnabled - ? (v) => ref - .read(settingsProvider.notifier) - .setHapticFeedback(v) - : null, - ), - ], - ), + // ===== Notifications Group ===== + _SettingsGroup( + cardColor: cardColor, + children: [ + _SettingsToggleRow( + icon: Icons.notifications_outlined, + iconBg: const Color(0xFFEF4444), + title: '推送通知', + value: settings.notificationsEnabled, + onChanged: (v) => + ref.read(settingsProvider.notifier).setNotificationsEnabled(v), + ), + _SettingsToggleRow( + icon: Icons.volume_up_outlined, + iconBg: const Color(0xFFF59E0B), + title: '提示音', + value: settings.soundEnabled, + onChanged: settings.notificationsEnabled + ? (v) => + ref.read(settingsProvider.notifier).setSoundEnabled(v) + : null, + ), + _SettingsToggleRow( + icon: Icons.vibration, + iconBg: const Color(0xFF22C55E), + title: '震动反馈', + value: settings.hapticFeedback, + onChanged: settings.notificationsEnabled + ? (v) => + ref.read(settingsProvider.notifier).setHapticFeedback(v) + : null, + ), + ], ), - const SizedBox(height: 20), + const SizedBox(height: 24), - // --- About Section --- - _SectionHeader(title: '关于'), - const SizedBox(height: 8), - Card( - color: cardColor, - child: Column( - children: [ - const ListTile( - leading: Icon(Icons.info_outline), - title: Text('应用版本'), - subtitle: Text('iAgent v1.0.0'), - ), - if (settings.selectedTenantName != null) ...[ - const Divider(height: 1), - ListTile( - leading: const Icon(Icons.business), - title: const Text('租户'), - subtitle: Text(settings.selectedTenantName!), - ), - ], - ], - ), + // ===== Security Group ===== + _SettingsGroup( + cardColor: cardColor, + children: [ + _SettingsRow( + icon: Icons.lock_outline, + iconBg: const Color(0xFF8B5CF6), + title: '修改密码', + onTap: _showChangePasswordSheet, + ), + ], ), - const SizedBox(height: 20), + const SizedBox(height: 24), - // --- Logout --- - Card( - color: cardColor, - child: ListTile( - leading: const Icon(Icons.logout, color: Colors.red), - title: const Text('退出登录', - style: TextStyle(color: Colors.red)), - onTap: () => _confirmLogout(), - ), + // ===== About Group ===== + _SettingsGroup( + cardColor: cardColor, + children: [ + _SettingsRow( + icon: Icons.info_outline, + iconBg: const Color(0xFF64748B), + title: '版本', + trailing: Text('v1.0.0', + style: TextStyle(color: subtitleColor, fontSize: 14)), + ), + if (settings.selectedTenantName != null) + _SettingsRow( + icon: Icons.business_outlined, + iconBg: const Color(0xFF0EA5E9), + title: '租户', + trailing: Text(settings.selectedTenantName!, + style: TextStyle(color: subtitleColor, fontSize: 14)), + ), + ], ), const SizedBox(height: 32), + + // ===== Logout ===== + SizedBox( + width: double.infinity, + child: OutlinedButton( + onPressed: _confirmLogout, + style: OutlinedButton.styleFrom( + foregroundColor: AppColors.error, + side: const BorderSide(color: AppColors.error, width: 1), + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + ), + child: const Text('退出登录', style: TextStyle(fontSize: 16)), + ), + ), + const SizedBox(height: 48), ], ), ); } + // ---- Profile Card (top) --------------------------------------------------- + + Widget _buildProfileCard( + AccountProfile profile, ThemeData theme, Color cardColor, Color? subtitleColor) { + final initial = profile.displayName.isNotEmpty + ? profile.displayName[0].toUpperCase() + : '?'; + + return Card( + color: cardColor, + elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: () => _showEditNameDialog(profile.displayName), + child: Padding( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + // Avatar + Container( + width: 60, + height: 60, + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFF6366F1), Color(0xFF818CF8)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(20), + ), + child: Center( + child: Text( + initial, + style: const TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + const SizedBox(width: 16), + // Name & email + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + profile.displayName.isNotEmpty + ? profile.displayName + : '加载中...', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + fontSize: 18, + ), + ), + const SizedBox(height: 4), + Text( + profile.email.isNotEmpty ? profile.email : ' ', + style: TextStyle(color: subtitleColor, fontSize: 14), + ), + ], + ), + ), + Icon(Icons.chevron_right, color: subtitleColor), + ], + ), + ), + ), + ); + } + + // ---- Theme Picker --------------------------------------------------------- + + void _showThemePicker(ThemeMode current) { + showModalBottomSheet( + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (ctx) { + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 12), + Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: Colors.grey[400], + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(height: 16), + Text('选择主题', + style: Theme.of(ctx).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + )), + const SizedBox(height: 8), + _ThemeOption( + icon: Icons.dark_mode, + label: '深色模式', + selected: current == ThemeMode.dark, + onTap: () { + ref.read(settingsProvider.notifier).setThemeMode(ThemeMode.dark); + Navigator.pop(ctx); + }, + ), + _ThemeOption( + icon: Icons.light_mode, + label: '浅色模式', + selected: current == ThemeMode.light, + onTap: () { + ref.read(settingsProvider.notifier).setThemeMode(ThemeMode.light); + Navigator.pop(ctx); + }, + ), + _ThemeOption( + icon: Icons.settings_brightness, + label: '跟随系统', + selected: current == ThemeMode.system, + onTap: () { + ref.read(settingsProvider.notifier).setThemeMode(ThemeMode.system); + Navigator.pop(ctx); + }, + ), + const SizedBox(height: 16), + ], + ), + ); + }, + ); + } + + // ---- Edit Name Dialog ----------------------------------------------------- + void _showEditNameDialog(String currentName) { final controller = TextEditingController(text: currentName); showDialog( context: context, builder: (ctx) => AlertDialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), title: const Text('修改显示名称'), content: TextField( controller: controller, autofocus: true, - decoration: const InputDecoration( + decoration: InputDecoration( labelText: '显示名称', hintText: '输入新的显示名称', + border: + OutlineInputBorder(borderRadius: BorderRadius.circular(12)), ), ), actions: [ @@ -223,9 +325,7 @@ class _SettingsPageState extends ConsumerState { .updateDisplayName(name); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(success ? '名称已更新' : '更新失败'), - ), + SnackBar(content: Text(success ? '名称已更新' : '更新失败')), ); } }, @@ -236,6 +336,8 @@ class _SettingsPageState extends ConsumerState { ); } + // ---- Change Password Sheet ------------------------------------------------ + void _showChangePasswordSheet() { final currentCtrl = TextEditingController(); final newCtrl = TextEditingController(); @@ -244,38 +346,72 @@ class _SettingsPageState extends ConsumerState { showModalBottomSheet( context: context, isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), builder: (ctx) => Padding( padding: EdgeInsets.fromLTRB( - 24, - 24, - 24, - MediaQuery.of(ctx).viewInsets.bottom + 24, - ), + 24, 24, 24, MediaQuery.of(ctx).viewInsets.bottom + 24), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text('修改密码', style: Theme.of(ctx).textTheme.titleLarge), + Center( + child: Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: Colors.grey[400], + borderRadius: BorderRadius.circular(2), + ), + ), + ), const SizedBox(height: 16), + Text('修改密码', + style: Theme.of(ctx) + .textTheme + .titleLarge + ?.copyWith(fontWeight: FontWeight.w600)), + const SizedBox(height: 20), TextField( controller: currentCtrl, obscureText: true, - decoration: const InputDecoration(labelText: '当前密码'), + decoration: InputDecoration( + labelText: '当前密码', + prefixIcon: const Icon(Icons.lock_outline), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12)), + ), ), - const SizedBox(height: 12), + const SizedBox(height: 14), TextField( controller: newCtrl, obscureText: true, - decoration: const InputDecoration(labelText: '新密码'), + decoration: InputDecoration( + labelText: '新密码', + prefixIcon: const Icon(Icons.lock_reset), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12)), + ), ), - const SizedBox(height: 12), + const SizedBox(height: 14), TextField( controller: confirmCtrl, obscureText: true, - decoration: const InputDecoration(labelText: '确认新密码'), + decoration: InputDecoration( + labelText: '确认新密码', + prefixIcon: const Icon(Icons.lock_reset), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12)), + ), ), - const SizedBox(height: 20), + const SizedBox(height: 24), FilledButton( + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + ), onPressed: () async { if (newCtrl.text != confirmCtrl.text) { ScaffoldMessenger.of(context).showSnackBar( @@ -299,16 +435,14 @@ class _SettingsPageState extends ConsumerState { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text( - result.success - ? '密码已修改' - : result.message ?? '修改失败', - ), + content: Text(result.success + ? '密码已修改' + : result.message ?? '修改失败'), ), ); } }, - child: const Text('确认修改'), + child: const Text('确认修改', style: TextStyle(fontSize: 16)), ), ], ), @@ -316,10 +450,13 @@ class _SettingsPageState extends ConsumerState { ); } + // ---- Confirm Logout ------------------------------------------------------- + void _confirmLogout() async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), title: const Text('退出登录'), content: const Text('确定要退出登录吗?'), actions: [ @@ -328,6 +465,7 @@ class _SettingsPageState extends ConsumerState { child: const Text('取消'), ), FilledButton( + style: FilledButton.styleFrom(backgroundColor: AppColors.error), onPressed: () => Navigator.of(ctx).pop(true), child: const Text('退出'), ), @@ -336,28 +474,172 @@ class _SettingsPageState extends ConsumerState { ); if (confirmed == true) { await ref.read(authStateProvider.notifier).logout(); - if (mounted) { - context.go('/login'); - } + if (mounted) context.go('/login'); } } } -class _SectionHeader extends StatelessWidget { - final String title; - const _SectionHeader({required this.title}); +// ============================================================================= +// Reusable settings widgets +// ============================================================================= + +/// A group of settings rows wrapped in a rounded card. +class _SettingsGroup extends StatelessWidget { + final Color cardColor; + final List children; + + const _SettingsGroup({required this.cardColor, required this.children}); @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 4), - child: Text( - title, - style: Theme.of(context).textTheme.titleSmall?.copyWith( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.bold, - ), + return Card( + color: cardColor, + elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + clipBehavior: Clip.antiAlias, + child: Column( + children: [ + for (int i = 0; i < children.length; i++) ...[ + children[i], + if (i < children.length - 1) + Divider( + height: 1, + indent: 56, + color: Theme.of(context).dividerColor.withAlpha(80), + ), + ], + ], ), ); } } + +/// A single settings row with icon, title, optional trailing, and tap action. +class _SettingsRow extends StatelessWidget { + final IconData icon; + final Color iconBg; + final String title; + final Widget? trailing; + final VoidCallback? onTap; + + const _SettingsRow({ + required this.icon, + required this.iconBg, + required this.title, + this.trailing, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: iconBg, + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, color: Colors.white, size: 18), + ), + title: Text(title), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (trailing != null) trailing!, + if (onTap != null) ...[ + const SizedBox(width: 4), + Icon(Icons.chevron_right, + size: 20, color: Theme.of(context).hintColor), + ], + ], + ), + onTap: onTap, + ); + } +} + +/// A settings row with a switch toggle. +class _SettingsToggleRow extends StatelessWidget { + final IconData icon; + final Color iconBg; + final String title; + final bool value; + final ValueChanged? onChanged; + + const _SettingsToggleRow({ + required this.icon, + required this.iconBg, + required this.title, + required this.value, + this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return SwitchListTile( + secondary: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: iconBg, + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, color: Colors.white, size: 18), + ), + title: Text(title), + value: value, + onChanged: onChanged, + ); + } +} + +/// Label showing the current theme mode. +class _ThemeLabel extends StatelessWidget { + final ThemeMode mode; + const _ThemeLabel({required this.mode}); + + @override + Widget build(BuildContext context) { + final label = switch (mode) { + ThemeMode.dark => '深色', + ThemeMode.light => '浅色', + ThemeMode.system => '跟随系统', + }; + return Text(label, + style: TextStyle(color: Theme.of(context).hintColor, fontSize: 14)); + } +} + +/// Theme option in the bottom sheet picker. +class _ThemeOption extends StatelessWidget { + final IconData icon; + final String label; + final bool selected; + final VoidCallback onTap; + + const _ThemeOption({ + required this.icon, + required this.label, + required this.selected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: Icon(icon, + color: selected ? Theme.of(context).colorScheme.primary : null), + title: Text(label, + style: TextStyle( + fontWeight: selected ? FontWeight.w600 : FontWeight.normal, + color: selected ? Theme.of(context).colorScheme.primary : null, + )), + trailing: selected + ? Icon(Icons.check_circle, + color: Theme.of(context).colorScheme.primary) + : null, + onTap: onTap, + ); + } +} diff --git a/it0_app/lib/features/standing_orders/presentation/pages/standing_orders_page.dart b/it0_app/lib/features/standing_orders/presentation/pages/standing_orders_page.dart index 5ce4b1e..3337b2a 100644 --- a/it0_app/lib/features/standing_orders/presentation/pages/standing_orders_page.dart +++ b/it0_app/lib/features/standing_orders/presentation/pages/standing_orders_page.dart @@ -39,7 +39,7 @@ class StandingOrdersPage extends ConsumerWidget { return Scaffold( appBar: AppBar( - title: const Text('Standing Orders'), + title: const Text('常驻指令'), actions: [ IconButton( icon: const Icon(Icons.refresh), @@ -54,8 +54,8 @@ class StandingOrdersPage extends ConsumerWidget { if (orders.isEmpty) { return const EmptyState( icon: Icons.rule_outlined, - title: 'No standing orders', - subtitle: 'Standing orders will appear here once configured', + title: '暂无常驻指令', + subtitle: '配置后常驻指令将显示在此处', ); } return ListView.builder( @@ -78,7 +78,7 @@ class StandingOrdersPage extends ConsumerWidget { color: AppColors.error, size: 48), const SizedBox(height: 12), const Text( - 'Failed to load standing orders', + '加载常驻指令失败', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), ), @@ -94,7 +94,7 @@ class StandingOrdersPage extends ConsumerWidget { onPressed: () => ref.invalidate(standingOrdersProvider), icon: const Icon(Icons.refresh), - label: const Text('Retry'), + label: const Text('重试'), ), ], ), @@ -145,7 +145,7 @@ class _StandingOrderCardState extends ConsumerState<_StandingOrderCard> { final lastExecLabel = lastExecution != null ? DateFormatter.timeAgo(DateTime.parse(lastExecution)) - : 'Never'; + : '从未执行'; return Card( color: AppColors.surface, @@ -236,7 +236,7 @@ class _StandingOrderCardState extends ConsumerState<_StandingOrderCard> { childrenPadding: const EdgeInsets.fromLTRB(14, 0, 14, 10), title: Text( - 'Execution History (${executions.length})', + '执行历史 (${executions.length})', style: const TextStyle( fontSize: 13, color: AppColors.textSecondary), ), @@ -264,7 +264,7 @@ class _StandingOrderCardState extends ConsumerState<_StandingOrderCard> { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Failed to update status: $e'), + content: Text('更新状态失败: $e'), backgroundColor: AppColors.error, ), ); diff --git a/it0_app/lib/features/tasks/presentation/pages/tasks_page.dart b/it0_app/lib/features/tasks/presentation/pages/tasks_page.dart index 6cc804f..4094386 100644 --- a/it0_app/lib/features/tasks/presentation/pages/tasks_page.dart +++ b/it0_app/lib/features/tasks/presentation/pages/tasks_page.dart @@ -52,7 +52,7 @@ class TasksPage extends ConsumerWidget { return Scaffold( appBar: AppBar( - title: const Text('Operations Tasks'), + title: const Text('运维任务'), actions: [ IconButton( icon: const Icon(Icons.refresh), @@ -67,8 +67,8 @@ class TasksPage extends ConsumerWidget { if (tasks.isEmpty) { return const EmptyState( icon: Icons.assignment_outlined, - title: 'No tasks yet', - subtitle: 'Tap + to create a new task', + title: '暂无任务', + subtitle: '点击 + 创建新任务', ); } return ListView.builder( @@ -90,7 +90,7 @@ class TasksPage extends ConsumerWidget { const Icon(Icons.error_outline, color: AppColors.error, size: 48), const SizedBox(height: 12), Text( - 'Failed to load tasks', + '加载任务失败', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), ), const SizedBox(height: 6), @@ -103,7 +103,7 @@ class TasksPage extends ConsumerWidget { OutlinedButton.icon( onPressed: () => ref.invalidate(tasksProvider), icon: const Icon(Icons.refresh), - label: const Text('Retry'), + label: const Text('重试'), ), ], ), @@ -291,7 +291,7 @@ class _CreateTaskSheetState extends State<_CreateTaskSheet> { ), const SizedBox(height: 16), const Text( - 'New Task', + '新建任务', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), const SizedBox(height: 20), @@ -300,11 +300,11 @@ class _CreateTaskSheetState extends State<_CreateTaskSheet> { TextFormField( controller: _titleController, decoration: const InputDecoration( - labelText: 'Title', - hintText: 'e.g. Restart nginx on web-01', + labelText: '标题', + hintText: '例如: 重启 web-01 的 nginx', border: OutlineInputBorder(), ), - validator: (v) => (v == null || v.trim().isEmpty) ? 'Title is required' : null, + validator: (v) => (v == null || v.trim().isEmpty) ? '请输入标题' : null, textInputAction: TextInputAction.next, ), const SizedBox(height: 14), @@ -313,8 +313,8 @@ class _CreateTaskSheetState extends State<_CreateTaskSheet> { TextFormField( controller: _descriptionController, decoration: const InputDecoration( - labelText: 'Description', - hintText: 'Optional details...', + labelText: '描述', + hintText: '可选详情...', border: OutlineInputBorder(), ), maxLines: 3, @@ -326,7 +326,7 @@ class _CreateTaskSheetState extends State<_CreateTaskSheet> { DropdownButtonFormField( value: _priority, decoration: const InputDecoration( - labelText: 'Priority', + labelText: '优先级', border: OutlineInputBorder(), ), items: _priorities @@ -348,11 +348,11 @@ class _CreateTaskSheetState extends State<_CreateTaskSheet> { return DropdownButtonFormField( value: _selectedServerId, decoration: const InputDecoration( - labelText: 'Server (optional)', + labelText: '服务器(可选)', border: OutlineInputBorder(), ), items: [ - const DropdownMenuItem(value: null, child: Text('None')), + const DropdownMenuItem(value: null, child: Text('不指定')), ...servers.map((s) { final id = s['id']?.toString() ?? ''; final name = s['hostname'] as String? ?? @@ -378,7 +378,7 @@ class _CreateTaskSheetState extends State<_CreateTaskSheet> { height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), ) - : const Text('Create Task'), + : const Text('创建任务'), ), const SizedBox(height: 8), ], @@ -413,7 +413,7 @@ class _CreateTaskSheetState extends State<_CreateTaskSheet> { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Failed to create task: $e'), + content: Text('创建任务失败: $e'), backgroundColor: AppColors.error, ), ); diff --git a/it0_app/lib/features/terminal/presentation/pages/terminal_page.dart b/it0_app/lib/features/terminal/presentation/pages/terminal_page.dart index 8378301..744dd14 100644 --- a/it0_app/lib/features/terminal/presentation/pages/terminal_page.dart +++ b/it0_app/lib/features/terminal/presentation/pages/terminal_page.dart @@ -56,8 +56,8 @@ class _TerminalPageState extends ConsumerState { super.initState(); _terminal = Terminal(maxLines: 10000); _terminal.onOutput = _onTerminalInput; - _terminal.write('iAgent Remote Terminal\r\n'); - _terminal.write('Select a server and press Connect to begin.\r\n'); + _terminal.write('iAgent 远程终端\r\n'); + _terminal.write('请选择服务器并点击连接。\r\n'); } @override @@ -70,12 +70,12 @@ class _TerminalPageState extends ConsumerState { Future _connect() async { if (_selectedServerId == null || _selectedServerId!.isEmpty) { - _terminal.write('\r\n\x1B[33m[!] Please select a server first.\x1B[0m\r\n'); + _terminal.write('\r\n\x1B[33m[!] 请先选择服务器\x1B[0m\r\n'); return; } setState(() => _status = _ConnectionStatus.connecting); - _terminal.write('\r\n\x1B[33m[*] Connecting to server $_selectedServerId...\x1B[0m\r\n'); + _terminal.write('\r\n\x1B[33m[*] 正在连接服务器 $_selectedServerId...\x1B[0m\r\n'); try { final config = ref.read(appConfigProvider); @@ -106,12 +106,12 @@ class _TerminalPageState extends ConsumerState { if (mounted) { setState(() => _status = _ConnectionStatus.connected); - _terminal.write('\x1B[32m[+] Connected.\x1B[0m\r\n'); + _terminal.write('\x1B[32m[+] 已连接\x1B[0m\r\n'); } } catch (e) { if (mounted) { setState(() => _status = _ConnectionStatus.disconnected); - _terminal.write('\x1B[31m[-] Connection failed: $e\x1B[0m\r\n'); + _terminal.write('\x1B[31m[-] 连接失败: $e\x1B[0m\r\n'); } } } @@ -148,14 +148,14 @@ class _TerminalPageState extends ConsumerState { void _onWsDone() { if (mounted) { setState(() => _status = _ConnectionStatus.disconnected); - _terminal.write('\r\n\x1B[33m[*] Connection closed.\x1B[0m\r\n'); + _terminal.write('\r\n\x1B[33m[*] 连接已关闭\x1B[0m\r\n'); } } void _onWsError(dynamic error) { if (mounted) { setState(() => _status = _ConnectionStatus.disconnected); - _terminal.write('\r\n\x1B[31m[-] WebSocket error: $error\x1B[0m\r\n'); + _terminal.write('\r\n\x1B[31m[-] WebSocket 错误: $error\x1B[0m\r\n'); } } @@ -185,11 +185,11 @@ class _TerminalPageState extends ConsumerState { String get _statusLabel { switch (_status) { case _ConnectionStatus.connected: - return 'Connected'; + return '已连接'; case _ConnectionStatus.connecting: - return 'Connecting...'; + return '连接中...'; case _ConnectionStatus.disconnected: - return 'Disconnected'; + return '未连接'; } } @@ -201,7 +201,7 @@ class _TerminalPageState extends ConsumerState { return Scaffold( appBar: AppBar( - title: const Text('Remote Terminal'), + title: const Text('远程终端'), actions: [ // Connection status indicator Padding( @@ -242,7 +242,7 @@ class _TerminalPageState extends ConsumerState { tooltip: 'Disconnect', onPressed: () { _disconnect(); - _terminal.write('\r\n\x1B[33m[*] Disconnected by user.\x1B[0m\r\n'); + _terminal.write('\r\n\x1B[33m[*] 已断开连接\x1B[0m\r\n'); }, ), ], @@ -262,7 +262,7 @@ class _TerminalPageState extends ConsumerState { data: (servers) { if (servers.isEmpty) { return const Text( - 'No servers available', + '暂无可用服务器', style: TextStyle( color: AppColors.textMuted, fontSize: 14, @@ -273,7 +273,7 @@ class _TerminalPageState extends ConsumerState { child: DropdownButton( value: _selectedServerId, hint: const Text( - 'Select a server...', + '选择服务器...', style: TextStyle( color: AppColors.textMuted, fontSize: 14, @@ -317,7 +317,7 @@ class _TerminalPageState extends ConsumerState { const SizedBox(width: 6), Expanded( child: Text( - 'Failed to load servers', + '加载服务器失败', style: const TextStyle( color: AppColors.error, fontSize: 13, @@ -344,7 +344,7 @@ class _TerminalPageState extends ConsumerState { ? () { _disconnect(); _terminal.write( - '\r\n\x1B[33m[*] Disconnected by user.\x1B[0m\r\n'); + '\r\n\x1B[33m[*] 已断开连接\x1B[0m\r\n'); } : _connect, icon: Icon( @@ -355,10 +355,10 @@ class _TerminalPageState extends ConsumerState { ), label: Text( _status == _ConnectionStatus.connected - ? 'Disconnect' + ? '断开' : _status == _ConnectionStatus.connecting - ? 'Connecting...' - : 'Connect', + ? '连接中...' + : '连接', style: const TextStyle(fontSize: 13), ), ),