云算力平台windows桌面应用

This commit is contained in:
lzx
2026-01-22 15:14:27 +08:00
commit 1fe0e54138
52 changed files with 5447 additions and 0 deletions

View File

@@ -0,0 +1,392 @@
import 'package:flutter/material.dart';
import '../services/config_service.dart';
import '../utils/ini_utils.dart';
class ConfigEditorScreen extends StatefulWidget {
const ConfigEditorScreen({super.key});
@override
State<ConfigEditorScreen> createState() => _ConfigEditorScreenState();
}
class _ConfigEditorScreenState extends State<ConfigEditorScreen> {
final ConfigService _configService = ConfigService();
bool _isLoading = false;
bool _isModified = false;
// 配置项数据
final Map<String, Map<String, TextEditingController>> _controllers = {};
final Map<String, Map<String, bool>> _enabledFlags = {};
final List<String> _sections = [];
@override
void initState() {
super.initState();
_loadConfig();
}
Future<void> _loadConfig() async {
setState(() {
_isLoading = true;
});
try {
final content = await _configService.readConfig();
if (content.isNotEmpty) {
_parseConfig(content);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('加载配置失败: $e')),
);
}
} finally {
setState(() {
_isLoading = false;
});
}
}
void _parseConfig(String content) {
final config = parseIni(content);
_controllers.clear();
_enabledFlags.clear();
_sections.clear();
// 定义所有可能的配置项
final configStructure = {
'client': ['server_url', 'update_url'],
'lolminer': ['path'],
'rigel': ['path'],
'bzminer': ['path'],
'proxy': ['proxy'],
'sustain': ['enabled', 'algo', 'coin', 'miner', 'pool_url', 'wallet', 'worker_id', 'pool_user', 'wallet_mining'],
};
for (final section in configStructure.keys) {
_sections.add(section);
_controllers[section] = {};
_enabledFlags[section] = {};
for (final option in configStructure[section]!) {
final value = config.get(section, option) ?? '';
_controllers[section]![option] = TextEditingController(text: value);
_controllers[section]![option]!.addListener(() {
setState(() {
_isModified = true;
});
});
// 默认所有配置项都是禁用的(需要勾选才能编辑)
_enabledFlags[section]![option] = false;
}
}
}
Future<void> _saveConfig() async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('确认保存'),
content: const Text('确定要保存配置文件吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('取消'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('确定'),
),
],
),
);
if (confirmed == true) {
setState(() {
_isLoading = true;
});
try {
final content = _generateConfigContent();
final success = await _configService.saveConfig(content);
if (mounted) {
setState(() {
_isLoading = false;
_isModified = !success;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(success ? '保存成功' : '保存失败'),
backgroundColor: success ? Colors.green : Colors.red,
),
);
}
} catch (e) {
if (mounted) {
setState(() {
_isLoading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('保存失败: $e')),
);
}
}
}
}
String _generateConfigContent() {
final buffer = StringBuffer();
// 添加注释
buffer.writeln('#请确认您的主机上安装了下列挖矿软件,确认后可以打开注释,并修改其路径,如果没有安装,请勿打开注释');
buffer.writeln('#请使用双\\\\,否则可能无法解析出准确的路径');
buffer.writeln();
for (final section in _sections) {
if (section == 'client') {
buffer.writeln('[client]');
_writeOption(buffer, section, 'server_url');
_writeOption(buffer, section, 'update_url');
buffer.writeln();
buffer.writeln('#请确认您的主机上安装了下列挖矿软件,确认后可以打开注释,并修改其路径,如果没有安装,请勿打开注释');
buffer.writeln('#请使用双\\\\,否则可能无法解析出准确的路径');
} else if (section == 'lolminer' || section == 'rigel' || section == 'bzminer') {
buffer.writeln('[$section]');
_writeOption(buffer, section, 'path', commentPrefix: '# ');
} else if (section == 'proxy') {
buffer.writeln();
buffer.writeln('#如果您的网络无法直接连通各个矿池需要使用各大矿池专用网络请打开proxy的注释');
buffer.writeln('#打开此注释后会使用各大矿池的专用网络每笔订单额外增加1%的网络费用');
buffer.writeln('[proxy]');
_writeOption(buffer, section, 'proxy', commentPrefix: '# ');
} else if (section == 'sustain') {
buffer.writeln();
buffer.writeln('#持续挖矿开关,即在矿机没有租约期间是否自行挖矿');
buffer.writeln('#开启此选项启动客户端后客户端会自动根据下面配置开启挖矿任务直到云算力平台有人租赁本台GPU主机');
buffer.writeln('#当该租约结束后本台GPU主机会自动切回下方配置的挖矿任务');
buffer.writeln('[sustain]');
_writeOption(buffer, section, 'enabled', commentPrefix: '#');
_writeOption(buffer, section, 'algo', commentPrefix: '#');
_writeOption(buffer, section, 'coin', commentPrefix: '#');
_writeOption(buffer, section, 'miner', commentPrefix: '#');
_writeOption(buffer, section, 'pool_url', commentPrefix: '#');
_writeOption(buffer, section, 'wallet', commentPrefix: '#');
_writeOption(buffer, section, 'worker_id', commentPrefix: '#');
_writeOption(buffer, section, 'pool_user', commentPrefix: '#');
_writeOption(buffer, section, 'wallet_mining', commentPrefix: '#');
}
buffer.writeln();
}
return buffer.toString();
}
void _writeOption(StringBuffer buffer, String section, String option, {String commentPrefix = ''}) {
final controller = _controllers[section]?[option];
final enabled = _enabledFlags[section]?[option] ?? false;
final value = controller?.text ?? '';
if (value.isEmpty || !enabled) {
// 如果值为空或未启用,使用注释形式
if (option == 'path') {
buffer.writeln('$commentPrefix$option=C:\\\\path\\\\${section}');
} else if (option == 'proxy') {
buffer.writeln('$commentPrefix$option=true');
} else if (option == 'enabled') {
buffer.writeln('$commentPrefix$option=true');
} else if (option == 'algo') {
buffer.writeln('$commentPrefix$option="算法"');
} else if (option == 'coin') {
buffer.writeln('$commentPrefix$option="币种"');
} else if (option == 'miner') {
buffer.writeln('$commentPrefix$option="挖矿软件名此处使用的挖矿软件要使用上方已经配置路径的挖矿软件名即bzminer/lolminer/rigel只能填一个自行选择"');
} else if (option == 'pool_url') {
buffer.writeln('$commentPrefix$option="挖矿地址"');
} else if (option == 'wallet') {
buffer.writeln('$commentPrefix$option="挖矿钱包"');
} else if (option == 'worker_id') {
buffer.writeln('$commentPrefix$option="矿工号"');
} else if (option == 'pool_user') {
buffer.writeln('$commentPrefix$option="挖矿账号名f2pool/m2pool等不支持钱包挖矿的矿池需配置其余支持钱包挖矿的矿池无需配置"');
} else if (option == 'wallet_mining') {
buffer.writeln('$commentPrefix$option=true #pool_user打开时同时打开本配置');
} else {
buffer.writeln('$commentPrefix$option=$value');
}
} else {
// 如果值不为空且已启用,写入实际值
if (option == 'path' || option == 'server_url' || option == 'update_url' ||
option == 'pool_url' || option == 'wallet' || option == 'worker_id' ||
option == 'pool_user' || option == 'algo' || option == 'coin' || option == 'miner') {
buffer.writeln('$option=$value');
} else {
buffer.writeln('$option=$value');
}
}
}
@override
void dispose() {
for (final section in _controllers.values) {
for (final controller in section.values) {
controller.dispose();
}
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('配置文件编辑'),
actions: [
if (_isModified)
IconButton(
icon: const Icon(Icons.save),
onPressed: _isLoading ? null : _saveConfig,
tooltip: '保存',
),
],
),
body: _isLoading && _controllers.isEmpty
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_isModified)
Container(
padding: const EdgeInsets.all(12.0),
margin: const EdgeInsets.only(bottom: 16.0),
decoration: BoxDecoration(
color: Colors.orange.shade50,
borderRadius: BorderRadius.circular(8.0),
border: Border.all(color: Colors.orange),
),
child: Row(
children: [
Icon(Icons.info, color: Colors.orange.shade700),
const SizedBox(width: 8),
const Expanded(
child: Text(
'配置已修改,请点击保存按钮保存更改',
style: TextStyle(fontWeight: FontWeight.w500),
),
),
],
),
),
..._sections.map((section) => _buildSection(section)),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _isLoading || !_isModified ? null : _saveConfig,
icon: const Icon(Icons.save),
label: const Text('保存配置'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
),
],
),
),
);
}
Widget _buildSection(String section) {
final sectionNames = {
'client': '客户端配置',
'lolminer': 'LolMiner 配置',
'rigel': 'Rigel 配置',
'bzminer': 'BzMiner 配置',
'proxy': '代理配置',
'sustain': '持续挖矿配置',
};
return Card(
margin: const EdgeInsets.only(bottom: 16.0),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
sectionNames[section] ?? section,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const Divider(),
...(_controllers[section]?.keys.map((option) => _buildConfigField(section, option)) ?? []),
],
),
),
);
}
Widget _buildConfigField(String section, String option) {
final controller = _controllers[section]?[option];
final enabled = _enabledFlags[section]?[option] ?? false;
final fieldNames = {
'server_url': '服务器地址',
'update_url': '更新服务器地址',
'path': '软件路径',
'proxy': '启用代理',
'enabled': '启用持续挖矿',
'algo': '算法',
'coin': '币种',
'miner': '挖矿软件',
'pool_url': '矿池地址',
'wallet': '钱包地址',
'worker_id': '矿工号',
'pool_user': '矿池用户名',
'wallet_mining': '钱包挖矿',
};
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Checkbox(
value: enabled,
onChanged: (value) {
setState(() {
_enabledFlags[section]![option] = value ?? false;
_isModified = true;
});
},
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
fieldNames[option] ?? option,
style: const TextStyle(fontWeight: FontWeight.w500),
),
const SizedBox(height: 4),
TextField(
controller: controller,
enabled: enabled,
decoration: InputDecoration(
border: const OutlineInputBorder(),
hintText: '请输入${fieldNames[option] ?? option}',
filled: !enabled,
fillColor: enabled ? null : Colors.grey.shade200,
),
),
],
),
),
],
),
);
}
}

