rwadurian/frontend/mobile-app/lib/features/mining/presentation/pages/mining_page.dart

715 lines
22 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 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../../../../core/di/injection_container.dart';
import '../../../../core/storage/storage_keys.dart';
/// 监控状态枚举
enum MonitorStatus {
pending, // 待开启
active, // 监控中
}
/// 钱包生成状态枚举
enum WalletCreationStatus {
unknown, // 未知(初始状态)
creating, // 创建中(显示"创建账号审核中..."
ready, // 已就绪
}
/// 监控页面 - 显示监控状态和控制
/// 展示用户序列号、社区信息,未来将接入实时监控视频流
class MiningPage extends ConsumerStatefulWidget {
const MiningPage({super.key});
@override
ConsumerState<MiningPage> createState() => _MiningPageState();
}
class _MiningPageState extends ConsumerState<MiningPage> {
// 当前监控状态
MonitorStatus _monitorStatus = MonitorStatus.pending;
// 用户数据(从存储加载)
String _serialNumber = '--';
String? _avatarSvg;
String? _avatarUrl;
String? _localAvatarPath; // 本地头像文件路径
// 授权数据(从 authorization-service 获取)
String _community = '--';
String _province = '--';
String _city = '--';
// 钱包生成状态
WalletCreationStatus _walletStatus = WalletCreationStatus.unknown;
Timer? _walletPollingTimer;
@override
void initState() {
super.initState();
// 先同步检查本地头像,再异步加载其他数据
_checkLocalAvatarSync();
_loadUserData();
_loadAuthorizationData();
// 开始轮询钱包状态
_startWalletPolling();
}
@override
void dispose() {
_walletPollingTimer?.cancel();
super.dispose();
}
/// 同步检查本地头像文件(在 build 之前快速获取)
void _checkLocalAvatarSync() {
WidgetsBinding.instance.addPostFrameCallback((_) async {
final accountService = ref.read(accountServiceProvider);
final localPath = await accountService.getLocalAvatarPath();
if (mounted && localPath != null && _localAvatarPath == null) {
setState(() {
_localAvatarPath = localPath;
});
}
});
}
/// 加载用户数据
Future<void> _loadUserData() async {
final accountService = ref.read(accountServiceProvider);
// 并行加载所有数据
final results = await Future.wait([
accountService.getUserSerialNum(),
accountService.getAvatarSvg(),
accountService.getAvatarUrl(),
accountService.getLocalAvatarPath(),
]);
final serialNum = results[0] as String?;
final avatarSvg = results[1] as String?;
final avatarUrl = results[2] as String?;
final localAvatarPath = results[3] as String?;
if (mounted) {
setState(() {
_serialNumber = serialNum?.toString() ?? '--';
_avatarSvg = avatarSvg;
_avatarUrl = avatarUrl;
_localAvatarPath = localAvatarPath;
});
// 如果有远程URL但没有本地缓存后台下载并缓存
if (avatarUrl != null && avatarUrl.isNotEmpty && localAvatarPath == null) {
_downloadAndCacheAvatar(avatarUrl);
}
}
}
/// 后台下载并缓存头像
Future<void> _downloadAndCacheAvatar(String url) async {
final accountService = ref.read(accountServiceProvider);
final localPath = await accountService.downloadAndCacheAvatar(url);
if (mounted && localPath != null) {
setState(() {
_localAvatarPath = localPath;
});
}
}
/// 加载授权数据(社区、省、市)
Future<void> _loadAuthorizationData() async {
try {
debugPrint('[MiningPage] 开始加载授权数据...');
final authorizationService = ref.read(authorizationServiceProvider);
final summary = await authorizationService.getMyAuthorizationSummary();
if (mounted) {
setState(() {
_community = summary.communityName ?? '--';
_city = summary.cityCompanyName ?? '--';
_province = summary.provinceCompanyName ?? '--';
});
debugPrint('[MiningPage] 授权数据加载成功:');
debugPrint('[MiningPage] 社区: $_community');
debugPrint('[MiningPage] 市公司: $_city');
debugPrint('[MiningPage] 省公司: $_province');
}
} catch (e, stackTrace) {
debugPrint('[MiningPage] 加载授权数据失败: $e');
debugPrint('[MiningPage] 堆栈: $stackTrace');
// 失败时保持默认值 '--'
}
}
/// 开始轮询钱包生成状态
Future<void> _startWalletPolling() async {
debugPrint('[MiningPage] 开始轮询钱包生成状态...');
// 先从本地存储读取钱包状态
final secureStorage = ref.read(secureStorageProvider);
final isWalletReady = await secureStorage.read(key: StorageKeys.isWalletReady);
if (isWalletReady == 'true') {
// 本地已标记为 ready直接设置状态不需要轮询
debugPrint('[MiningPage] 本地存储显示钱包已就绪,无需轮询');
if (mounted) {
setState(() {
_walletStatus = WalletCreationStatus.ready;
});
}
return;
}
// 本地未标记为 ready开始轮询
// 立即检查一次
_checkWalletStatus();
// 每5秒检查一次
_walletPollingTimer = Timer.periodic(const Duration(seconds: 5), (_) {
_checkWalletStatus();
});
}
/// 检查钱包生成状态
Future<void> _checkWalletStatus() async {
try {
final accountService = ref.read(accountServiceProvider);
final walletInfo = await accountService.getWalletInfo(_serialNumber);
if (!mounted) return;
if (walletInfo.isReady) {
// 钱包已就绪,保存到本地存储并停止轮询
final secureStorage = ref.read(secureStorageProvider);
await secureStorage.write(key: StorageKeys.isWalletReady, value: 'true');
setState(() {
_walletStatus = WalletCreationStatus.ready;
});
_walletPollingTimer?.cancel();
debugPrint('[MiningPage] 钱包已就绪,停止轮询');
} else {
// 非 ready 状态(包括 failed、creating都继续轮询
setState(() {
_walletStatus = WalletCreationStatus.creating;
});
debugPrint('[MiningPage] 钱包创建中,继续轮询...');
}
} catch (e) {
debugPrint('[MiningPage] 检查钱包状态失败: $e');
// 不改变状态,继续轮询
}
}
/// 显示帮助信息
void _showHelpInfo() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('监控说明'),
content: const Text('监控是您查看榴莲种植实况的工具。\n\n'
'开启监控后,您可以实时查看种植基地的视频流。\n\n'
'目前功能正在开发中,敬请期待。'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('知道了'),
),
],
),
);
}
/// 开启监控
void _startMonitor() {
setState(() {
_monitorStatus = MonitorStatus.active;
});
// TODO: 未来在这里连接实时监控视频流
}
/// 关闭监控
void _stopMonitor() {
setState(() {
_monitorStatus = MonitorStatus.pending;
});
// TODO: 未来在这里断开视频流连接
}
/// 获取状态文本
String _getStatusText() {
switch (_monitorStatus) {
case MonitorStatus.pending:
return '开启监控';
case MonitorStatus.active:
return '监控中';
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Container(
width: double.infinity,
height: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFFFFF5E6),
Color(0xFFFFE4B5),
],
),
),
child: SafeArea(
child: Column(
children: [
// 顶部标题栏
_buildAppBar(),
// 用户信息区域
_buildUserInfo(),
const SizedBox(height: 24),
// 挖矿状态区域
Expanded(
child: _buildMiningStatusArea(),
),
],
),
),
),
);
}
/// 构建顶部标题栏
Widget _buildAppBar() {
return Container(
height: 56,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
// 帮助按钮(左侧)
GestureDetector(
onTap: _showHelpInfo,
child: Container(
width: 48,
height: 48,
alignment: Alignment.center,
child: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: const Color(0xFF8B5A2B),
width: 2,
),
),
child: const Center(
child: Text(
'i',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Color(0xFF8B5A2B),
),
),
),
),
),
),
// 标题
const Expanded(
child: Text(
'监控',
style: TextStyle(
fontSize: 18,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.25,
letterSpacing: -0.27,
color: Color(0xFF5D4037),
),
textAlign: TextAlign.center,
),
),
// 关闭按钮(仅在监控开启时显示)
if (_monitorStatus == MonitorStatus.active)
GestureDetector(
onTap: _stopMonitor,
child: Container(
width: 48,
height: 48,
alignment: Alignment.center,
child: const Icon(
Icons.close,
size: 24,
color: Color(0xFF8B5A2B),
),
),
)
else
const SizedBox(width: 48), // 占位保持布局平衡
],
),
);
}
/// 构建用户信息区域
Widget _buildUserInfo() {
// 构建省市显示文本
String locationText = '';
if (_province != '--' && _city != '--') {
locationText = '$_province · $_city';
} else if (_province != '--') {
locationText = _province;
} else if (_city != '--') {
locationText = _city;
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
// 头像
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(40),
color: const Color(0xFFFFF5E6),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(40),
child: _buildAvatarContent(),
),
),
const SizedBox(width: 16),
// 用户信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 根据钱包状态显示不同内容
if (_walletStatus == WalletCreationStatus.creating)
Row(
children: [
const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF8B5A2B)),
),
),
const SizedBox(width: 8),
Text(
'创建账号审核中...',
style: const TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Color(0xFF8B5A2B),
),
),
],
)
else if (_walletStatus == WalletCreationStatus.ready)
Text(
'序列号$_serialNumber',
style: const TextStyle(
fontSize: 20,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.25,
letterSpacing: -0.3,
color: Color(0xFF5D4037),
),
)
else
// unknown 状态也显示"创建账号审核中..."
Row(
children: [
const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF8B5A2B)),
),
),
const SizedBox(width: 8),
Text(
'创建账号审核中...',
style: const TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Color(0xFF8B5A2B),
),
),
],
),
const SizedBox(height: 4),
Text(
'社区: $_community${locationText.isNotEmpty ? ' / $locationText' : ''}',
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
height: 1.5,
color: Color(0xFF8B5A2B),
),
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
);
}
/// 构建监控状态区域
Widget _buildMiningStatusArea() {
return Padding(
padding: const EdgeInsets.all(16),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: const Color(0x66FFFFFF),
borderRadius: BorderRadius.circular(12),
),
child: _monitorStatus == MonitorStatus.active
? _buildMonitorActiveView()
: _buildMonitorPendingView(),
),
);
}
/// 构建监控待开启视图
Widget _buildMonitorPendingView() {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 开启监控按钮
GestureDetector(
onTap: _startMonitor,
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.transparent,
border: Border.all(
color: const Color(0xFFD4AF37).withOpacity(0.5),
width: 3,
),
),
child: Icon(
Icons.videocam_outlined,
size: 48,
color: const Color(0xFFD4AF37).withOpacity(0.5),
),
),
),
const SizedBox(height: 16),
// 状态文本
Text(
_getStatusText(),
style: const TextStyle(
fontSize: 18,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.56,
color: Color(0xFF8B5A2B),
),
),
const SizedBox(height: 8),
const Text(
'点击开启实时监控',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
color: Color(0x998B5A2B),
),
),
],
);
}
/// 构建监控中视图
Widget _buildMonitorActiveView() {
return Column(
children: [
// 视频流占位区域
Expanded(
child: Container(
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.black87,
borderRadius: BorderRadius.circular(8),
),
child: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.videocam,
size: 64,
color: Color(0xFFD4AF37),
),
SizedBox(height: 16),
Text(
'监控中',
style: TextStyle(
fontSize: 18,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Color(0xFFD4AF37),
),
),
SizedBox(height: 8),
Text(
'视频流功能开发中...',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
color: Colors.white70,
),
),
],
),
),
),
),
// 底部提示
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Text(
'点击右上角 × 关闭监控',
style: TextStyle(
fontSize: 12,
fontFamily: 'Inter',
color: const Color(0xFF8B5A2B).withOpacity(0.6),
),
),
),
],
);
}
/// 构建头像内容优先本地文件其次网络URL最后SVG
Widget _buildAvatarContent() {
// 1. 优先显示本地缓存的头像文件
if (_localAvatarPath != null && _localAvatarPath!.isNotEmpty) {
final file = File(_localAvatarPath!);
// 同步检查文件是否存在
if (file.existsSync()) {
return Image.file(
file,
width: 80,
height: 80,
fit: BoxFit.cover,
gaplessPlayback: true, // 防止图片切换时闪烁
errorBuilder: (context, error, stackTrace) {
// 本地文件加载失败尝试网络URL
return _buildNetworkOrSvgAvatar();
},
);
}
}
// 2. 没有本地缓存尝试网络URL或SVG
return _buildNetworkOrSvgAvatar();
}
/// 构建网络URL或SVG头像
Widget _buildNetworkOrSvgAvatar() {
// 尝试显示网络图片URL
if (_avatarUrl != null && _avatarUrl!.isNotEmpty) {
return Image.network(
_avatarUrl!,
width: 80,
height: 80,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return const Center(
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFD4AF37)),
),
),
);
},
errorBuilder: (context, error, stackTrace) {
// 加载失败时显示SVG或默认头像
return _buildSvgOrDefaultAvatar();
},
);
}
// 显示SVG或默认头像
return _buildSvgOrDefaultAvatar();
}
/// 构建SVG或默认头像
/// 注意_avatarSvg 可能存储的是 URL用户上传的图片或 SVG 字符串(随机生成的头像)
Widget _buildSvgOrDefaultAvatar() {
if (_avatarSvg != null && _avatarSvg!.isNotEmpty) {
// 检测是否是 URL用户上传的头像图片
if (_avatarSvg!.startsWith('http://') || _avatarSvg!.startsWith('https://')) {
return Image.network(
_avatarSvg!,
width: 80,
height: 80,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return const Center(
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFD4AF37)),
),
),
);
},
errorBuilder: (context, error, stackTrace) {
return const Icon(
Icons.person,
size: 40,
color: Color(0xFF8B5A2B),
);
},
);
}
// 检测是否是 SVG 字符串(随机生成的 SVG 头像)
if (_avatarSvg!.contains('<svg') || _avatarSvg!.startsWith('<?xml')) {
return SvgPicture.string(
_avatarSvg!,
width: 80,
height: 80,
fit: BoxFit.cover,
);
}
}
return const Icon(
Icons.person,
size: 40,
color: Color(0xFF8B5A2B),
);
}
}