fix: unify all pages to Chinese + fix bottom nav selected state

1. 所有页面英文文本统一替换为中文(仪表盘、对话、任务、告警、
   服务器、常驻指令、审批、终端、设置)
2. 底部导航栏添加 selectedIndex 追踪当前路由,点击后正确高亮
3. 导航图标添加 outlined/filled 选中态区分
4. 设置页重构为大厂风格(圆角图标分组 + 主题底部弹窗选择)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-02-23 01:59:09 -08:00
parent b76b5246cc
commit 9f44878fea
10 changed files with 621 additions and 311 deletions

View File

@ -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]);
}
},
),
);

View File

@ -60,7 +60,7 @@ class _AlertsPageState extends ConsumerState<AlertsPage> {
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<AlertsPage> {
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<AlertsPage> {
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<AlertsPage> {
onPressed: () =>
ref.invalidate(alertEventsProvider),
icon: const Icon(Icons.refresh),
label: const Text('Retry'),
label: const Text('重试'),
),
],
),
@ -157,7 +157,7 @@ class _AlertsPageState extends ConsumerState<AlertsPage> {
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<AlertsPage> {
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<AlertsPage> {
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<AlertsPage> {
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,

View File

@ -77,7 +77,7 @@ class _ApprovalsPageState extends ConsumerState<ApprovalsPage> {
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<ApprovalsPage> {
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<ApprovalsPage> {
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<ApprovalsPage> {
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<ApprovalsPage> {
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,

View File

@ -103,7 +103,7 @@ class ChatNotifier extends StateNotifier<ChatState> {
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<ChatState> {
} 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<ChatPage> 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<ChatPage> with SingleTickerProviderSt
TextButton(
onPressed: () =>
ref.read(chatMessagesProvider.notifier).clearChat(),
child: const Text('Dismiss'),
child: const Text('关闭'),
),
],
),
@ -404,12 +404,12 @@ class _ChatPageState extends ConsumerState<ChatPage> 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<ChatPage> 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<ChatPage> with SingleTickerProviderSt
if (_isListening)
TextButton(
onPressed: () => _stopListening(),
child: const Text('Cancel'),
child: const Text('取消'),
),
],
),
@ -483,7 +483,7 @@ class _ChatPageState extends ConsumerState<ChatPage> 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,

View File

@ -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)),
),
),
);

View File

@ -47,7 +47,7 @@ class _ServersPageState extends ConsumerState<ServersPage> {
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<ServersPage> {
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<ServersPage> {
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<ServersPage> {
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<ServersPage> {
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<ServersPage> {
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),
],
),
),

View File

@ -26,186 +26,288 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
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<ThemeMode>(
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<SettingsPage> {
.updateDisplayName(name);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(success ? '名称已更新' : '更新失败'),
),
SnackBar(content: Text(success ? '名称已更新' : '更新失败')),
);
}
},
@ -236,6 +336,8 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
);
}
// ---- Change Password Sheet ------------------------------------------------
void _showChangePasswordSheet() {
final currentCtrl = TextEditingController();
final newCtrl = TextEditingController();
@ -244,38 +346,72 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
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<SettingsPage> {
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<SettingsPage> {
);
}
// ---- Confirm Logout -------------------------------------------------------
void _confirmLogout() async {
final confirmed = await showDialog<bool>(
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<SettingsPage> {
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<SettingsPage> {
);
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<Widget> 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<bool>? 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,
);
}
}

View File

@ -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,
),
);

View File

@ -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<String>(
value: _priority,
decoration: const InputDecoration(
labelText: 'Priority',
labelText: '优先级',
border: OutlineInputBorder(),
),
items: _priorities
@ -348,11 +348,11 @@ class _CreateTaskSheetState extends State<_CreateTaskSheet> {
return DropdownButtonFormField<String?>(
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,
),
);

View File

@ -56,8 +56,8 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
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<TerminalPage> {
Future<void> _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<TerminalPage> {
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<TerminalPage> {
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<TerminalPage> {
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<TerminalPage> {
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<TerminalPage> {
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<TerminalPage> {
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<TerminalPage> {
child: DropdownButton<String>(
value: _selectedServerId,
hint: const Text(
'Select a server...',
'选择服务器...',
style: TextStyle(
color: AppColors.textMuted,
fontSize: 14,
@ -317,7 +317,7 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
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<TerminalPage> {
? () {
_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<TerminalPage> {
),
label: Text(
_status == _ConnectionStatus.connected
? 'Disconnect'
? '断开'
: _status == _ConnectionStatus.connecting
? 'Connecting...'
: 'Connect',
? '连接中...'
: '连接',
style: const TextStyle(fontSize: 13),
),
),