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

505 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 'package:it0_app/l10n/app_localizations.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/widgets/robot_avatar.dart';
import '../../../chat/presentation/providers/chat_providers.dart';
import '../../../notifications/presentation/providers/notification_providers.dart';
import '../../../settings/presentation/providers/settings_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),
Text(
AppLocalizations.of(context).homeSubtitle,
style: const 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 ─────────────────────────────
_SectionHeader(title: AppLocalizations.of(context).officialAgentsSection),
const SizedBox(height: 12),
const _OfficialAgentsRow(),
const SizedBox(height: 20),
// ── Guide if nothing created yet ─────────────────────────
_SectionHeader(title: AppLocalizations.of(context).myAgentsSection),
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: [
Text(
AppLocalizations.of(context).appTitle,
style: const 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();
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context);
final agents = [
_AgentCard(
name: l.officialAgent1Name,
desc: l.officialAgent1Desc,
icon: Icons.dns_outlined,
color: const Color(0xFF6366F1),
isOfficial: true,
),
_AgentCard(
name: l.officialAgent2Name,
desc: l.officialAgent2Desc,
icon: Icons.security_outlined,
color: const Color(0xFF22C55E),
isOfficial: true,
),
_AgentCard(
name: l.officialAgent3Name,
desc: l.officialAgent3Desc,
icon: Icons.storage_outlined,
color: const Color(0xFF0EA5E9),
isOfficial: true,
),
];
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(
AppLocalizations.of(context).officialBadge,
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),
Text(
AppLocalizations.of(context).noOwnAgentsTitle,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
),
const SizedBox(height: 6),
Text(
AppLocalizations.of(context).noOwnAgentsDesc,
style: const TextStyle(
fontSize: 13,
color: AppColors.textSecondary,
height: 1.5,
),
textAlign: TextAlign.center,
),
],
),
);
}
}
// ---------------------------------------------------------------------------
// Quick tips card
// ---------------------------------------------------------------------------
class _QuickTipsCard extends StatelessWidget {
const _QuickTipsCard();
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context);
final tips = [
l.quickTip1,
l.quickTip2,
l.quickTip3,
l.quickTip4,
];
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l.quickTipsHeader,
style: const 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,
),
),
),
),
],
),
);
}
}