406 lines
11 KiB
Dart
406 lines
11 KiB
Dart
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,
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|