View File

@@ -0,0 +1,145 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/client_provider.dart';
import '../services/log_service.dart';
import 'dart:async';
class LogViewerScreen extends StatefulWidget {
const LogViewerScreen({super.key});
@override
State<LogViewerScreen> createState() => _LogViewerScreenState();
}
class _LogViewerScreenState extends State<LogViewerScreen> {
final LogService _logService = LogService();
final ScrollController _scrollController = ScrollController();
String _logs = '';
StreamSubscription<String>? _logSubscription;
StreamSubscription<String>? _clientLogSubscription;
bool _autoScroll = true;
@override
void initState() {
super.initState();
_loadLogs();
_startMonitoring();
}
Future<void> _loadLogs() async {
final logs = await _logService.getRecentLogs();
setState(() {
_logs = logs;
});
if (_autoScroll && _scrollController.hasClients) {
_scrollToBottom();
}
}
void _startMonitoring() {
// 监控日志文件
_logSubscription = _logService.logStream.listen((log) {
setState(() {
_logs += '\n$log';
});
if (_autoScroll && _scrollController.hasClients) {
_scrollToBottom();
}
});
// 监控客户端日志流
final clientProvider = Provider.of<ClientProvider>(context, listen: false);
_clientLogSubscription = clientProvider.logStream.listen((log) {
setState(() {
_logs += '\n$log';
});
if (_autoScroll && _scrollController.hasClients) {
_scrollToBottom();
}
});
}
void _scrollToBottom() {
if (_scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
}
@override
void dispose() {
_logSubscription?.cancel();
_clientLogSubscription?.cancel();
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('日志查看'),
actions: [
IconButton(
icon: Icon(_autoScroll ? Icons.arrow_downward : Icons.arrow_upward),
onPressed: () {
setState(() {
_autoScroll = !_autoScroll;
});
},
tooltip: _autoScroll ? '关闭自动滚动' : '开启自动滚动',
),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadLogs,
tooltip: '刷新日志',
),
IconButton(
icon: const Icon(Icons.clear),
onPressed: () async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('确认清理'),
content: const Text('确定要清理所有日志吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('取消'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('确定'),
),
],
),
);
if (confirmed == true) {
await _logService.clearLogs();
_loadLogs();
}
},
tooltip: '清理日志',
),
],
),
body: Container(
padding: const EdgeInsets.all(8.0),
child: SingleChildScrollView(
controller: _scrollController,
child: SelectableText(
_logs.isEmpty ? '暂无日志' : _logs,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
),
),
),
),
);
}
}

