it0/it0_app/lib/features/home/presentation/pages/home_page.dart

502 lines
16 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/widgets/robot_avatar.dart';
import '../../../auth/data/providers/auth_provider.dart';
import '../../../chat/presentation/providers/chat_providers.dart';
import '../../../notifications/presentation/providers/notification_providers.dart';
// ---------------------------------------------------------------------------
// Home page — "主页" Tab
// Shows active agent cards + recent activity + welcome guide
// ---------------------------------------------------------------------------
class HomePage extends ConsumerWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final profile = ref.watch(accountProfileProvider);
final chatState = ref.watch(chatProvider);
final unreadCount = ref.watch(unreadNotificationCountProvider);
final greeting = _greeting();
final name = profile.displayName.isNotEmpty ? profile.displayName : '用户';
return Scaffold(
backgroundColor: AppColors.background,
body: CustomScrollView(
slivers: [
// ── AppBar ──────────────────────────────────────────────────────
SliverAppBar(
expandedHeight: 120,
pinned: true,
backgroundColor: AppColors.background,
flexibleSpace: FlexibleSpaceBar(
titlePadding: const EdgeInsets.fromLTRB(20, 0, 20, 16),
title: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$greeting$name',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary,
),
),
const SizedBox(height: 2),
const Text(
'iAgent 随时为你服务',
style: TextStyle(
fontSize: 12,
color: AppColors.textSecondary,
),
),
],
),
),
actions: [
if (unreadCount > 0)
Stack(
children: [
const Padding(
padding: EdgeInsets.all(12),
child: Icon(Icons.notifications_outlined,
color: AppColors.textSecondary),
),
Positioned(
right: 8,
top: 8,
child: Container(
width: 16,
height: 16,
decoration: const BoxDecoration(
color: AppColors.error,
shape: BoxShape.circle,
),
child: Center(
child: Text(
'$unreadCount',
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
),
const SizedBox(width: 8),
],
),
SliverPadding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 100),
sliver: SliverList(
delegate: SliverChildListDelegate([
// ── Active agent status card ─────────────────────────────
_AgentStatusCard(chatState: chatState),
const SizedBox(height: 20),
// ── IT0 official agent cards ─────────────────────────────
const _SectionHeader(title: 'IT0 官方智能体'),
const SizedBox(height: 12),
const _OfficialAgentsRow(),
const SizedBox(height: 20),
// ── Guide if nothing created yet ─────────────────────────
const _SectionHeader(title: '我的智能体'),
const SizedBox(height: 12),
const _MyAgentsPlaceholder(),
const SizedBox(height: 20),
// ── Quick tips ───────────────────────────────────────────
const _QuickTipsCard(),
]),
),
),
],
),
);
}
String _greeting() {
final hour = DateTime.now().hour;
if (hour < 6) return '夜深了';
if (hour < 12) return '早上好';
if (hour < 14) return '中午好';
if (hour < 18) return '下午好';
return '晚上好';
}
}
// ---------------------------------------------------------------------------
// Agent status card — shows current iAgent state
// ---------------------------------------------------------------------------
class _AgentStatusCard extends StatelessWidget {
final ChatState chatState;
const _AgentStatusCard({required this.chatState});
@override
Widget build(BuildContext context) {
final isActive = chatState.isStreaming;
final statusText = switch (chatState.agentStatus) {
AgentStatus.idle => '空闲中',
AgentStatus.thinking => '正在思考...',
AgentStatus.executing => '执行指令中...',
AgentStatus.awaitingApproval => '等待审批',
AgentStatus.error => '发生错误',
};
final robotState = switch (chatState.agentStatus) {
AgentStatus.idle => RobotState.idle,
AgentStatus.thinking => RobotState.thinking,
AgentStatus.executing => RobotState.executing,
AgentStatus.awaitingApproval => RobotState.alert,
AgentStatus.error => RobotState.alert,
};
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isActive
? AppColors.primary.withOpacity(0.5)
: AppColors.surfaceLight.withOpacity(0.3),
),
gradient: isActive
? LinearGradient(
colors: [
AppColors.primary.withOpacity(0.08),
AppColors.surface,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
)
: null,
),
child: Row(
children: [
RobotAvatar(state: robotState, size: 64),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'iAgent',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary,
),
),
const SizedBox(height: 4),
Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isActive ? AppColors.success : AppColors.textMuted,
),
),
const SizedBox(width: 6),
Text(
statusText,
style: TextStyle(
fontSize: 13,
color: isActive
? AppColors.success
: AppColors.textSecondary,
),
),
],
),
if (chatState.messages.isNotEmpty) ...[
const SizedBox(height: 6),
Text(
'对话中 · ${chatState.messages.length} 条消息',
style: const TextStyle(
fontSize: 12,
color: AppColors.textMuted,
),
),
],
],
),
),
// Arrow hint
const Icon(Icons.chevron_right, color: AppColors.textMuted),
],
),
);
}
}
// ---------------------------------------------------------------------------
// Section header
// ---------------------------------------------------------------------------
class _SectionHeader extends StatelessWidget {
final String title;
const _SectionHeader({required this.title});
@override
Widget build(BuildContext context) {
return Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
);
}
}
// ---------------------------------------------------------------------------
// Official agents row (horizontal scroll cards)
// ---------------------------------------------------------------------------
class _OfficialAgentsRow extends StatelessWidget {
const _OfficialAgentsRow();
static const _agents = [
_AgentCard(
name: 'iAgent 运维助手',
desc: '服务器管理、SSH 执行、日志分析',
icon: Icons.dns_outlined,
color: Color(0xFF6366F1),
isOfficial: true,
),
_AgentCard(
name: '安全审计助手',
desc: '漏洞扫描、权限审查、合规检查',
icon: Icons.security_outlined,
color: Color(0xFF22C55E),
isOfficial: true,
),
_AgentCard(
name: '数据库巡检',
desc: '慢查询分析、索引优化、备份验证',
icon: Icons.storage_outlined,
color: Color(0xFF0EA5E9),
isOfficial: true,
),
];
@override
Widget build(BuildContext context) {
return SizedBox(
height: 130,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: _agents.length,
separatorBuilder: (_, __) => const SizedBox(width: 12),
itemBuilder: (context, index) => _agents[index],
),
);
}
}
class _AgentCard extends StatelessWidget {
final String name;
final String desc;
final IconData icon;
final Color color;
final bool isOfficial;
const _AgentCard({
required this.name,
required this.desc,
required this.icon,
required this.color,
this.isOfficial = false,
});
@override
Widget build(BuildContext context) {
return Container(
width: 180,
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: color.withOpacity(0.15),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: color, size: 20),
),
if (isOfficial) ...[
const Spacer(),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: color.withOpacity(0.15),
borderRadius: BorderRadius.circular(6),
),
child: Text(
'官方',
style: TextStyle(
fontSize: 10,
color: color,
fontWeight: FontWeight.w600,
),
),
),
],
],
),
const SizedBox(height: 10),
Text(
name,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
desc,
style: const TextStyle(
fontSize: 11,
color: AppColors.textMuted,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
}
// ---------------------------------------------------------------------------
// My agents placeholder — guides user to talk to iAgent
// ---------------------------------------------------------------------------
class _MyAgentsPlaceholder extends StatelessWidget {
const _MyAgentsPlaceholder();
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppColors.primary.withOpacity(0.2),
style: BorderStyle.solid,
),
),
child: Column(
children: [
const Icon(
Icons.add_circle_outline,
size: 40,
color: AppColors.primary,
),
const SizedBox(height: 12),
const Text(
'还没有自己的智能体',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
),
const SizedBox(height: 6),
const Text(
'点击下方机器人按钮,告诉 iAgent\n"帮我创建一个 OpenClaw 智能体"',
style: TextStyle(
fontSize: 13,
color: AppColors.textSecondary,
height: 1.5,
),
textAlign: TextAlign.center,
),
],
),
);
}
}
// ---------------------------------------------------------------------------
// Quick tips card
// ---------------------------------------------------------------------------
class _QuickTipsCard extends StatelessWidget {
const _QuickTipsCard();
static const _tips = [
'💬 "帮我创建一个监控 GitHub Actions 的智能体"',
'🔧 "把我的 OpenClaw 配置导出为 JSON"',
'📊 "分析我的服务器最近7天的负载情况"',
'🛡️ "帮我设置每天凌晨2点自动备份数据库"',
];
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'你可以这样说...',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.textSecondary,
),
),
const SizedBox(height: 10),
..._tips.map(
(tip) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
tip,
style: const TextStyle(
fontSize: 13,
color: AppColors.textMuted,
height: 1.4,
),
),
),
),
],
),
);
}
}