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,
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
],
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|