Files
chat/client/flutter/lib/pages/dashboard_page.dart
2026-04-25 16:36:34 +08:00

406 lines
11 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:provider/provider.dart';
import 'package:sales_chat/providers/stats_provider.dart';
import 'package:sales_chat/widgets/user_avatar.dart';
import 'package:sales_chat/theme/app_theme.dart';
/// 看板页面 —— Twitter 风格
class DashboardPage extends StatefulWidget {
const DashboardPage({super.key});
@override
State<DashboardPage> createState() => _DashboardPageState();
}
class _DashboardPageState extends State<DashboardPage> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _loadData());
}
Future<void> _loadData() async {
final statsProvider = context.read<StatsProvider>();
await statsProvider.loadMyStats();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('看板'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadData,
),
],
),
body: Consumer<StatsProvider>(
builder: (context, statsProvider, _) {
if (statsProvider.isLoading && statsProvider.myStats == null) {
return const Center(child: CircularProgressIndicator());
}
final stats = statsProvider.myStats;
return RefreshIndicator(
onRefresh: _loadData,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (stats != null) _buildStatsOverview(context, stats),
const SizedBox(height: 8),
_buildTodayStats(context, stats),
const SizedBox(height: 8),
_buildRankingSection(context, statsProvider),
],
),
),
);
},
),
);
}
/// 统计概览 —— 卡片区块2x2 网格
Widget _buildStatsOverview(BuildContext context, dynamic myStats) {
final total = myStats.total;
return Container(
width: double.infinity,
color: AppTheme.cardBackground,
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'我的统计',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 16),
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
mainAxisSpacing: 16,
crossAxisSpacing: 16,
childAspectRatio: 2.0,
children: [
_StatGridItem(
value: _formatNumber(total.totalInvites),
label: '总邀请数',
icon: Icons.card_giftcard_outlined,
color: AppTheme.primaryColor,
),
_StatGridItem(
value: _formatNumber(total.totalJoins),
label: '总加入数',
icon: Icons.person_add_outlined,
color: AppTheme.successColor,
),
_StatGridItem(
value: '+${myStats.today.invitesCreated}',
label: '今日邀请',
icon: Icons.add_circle_outline,
color: AppTheme.warningColor,
),
_StatGridItem(
value: '+${myStats.today.joins}',
label: '今日加入',
icon: Icons.people,
color: AppTheme.infoColor,
),
],
),
],
),
);
}
/// 今日统计 —— 独立卡片区块
Widget _buildTodayStats(BuildContext context, dynamic myStats) {
if (myStats == null) return const SizedBox.shrink();
final today = myStats.today;
return Container(
width: double.infinity,
color: AppTheme.cardBackground,
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'今日数据',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _TodayStatItem(
value: '${today.invitesCreated}',
label: '新建邀请',
icon: Icons.add_circle_outline,
color: AppTheme.primaryColor,
),
),
Container(
width: 0.5,
height: 40,
color: AppTheme.dividerColor,
),
Expanded(
child: _TodayStatItem(
value: '${today.joins}',
label: '新加入',
icon: Icons.person_add,
color: AppTheme.successColor,
),
),
],
),
],
),
);
}
/// 排行榜区块
Widget _buildRankingSection(BuildContext context, StatsProvider provider) {
return Container(
color: AppTheme.cardBackground,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题行
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'排行榜',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
GestureDetector(
onTap: () => provider.loadRanking(),
child: const Text(
'加载排行',
style: TextStyle(
fontSize: 13,
color: AppTheme.primaryColor,
),
),
),
],
),
),
if (provider.ranking.isEmpty)
Padding(
padding: const EdgeInsets.all(24),
child: Center(
child: Text(
'暂无排行数据',
style: TextStyle(color: AppTheme.textSecondary, fontSize: 14),
),
),
)
else
..._buildRankingItems(provider),
],
),
);
}
/// 构建排行列表项(带分隔线)
List<Widget> _buildRankingItems(StatsProvider provider) {
final rankings = provider.ranking.take(10).toList();
final items = <Widget>[];
for (int i = 0; i < rankings.length; i++) {
final entry = rankings[i];
items.add(Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
// 排名序号
SizedBox(
width: 28,
child: Text(
'${entry.rank}',
style: TextStyle(
fontSize: 16,
fontWeight: i < 3 ? FontWeight.bold : FontWeight.w500,
color: i < 3
? [const Color(0xFFFFC300), const Color(0xFFC0C0C0), const Color(0xFFCD7F32)][i]
: AppTheme.textHint,
),
textAlign: TextAlign.center,
),
),
const SizedBox(width: 12),
// 头像
UserAvatar(
displayName: entry.displayName,
radius: 16,
),
const SizedBox(width: 12),
// 名字
Expanded(
child: Text(
entry.displayName,
style: const TextStyle(
fontSize: 15,
color: AppTheme.textPrimary,
),
),
),
// 加入数
Text(
'${entry.totalJoins} 加入',
style: const TextStyle(
fontSize: 13,
color: AppTheme.textSecondary,
),
),
],
),
));
if (i < rankings.length - 1) {
items.add(const Padding(
padding: EdgeInsets.only(left: 56),
child: Divider(height: 0.5, thickness: 0.5),
));
}
}
return items;
}
/// 格式化数字
String _formatNumber(int number) {
if (number >= 10000) {
return '${(number / 10000).toStringAsFixed(1)}w';
} else if (number >= 1000) {
return '${(number / 1000).toStringAsFixed(1)}k';
}
return number.toString();
}
}
/// 统计网格项 —— 大数字 + 小标签 + 图标
class _StatGridItem extends StatelessWidget {
final String value;
final String label;
final IconData icon;
final Color color;
const _StatGridItem({
required this.value,
required this.label,
required this.icon,
required this.color,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.06),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
children: [
Icon(icon, color: color, size: 18),
const SizedBox(width: 6),
Text(
value,
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
],
),
const SizedBox(height: 4),
Text(
label,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
],
),
);
}
}
/// 今日统计项
class _TodayStatItem extends StatelessWidget {
final String value;
final String label;
final IconData icon;
final Color color;
const _TodayStatItem({
required this.value,
required this.label,
required this.icon,
required this.color,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color, size: 18),
const SizedBox(width: 6),
Text(
value,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
],
),
const SizedBox(height: 4),
Text(
label,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
],
);
}
}