View File

@@ -0,0 +1,426 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/client_provider.dart';
import '../models/client_status.dart';
import 'log_viewer_screen.dart';
import 'config_editor_screen.dart';
import 'mining_info_screen.dart';
import 'dart:io';
class MainScreen extends StatelessWidget {
const MainScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('云算力平台客户端'),
centerTitle: true,
),
body: Consumer<ClientProvider>(
builder: (context, provider, child) {
final clientInfo = provider.clientInfo;
if (provider.isLoading && clientInfo == null) {
return const Center(child: CircularProgressIndicator());
}
if (clientInfo == null) {
return const Center(child: Text('无法获取客户端信息'));
}
return SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 状态指示器
_buildStatusCard(clientInfo),
const SizedBox(height: 16),
// 基本信息卡片
_buildInfoCard(clientInfo),
const SizedBox(height: 16),
// 版本更新提示
if (provider.hasUpdate)
_buildUpdateCard(context, provider),
if (provider.hasUpdate) const SizedBox(height: 16),
// GPU信息卡片
_buildGPUCard(clientInfo.gpus),
const SizedBox(height: 16),
// 操作按钮
_buildActionButtons(context, clientInfo),
],
),
);
},
),
);
}
Widget _buildStatusCard(ClientInfo clientInfo) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: clientInfo.status.color,
shape: BoxShape.circle,
),
),
const SizedBox(width: 12),
Text(
'当前状态: ${clientInfo.status.displayName}',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
}
Widget _buildInfoCard(ClientInfo clientInfo) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'基本信息',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const Divider(),
_buildInfoRow('版本号', clientInfo.version),
_buildInfoRow('身份信息', clientInfo.auth),
_buildInfoRow('机器码', clientInfo.machineCode),
],
),
),
);
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 100,
child: Text(
'$label:',
style: const TextStyle(fontWeight: FontWeight.w500),
),
),
Expanded(
child: Text(value),
),
],
),
);
}
Widget _buildGPUCard(List<GPUInfo> gpus) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'GPU信息',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const Divider(),
if (gpus.isEmpty)
const Text('未检测到GPU')
else
...gpus.map((gpu) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Text('GPU ${gpu.index}: ${gpu.displayName}'),
)),
],
),
),
);
}
Widget _buildUpdateCard(BuildContext context, ClientProvider provider) {
return Card(
color: Colors.orange.shade50,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.system_update, color: Colors.orange.shade700),
const SizedBox(width: 8),
const Text(
'发现新版本',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.orange,
),
),
],
),
const SizedBox(height: 8),
Text('当前版本: ${provider.clientInfo?.version ?? '未知'}'),
Text('最新版本: ${provider.remoteVersion ?? '未知'}'),
const SizedBox(height: 12),
ElevatedButton.icon(
onPressed: () => _performUpdate(context, provider),
icon: const Icon(Icons.download),
label: const Text('立即更新'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
),
),
],
),
),
);
}
Future<void> _performUpdate(BuildContext context, ClientProvider provider) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('确认更新'),
content: const Text(
'更新将在下载完成后,下次启动时应用。\n'
'更新过程中请勿关闭程序。\n\n'
'确定要继续吗?',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('取消'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('确定'),
),
],
),
);
if (confirmed == true) {
// 显示进度对话框
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('正在下载更新...'),
],
),
),
);
final success = await provider.performUpdate();
if (context.mounted) {
Navigator.pop(context); // 关闭进度对话框
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(success ? '更新成功' : '更新失败'),
content: Text(
success
? '更新已下载完成,将在下次启动时应用。\n请重启程序以完成更新。'
: '更新下载失败,请检查网络连接或稍后重试。',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('确定'),
),
],
),
);
}
}
}
Widget _buildActionButtons(BuildContext context, ClientInfo clientInfo) {
return Wrap(
spacing: 12,
runSpacing: 12,
children: [
ElevatedButton.icon(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const LogViewerScreen()),
);
},
icon: const Icon(Icons.description),
label: const Text('查看日志'),
),
ElevatedButton.icon(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const ConfigEditorScreen()),
);
},
icon: const Icon(Icons.settings),
label: const Text('查看/修改配置'),
),
ElevatedButton.icon(
onPressed: clientInfo.status == ClientStatus.mining && clientInfo.miningInfo != null
? () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => MiningInfoScreen(miningInfo: clientInfo.miningInfo!),
),
);
}
: null,
icon: const Icon(Icons.diamond),
label: const Text('挖矿信息'),
style: ElevatedButton.styleFrom(
backgroundColor: clientInfo.status == ClientStatus.mining && clientInfo.miningInfo != null
? null
: Colors.grey,
),
),
ElevatedButton.icon(
onPressed: () => _restartClient(context),
icon: const Icon(Icons.refresh),
label: const Text('重启'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
),
),
ElevatedButton.icon(
onPressed: () => _exitApp(context),
icon: const Icon(Icons.exit_to_app),
label: const Text('退出程序'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
),
],
);
}
void _restartClient(BuildContext context) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('确认重启'),
content: const Text('确定要重启客户端吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('取消'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('确定'),
),
],
),
);
if (confirmed == true) {
// 显示加载对话框,禁用所有操作
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => PopScope(
canPop: false, // 禁止返回
child: const AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('正在重启客户端...'),
],
),
),
),
);
final provider = Provider.of<ClientProvider>(context, listen: false);
bool success = false;
String errorMessage = '';
try {
await provider.restart();
success = true;
} catch (e) {
errorMessage = e.toString();
}
if (context.mounted) {
Navigator.pop(context); // 关闭加载对话框
// 显示结果
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(success ? '客户端重启成功' : '客户端重启失败: $errorMessage'),
backgroundColor: success ? Colors.green : Colors.red,
duration: const Duration(seconds: 3),
),
);
}
}
}
void _exitApp(BuildContext context) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('确认退出'),
content: const Text('确定要退出程序吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('取消'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('确定'),
),
],
),
);
if (confirmed == true) {
exit(0);
}
}
}

