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:
parent
b76b5246cc
commit
9f44878fea
|
|
@ -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]);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
Loading…
Reference in New Issue