优化
This commit is contained in:
405
client/flutter/lib/pages/dashboard_page.dart
Normal file
405
client/flutter/lib/pages/dashboard_page.dart
Normal file
@@ -0,0 +1,405 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user