715 lines
22 KiB
Dart
715 lines
22 KiB
Dart
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),
|
||
);
|
||
}
|
||
}
|