Files
CloudHome-Ops/lib/screens/home/home_page.dart

262 lines
7.9 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:go_router/go_router.dart';
import '../../models/device.dart';
import '../../providers/dashboard_provider.dart';
import '../../providers/devices_provider.dart';
import '../../app/theme.dart';
import '../../utils/i10n.dart';
import '../../utils/responsive.dart';
import '../../providers/auth_provider.dart';
import '../../utils/app_update.dart';
import '../../utils/permissions.dart';
import '../../widgets/cloud_card.dart';
/// 首页:仪表盘概览(底部导航第 1 个 tab)。
///
/// 展示设备统计卡片与各模块快捷入口。
class HomePage extends ConsumerStatefulWidget {
const HomePage({super.key});
@override
ConsumerState<HomePage> createState() => _HomePageState();
}
class _HomePageState extends ConsumerState<HomePage> {
@override
void initState() {
super.initState();
// 加载仪表盘统计(设备/门店总数)。
Future.microtask(
() => ref.read(dashboardStatsProvider.notifier).load(),
);
// 启动后静默检测新版本(无更新/失败均不打扰,仅会话内首次有更新时弹窗)。
final orgId = ref.read(authProvider).user?.orgId;
final ctx = context;
Future.microtask(() {
if (!ctx.mounted) return;
checkAndPromptUpdate(
ctx,
silentIfNoUpdate: true,
orgId: orgId,
);
});
}
@override
Widget build(BuildContext context) {
final devicesAsync = ref.watch(devicesProvider);
final t = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(title: Text(t.appTitle)),
body: devicesAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (_, __) => _buildDashboard(context, null),
data: (devices) => _buildDashboard(context, devices),
),
);
}
Widget _buildDashboard(BuildContext context, List<Device>? devices) {
final stats = ref.watch(dashboardStatsProvider);
final totalCount = stats.deviceTotal;
final storeTotal = stats.storeTotal;
// 在线/离线统计改为后端真实聚合值(按 status 拉 totalCount),
// 不再用首屏列表客户端统计,避免与设备总数(12 万级)严重不符。
final onlineCount = stats.onlineCount;
final offlineCount = stats.offlineCount;
final t = AppLocalizations.of(context);
// 统计卡配色纳入橙白语义体系(仅改色,图标图形与 size 冻结不动)。
final scheme = Theme.of(context).colorScheme;
final semantic = Theme.of(context).extension<AppSemanticColors>();
return RefreshIndicator(
onRefresh: () async {
final _ = await ref.refresh(devicesProvider.future);
await ref.read(dashboardStatsProvider.notifier).load();
},
child: ListView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(16),
children: [
_buildBrandBanner(context),
const SizedBox(height: 16),
// 统计卡片
GridView.count(
crossAxisCount: context.responsive.gridCount(minTileWidth: 300),
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 1.4,
children: [
_StatCard(
title: t.totalDevices,
value: '$totalCount',
icon: Icons.devices,
color: scheme.primary,
),
_StatCard(
title: t.totalStores,
value: '$storeTotal',
icon: Icons.store,
color: scheme.tertiary,
),
_StatCard(
title: t.onlineDevices,
value: '$onlineCount',
icon: Icons.check_circle_outline,
color: semantic?.success ?? scheme.tertiary,
),
_StatCard(
title: t.offlineDevices,
value: '$offlineCount',
icon: Icons.highlight_off,
color: scheme.onSurfaceVariant,
),
],
),
const SizedBox(height: 24),
// 快捷操作
Text(
t.quickActions,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _QuickAction(
label: t.deviceManagement,
icon: Icons.devices,
onTap: () => context.go('/device'),
),
),
Expanded(
child: _QuickAction(
label: t.storeManagement,
icon: Icons.store,
onTap: () => context.go('/store'),
),
),
if (SessionPermissions.canViewUserManagement(
ref.read(authProvider).user))
Expanded(
child: _QuickAction(
label: t.userManagement,
icon: Icons.people,
onTap: () => context.push('/admin/sys/user'),
),
),
],
),
],
),
);
}
/// 品牌云卡横幅:首页招牌区块(橙→金渐变)。
Widget _buildBrandBanner(BuildContext context) {
final t = AppLocalizations.of(context);
return CloudCard(
child: Row(
children: [
const Icon(Icons.cloud, size: 36),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
t.appHeroTitle,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
const Text(
'终端管控·内容运营·门店管理',
style: TextStyle(fontSize: 13),
),
],
),
),
],
),
);
}
}
class _StatCard extends StatelessWidget {
const _StatCard({
required this.title,
required this.value,
required this.icon,
required this.color,
});
final String title;
final String value;
final IconData icon;
final Color color;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color, size: 28),
const SizedBox(height: 8),
Text(
value,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(title, style: Theme.of(context).textTheme.bodySmall),
],
),
),
);
}
}
class _QuickAction extends StatelessWidget {
const _QuickAction({
required this.label,
required this.icon,
required this.onTap,
});
final String label;
final IconData icon;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircleAvatar(
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
child: Icon(icon, color: Theme.of(context).colorScheme.primary),
),
const SizedBox(height: 4),
Text(label, style: Theme.of(context).textTheme.bodySmall),
],
),
);
}
}