View File

@@ -0,0 +1,239 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'dart:async';
import '../models/client_status.dart';
import '../core/mining_manager.dart';
class MiningInfoScreen extends StatefulWidget {
final MiningInfo miningInfo;
const MiningInfoScreen({
super.key,
required this.miningInfo,
});
@override
State<MiningInfoScreen> createState() => _MiningInfoScreenState();
}
class _MiningInfoScreenState extends State<MiningInfoScreen> {
final MiningManager _miningManager = MiningManager();
final ScrollController _logScrollController = ScrollController();
final List<String> _minerLogs = [];
StreamSubscription<String>? _logSubscription;
bool _autoScroll = true;
@override
void initState() {
super.initState();
_startLogMonitoring();
}
void _startLogMonitoring() {
_logSubscription = _miningManager.minerLogStream.listen((log) {
setState(() {
_minerLogs.add(log);
// 限制日志行数,避免内存溢出
if (_minerLogs.length > 1000) {
_minerLogs.removeAt(0);
}
});
if (_autoScroll && _logScrollController.hasClients) {
_scrollToBottom();
}
});
}
void _scrollToBottom() {
if (_logScrollController.hasClients) {
_logScrollController.animateTo(
_logScrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
}
@override
void dispose() {
_logSubscription?.cancel();
_logScrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final endTime = DateTime.fromMillisecondsSinceEpoch(widget.miningInfo.endTimestamp * 1000);
final formatter = DateFormat('yyyy-MM-dd HH:mm:ss');
return Scaffold(
appBar: AppBar(
title: const Text('挖矿信息'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 挖矿任务信息卡片
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'当前挖矿任务',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const Divider(),
_buildInfoRow('币种', widget.miningInfo.coin),
_buildInfoRow('算法', widget.miningInfo.algo),
_buildInfoRow('矿池', widget.miningInfo.pool),
_buildInfoRow('矿池地址', widget.miningInfo.poolUrl),
_buildInfoRow('钱包地址', widget.miningInfo.walletAddress),
_buildInfoRow('矿工号', widget.miningInfo.workerId),
if (widget.miningInfo.miner != null)
_buildInfoRow('挖矿软件', widget.miningInfo.miner!),
if (widget.miningInfo.pid != null)
_buildInfoRow('进程ID', widget.miningInfo.pid.toString()),
_buildInfoRow(
'结束时间',
formatter.format(endTime),
),
const SizedBox(height: 16),
_buildTimeRemaining(endTime),
],
),
),
),
const SizedBox(height: 16),
// 挖矿日志卡片
Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
const Text(
'挖矿日志',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
IconButton(
icon: Icon(_autoScroll ? Icons.arrow_downward : Icons.arrow_upward),
onPressed: () {
setState(() {
_autoScroll = !_autoScroll;
});
},
tooltip: _autoScroll ? '关闭自动滚动' : '开启自动滚动',
),
IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
setState(() {
_minerLogs.clear();
});
},
tooltip: '清空日志',
),
],
),
),
const Divider(height: 1),
Container(
height: 400,
padding: const EdgeInsets.all(8.0),
child: _minerLogs.isEmpty
? const Center(
child: Text(
'暂无日志输出',
style: TextStyle(color: Colors.grey),
),
)
: SingleChildScrollView(
controller: _logScrollController,
child: SelectableText(
_minerLogs.join('\n'),
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
),
),
),
),
],
),
),
],
),
),
);
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 100,
child: Text(
'$label:',
style: const TextStyle(fontWeight: FontWeight.w500),
),
),
Expanded(
child: Text(value),
),
],
),
);
}
Widget _buildTimeRemaining(DateTime endTime) {
final now = DateTime.now();
final remaining = endTime.difference(now);
if (remaining.isNegative) {
return const Card(
color: Colors.red,
child: Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'任务已结束',
style: TextStyle(color: Colors.white),
),
),
);
}
final hours = remaining.inHours;
final minutes = remaining.inMinutes.remainder(60);
final seconds = remaining.inSeconds.remainder(60);
return Card(
color: Colors.green,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'剩余时间: ${hours}小时 ${minutes}分钟 ${seconds}',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
);
}
}