1049 lines
33 KiB
Dart
1049 lines
33 KiB
Dart
import 'dart:convert';
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:shared_preferences/shared_preferences.dart';
|
||
import '../../../constants/app_constants.dart';
|
||
import '../../../utils/http/http_client.dart';
|
||
import 'user-plan.dart';
|
||
|
||
/// 时间: 2026-03-29
|
||
/// 功能: 离线数据管理页面
|
||
/// 介绍: 从服务器加载诗词数据到本地缓存,支持离线使用
|
||
/// 最新变化: 新建页面
|
||
|
||
class OfflineDataPage extends StatefulWidget {
|
||
const OfflineDataPage({super.key});
|
||
|
||
@override
|
||
State<OfflineDataPage> createState() => _OfflineDataPageState();
|
||
}
|
||
|
||
enum DownloadType {
|
||
poetry, // 诗句
|
||
quiz, // 答题
|
||
}
|
||
|
||
class _OfflineDataPageState extends State<OfflineDataPage> {
|
||
DownloadType _selectedType = DownloadType.poetry;
|
||
int _selectedCount = 30;
|
||
bool _isLoading = false;
|
||
bool _isCancelling = false;
|
||
int _progress = 0;
|
||
String _status = '';
|
||
int _cachedCount = 0;
|
||
bool _shouldCancel = false;
|
||
int _downloadedCount = 0;
|
||
|
||
int _poetryCount = 0;
|
||
int _quizCount = 0;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_loadCachedCount();
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
// 页面销毁时不取消下载,让下载在后台继续
|
||
super.dispose();
|
||
}
|
||
|
||
Future<void> _loadCachedCount() async {
|
||
final prefs = await SharedPreferences.getInstance();
|
||
final poetryData = prefs.getStringList('offline_poetry_data') ?? [];
|
||
final quizData = prefs.getStringList('offline_quiz_data') ?? [];
|
||
|
||
setState(() {
|
||
_poetryCount = poetryData.length;
|
||
_quizCount = quizData.length;
|
||
_cachedCount = _selectedType == DownloadType.poetry
|
||
? _poetryCount
|
||
: _quizCount;
|
||
});
|
||
}
|
||
|
||
Future<bool> _checkUserPlanStatus() async {
|
||
final prefs = await SharedPreferences.getInstance();
|
||
return prefs.getBool('user_plan_joined') ?? false;
|
||
}
|
||
|
||
Future<void> _downloadOfflineData() async {
|
||
if (_isLoading) return;
|
||
|
||
// 检查缓存数量限制
|
||
final prefs = await SharedPreferences.getInstance();
|
||
final existingData =
|
||
prefs.getStringList(
|
||
_selectedType == DownloadType.poetry
|
||
? 'offline_poetry_data'
|
||
: 'offline_quiz_data',
|
||
) ??
|
||
[];
|
||
if (existingData.length >= 500) {
|
||
if (mounted) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: const Text('缓存已达上限500条,请先清空缓存'),
|
||
backgroundColor: Colors.red,
|
||
),
|
||
);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 检查100条是否需要用户体验计划
|
||
if (_selectedCount == 100) {
|
||
final isUserPlanJoined = await _checkUserPlanStatus();
|
||
if (!isUserPlanJoined) {
|
||
if (mounted) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: const Text('100条下载需要加入用户体验计划'),
|
||
backgroundColor: Colors.orange,
|
||
action: SnackBarAction(
|
||
label: '去加入',
|
||
onPressed: () {
|
||
Navigator.push(
|
||
context,
|
||
MaterialPageRoute(builder: (_) => const UserPlanPage()),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
);
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
|
||
setState(() {
|
||
_isLoading = true;
|
||
_isCancelling = false;
|
||
_shouldCancel = false;
|
||
_progress = 0;
|
||
_status = '开始下载数据...';
|
||
_downloadedCount = 0;
|
||
});
|
||
|
||
try {
|
||
final dataKey = _selectedType == DownloadType.poetry
|
||
? 'offline_poetry_data'
|
||
: 'offline_quiz_data';
|
||
var currentData = prefs.getStringList(dataKey) ?? [];
|
||
|
||
for (int i = 0; i < _selectedCount; i++) {
|
||
// 检查是否需要取消
|
||
if (_shouldCancel) {
|
||
if (mounted) {
|
||
setState(() {
|
||
_status = '下载已取消';
|
||
_isLoading = false;
|
||
_isCancelling = false;
|
||
});
|
||
}
|
||
// 取消时保存已下载的数据
|
||
if (_downloadedCount > 0) {
|
||
await prefs.setStringList(dataKey, currentData);
|
||
if (mounted) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text('已保存 $_downloadedCount 条数据'),
|
||
backgroundColor: AppConstants.primaryColor,
|
||
),
|
||
);
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
dynamic response;
|
||
if (_selectedType == DownloadType.poetry) {
|
||
// 下载诗句
|
||
response = await HttpClient.get('pms.php');
|
||
} else {
|
||
// 下载答题:使用question接口获取完整数据(包含options)
|
||
// 使用循环id来获取不同的题目
|
||
response = await HttpClient.get(
|
||
'poe/api.php',
|
||
queryParameters: {'action': 'question', 'id': '$i'},
|
||
);
|
||
}
|
||
|
||
if (response.isSuccess && response.jsonData != null) {
|
||
final responseData = response.jsonData;
|
||
|
||
print('API完整响应: $responseData');
|
||
|
||
// 检查API返回格式
|
||
if (responseData['code'] == 0 && responseData['data'] != null) {
|
||
final itemData = responseData['data'] as Map<String, dynamic>;
|
||
|
||
print('API返回的data字段: $itemData');
|
||
print('API返回的options字段: ${itemData['options']}');
|
||
print('options字段类型: ${itemData['options']?.runtimeType}');
|
||
|
||
// 对于答题数据,确保options字段被正确序列化
|
||
if (_selectedType == DownloadType.quiz) {
|
||
// 深拷贝数据,避免修改原始数据
|
||
final dataToStore = Map<String, dynamic>.from(itemData);
|
||
|
||
// 确保options字段是JSON字符串格式
|
||
if (dataToStore.containsKey('options') &&
|
||
dataToStore['options'] != null) {
|
||
if (dataToStore['options'] is List) {
|
||
// 将List转换为JSON字符串
|
||
dataToStore['options'] = jsonEncode(dataToStore['options']);
|
||
print('存储答题数据options: ${dataToStore['options']}');
|
||
} else if (dataToStore['options'] is String) {
|
||
// 已经是字符串,直接使用
|
||
print('options已经是字符串,直接使用: ${dataToStore['options']}');
|
||
} else {
|
||
print('options类型异常: ${dataToStore['options'].runtimeType}');
|
||
}
|
||
} else {
|
||
print('警告:options字段不存在或为null');
|
||
// 如果没有options,添加一个空数组
|
||
dataToStore['options'] = jsonEncode([]);
|
||
}
|
||
|
||
// 将整个Map转换为JSON字符串存储
|
||
final storedString = jsonEncode(dataToStore);
|
||
print('存储答题数据完整: $storedString');
|
||
currentData.add(storedString);
|
||
} else {
|
||
currentData.add(itemData.toString());
|
||
}
|
||
|
||
_downloadedCount++;
|
||
|
||
// 下载一条,写入一条
|
||
await prefs.setStringList(dataKey, currentData);
|
||
|
||
// 实时更新缓存状态
|
||
if (mounted) {
|
||
setState(() {
|
||
if (_selectedType == DownloadType.poetry) {
|
||
_poetryCount = currentData.length;
|
||
} else {
|
||
_quizCount = currentData.length;
|
||
}
|
||
_cachedCount = currentData.length;
|
||
});
|
||
}
|
||
} else {
|
||
print('API返回错误: ${responseData['msg'] ?? '未知错误'}');
|
||
}
|
||
}
|
||
|
||
final currentProgress = ((i + 1) / _selectedCount * 100).toInt();
|
||
if (mounted) {
|
||
setState(() {
|
||
_progress = currentProgress;
|
||
_status = '下载中... ${i + 1}/$_selectedCount';
|
||
});
|
||
}
|
||
|
||
// 模拟网络延迟,避免请求过于频繁
|
||
await Future.delayed(const Duration(milliseconds: 500));
|
||
}
|
||
|
||
// 检查总缓存数量
|
||
if (currentData.length > 500) {
|
||
if (mounted) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: const Text('缓存将超过500条上限,请先清空缓存'),
|
||
backgroundColor: Colors.red,
|
||
),
|
||
);
|
||
setState(() {
|
||
_isLoading = false;
|
||
});
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (mounted) {
|
||
setState(() {
|
||
_status = '下载完成!';
|
||
_cachedCount = currentData.length;
|
||
});
|
||
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text(
|
||
'成功缓存 ${_selectedType == DownloadType.poetry ? '诗词' : '答题'} $_cachedCount 条数据',
|
||
),
|
||
backgroundColor: AppConstants.primaryColor,
|
||
),
|
||
);
|
||
}
|
||
} catch (e) {
|
||
if (mounted) {
|
||
setState(() {
|
||
_status = '下载失败: $e';
|
||
});
|
||
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(content: Text('下载失败: $e'), backgroundColor: Colors.red),
|
||
);
|
||
}
|
||
} finally {
|
||
if (!_shouldCancel && mounted) {
|
||
setState(() {
|
||
_isLoading = false;
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
Future<void> _clearOfflineData() async {
|
||
final prefs = await SharedPreferences.getInstance();
|
||
|
||
// 获取当前两种类型的缓存数量
|
||
final poetryCount =
|
||
(prefs.getStringList('offline_poetry_data') ?? []).length;
|
||
final quizCount = (prefs.getStringList('offline_quiz_data') ?? []).length;
|
||
|
||
if (poetryCount == 0 && quizCount == 0) {
|
||
if (mounted) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(
|
||
content: Text('暂无缓存数据'),
|
||
backgroundColor: Colors.orange,
|
||
),
|
||
);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 显示选择弹窗
|
||
if (mounted) {
|
||
showDialog(
|
||
context: context,
|
||
builder: (BuildContext context) {
|
||
return AlertDialog(
|
||
title: const Text('选择清空内容'),
|
||
content: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
if (poetryCount > 0) Text('• 精选诗句: $poetryCount 条'),
|
||
if (quizCount > 0) Text('• 答题挑战: $quizCount 条'),
|
||
const SizedBox(height: 16),
|
||
const Text('请选择要清空的缓存类型'),
|
||
],
|
||
),
|
||
actions: [
|
||
if (poetryCount > 0)
|
||
TextButton(
|
||
onPressed: () {
|
||
Navigator.of(context).pop();
|
||
_clearSpecificData('offline_poetry_data', '精选诗句');
|
||
},
|
||
child: const Text('清空诗句'),
|
||
),
|
||
if (quizCount > 0)
|
||
TextButton(
|
||
onPressed: () {
|
||
Navigator.of(context).pop();
|
||
_clearSpecificData('offline_quiz_data', '答题挑战');
|
||
},
|
||
child: const Text('清空答题'),
|
||
),
|
||
TextButton(
|
||
onPressed: () {
|
||
Navigator.of(context).pop();
|
||
_clearAllData();
|
||
},
|
||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||
child: const Text('清空全部'),
|
||
),
|
||
TextButton(
|
||
onPressed: () {
|
||
Navigator.of(context).pop();
|
||
},
|
||
child: const Text('取消'),
|
||
),
|
||
],
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|
||
|
||
Future<void> _clearSpecificData(String key, String typeName) async {
|
||
final prefs = await SharedPreferences.getInstance();
|
||
await prefs.remove(key);
|
||
|
||
if (mounted) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text('已清空$typeName离线数据'),
|
||
backgroundColor: AppConstants.primaryColor,
|
||
),
|
||
);
|
||
}
|
||
|
||
// 更新对应的计数
|
||
if (key == 'offline_poetry_data') {
|
||
setState(() {
|
||
_poetryCount = 0;
|
||
if (_selectedType == DownloadType.poetry) {
|
||
_cachedCount = 0;
|
||
}
|
||
});
|
||
} else if (key == 'offline_quiz_data') {
|
||
setState(() {
|
||
_quizCount = 0;
|
||
if (_selectedType == DownloadType.quiz) {
|
||
_cachedCount = 0;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
Future<void> _clearAllData() async {
|
||
final prefs = await SharedPreferences.getInstance();
|
||
await prefs.remove('offline_poetry_data');
|
||
await prefs.remove('offline_quiz_data');
|
||
|
||
if (mounted) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(
|
||
content: Text('已清空所有离线数据'),
|
||
backgroundColor: AppConstants.primaryColor,
|
||
),
|
||
);
|
||
}
|
||
|
||
setState(() {
|
||
_poetryCount = 0;
|
||
_quizCount = 0;
|
||
_cachedCount = 0;
|
||
});
|
||
}
|
||
|
||
void _cancelDownload() {
|
||
if (mounted) {
|
||
setState(() {
|
||
_isCancelling = true;
|
||
_shouldCancel = true;
|
||
_status = '正在取消下载...';
|
||
});
|
||
}
|
||
}
|
||
|
||
Future<void> _showServerInfo() async {
|
||
showDialog(
|
||
context: context,
|
||
barrierDismissible: false,
|
||
builder: (BuildContext context) {
|
||
return const AlertDialog(
|
||
content: Row(
|
||
children: [
|
||
CircularProgressIndicator(),
|
||
SizedBox(width: 16),
|
||
Text('正在获取服务器信息...'),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
);
|
||
|
||
try {
|
||
final response = await HttpClient.get('poe/load.php');
|
||
|
||
if (!mounted) return;
|
||
|
||
Navigator.of(context).pop();
|
||
|
||
if (response.isSuccess) {
|
||
try {
|
||
final data = response.jsonData;
|
||
if (data['status'] == 'success') {
|
||
_displayServerInfoDialog(data);
|
||
} else {
|
||
if (mounted) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text('服务器返回错误状态: ${data['status']}'),
|
||
backgroundColor: Colors.red,
|
||
duration: const Duration(seconds: 3),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
if (mounted) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text('解析服务器数据失败: $e'),
|
||
backgroundColor: Colors.red,
|
||
duration: const Duration(seconds: 3),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
} else {
|
||
if (mounted) {
|
||
final errorMsg =
|
||
'获取服务器信息失败\n'
|
||
'状态码: ${response.statusCode}\n'
|
||
'消息: ${response.message}';
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text(errorMsg),
|
||
backgroundColor: Colors.red,
|
||
duration: const Duration(seconds: 5),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
if (!mounted) return;
|
||
|
||
Navigator.of(context).pop();
|
||
if (mounted) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text('获取服务器信息异常: $e'),
|
||
backgroundColor: Colors.red,
|
||
duration: const Duration(seconds: 5),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
void _displayServerInfoDialog(Map<String, dynamic> data) {
|
||
final server = data['server'] as Map<String, dynamic>?;
|
||
final network = data['network'] as Map<String, dynamic>?;
|
||
final timestamp = data['timestamp'] as Map<String, dynamic>?;
|
||
|
||
final load = server?['load'] as Map<String, dynamic>?;
|
||
final latency = network?['latency'] as List<dynamic>?;
|
||
final serverResponseTime = network?['server_response_time'];
|
||
|
||
showDialog(
|
||
context: context,
|
||
builder: (BuildContext context) {
|
||
return AlertDialog(
|
||
title: Row(
|
||
children: [
|
||
Icon(Icons.cloud, color: AppConstants.primaryColor, size: 20),
|
||
const SizedBox(width: 8),
|
||
const Text('服务器信息', style: TextStyle(fontSize: 18)),
|
||
],
|
||
),
|
||
content: SingleChildScrollView(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
const Text(
|
||
'广州 server-ls',
|
||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||
),
|
||
const SizedBox(height: 12),
|
||
_buildInfoSection('⏰ 服务器时间', timestamp?['datetime'] ?? '--'),
|
||
const SizedBox(height: 12),
|
||
_buildInfoSection(
|
||
'📊 服务器负载',
|
||
'1分钟: ${load?['1min'] ?? '--'} | 5分钟: ${load?['5min'] ?? '--'} | 15分钟: ${load?['15min'] ?? '--'}',
|
||
),
|
||
const SizedBox(height: 12),
|
||
_buildInfoSection(
|
||
'⚡ 服务器响应',
|
||
'${serverResponseTime ?? '--'} ms',
|
||
),
|
||
const SizedBox(height: 16),
|
||
const Text(
|
||
'🌐 网络延迟',
|
||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||
),
|
||
const SizedBox(height: 8),
|
||
if (latency != null)
|
||
...latency.map<Widget>((item) {
|
||
final host = item['host'] as String?;
|
||
final ip = item['ip'] as String?;
|
||
final lat = item['latency'];
|
||
final status = item['status'] as String?;
|
||
return Padding(
|
||
padding: const EdgeInsets.only(bottom: 4),
|
||
child: Text(
|
||
'• $host ($ip): ${status == 'online' ? '$lat ms' : '离线'}',
|
||
style: const TextStyle(
|
||
fontSize: 12,
|
||
color: Colors.grey,
|
||
),
|
||
),
|
||
);
|
||
}).toList(),
|
||
],
|
||
),
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.of(context).pop(),
|
||
child: const Text('关闭'),
|
||
),
|
||
],
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
Widget _buildInfoSection(String title, String content) {
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
title,
|
||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text(content, style: const TextStyle(fontSize: 12, color: Colors.grey)),
|
||
],
|
||
);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
backgroundColor: const Color(0xFFF5F5F5),
|
||
appBar: AppBar(
|
||
title: Text(
|
||
'离线使用',
|
||
style: TextStyle(
|
||
color: AppConstants.primaryColor,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
backgroundColor: Colors.white,
|
||
elevation: 0,
|
||
centerTitle: true,
|
||
leading: IconButton(
|
||
icon: Icon(Icons.arrow_back, color: AppConstants.primaryColor),
|
||
onPressed: () => Navigator.of(context).pop(),
|
||
),
|
||
actions: [
|
||
IconButton(
|
||
icon: Icon(Icons.cloud_outlined, color: AppConstants.primaryColor),
|
||
onPressed: _showServerInfo,
|
||
tooltip: '服务器信息',
|
||
),
|
||
],
|
||
),
|
||
body: ListView(
|
||
padding: const EdgeInsets.all(16),
|
||
children: [
|
||
// 缓存状态卡片
|
||
Container(
|
||
padding: const EdgeInsets.all(20),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(16),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.black.withValues(alpha: 0.05),
|
||
blurRadius: 10,
|
||
offset: const Offset(0, 2),
|
||
),
|
||
],
|
||
),
|
||
child: Column(
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Icon(
|
||
Icons.download_done,
|
||
color: AppConstants.primaryColor,
|
||
size: 24,
|
||
),
|
||
const SizedBox(width: 12),
|
||
const Text(
|
||
'缓存状态',
|
||
style: TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 16),
|
||
Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'精选诗句: $_poetryCount 条',
|
||
style: const TextStyle(fontSize: 14, color: Colors.grey),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
'答题挑战: $_quizCount 条',
|
||
style: const TextStyle(fontSize: 14, color: Colors.grey),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
const SizedBox(height: 20),
|
||
|
||
// 下载类型选择
|
||
Container(
|
||
padding: const EdgeInsets.all(20),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(16),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.black.withValues(alpha: 0.05),
|
||
blurRadius: 10,
|
||
offset: const Offset(0, 2),
|
||
),
|
||
],
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Icon(
|
||
Icons.category,
|
||
color: AppConstants.primaryColor,
|
||
size: 20,
|
||
),
|
||
const SizedBox(width: 8),
|
||
const Text(
|
||
'下载类型',
|
||
style: TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 16),
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
_buildTypeOption(DownloadType.poetry, '精选诗句'),
|
||
_buildTypeOption(DownloadType.quiz, '答题挑战'),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
const SizedBox(height: 20),
|
||
|
||
// 下载数量选择
|
||
Container(
|
||
padding: const EdgeInsets.all(20),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(16),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.black.withValues(alpha: 0.05),
|
||
blurRadius: 10,
|
||
offset: const Offset(0, 2),
|
||
),
|
||
],
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Icon(
|
||
Icons.settings,
|
||
color: AppConstants.primaryColor,
|
||
size: 20,
|
||
),
|
||
const SizedBox(width: 8),
|
||
const Text(
|
||
'下载数量',
|
||
style: TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 16),
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: _selectedType == DownloadType.poetry
|
||
? [
|
||
_buildCountOption(20),
|
||
_buildCountOption(30),
|
||
_buildCountOption(60),
|
||
_buildCountOption(100),
|
||
]
|
||
: [
|
||
_buildCountOption(20),
|
||
_buildCountOption(50),
|
||
_buildCountOption(80),
|
||
_buildCountOption(100),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
const SizedBox(height: 20),
|
||
|
||
// 下载/取消按钮
|
||
SizedBox(
|
||
width: double.infinity,
|
||
child: ElevatedButton(
|
||
onPressed: _isLoading
|
||
? (_isCancelling ? null : _cancelDownload)
|
||
: _downloadOfflineData,
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: _isCancelling
|
||
? Colors.red
|
||
: AppConstants.primaryColor,
|
||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
),
|
||
child: _isCancelling
|
||
? const SizedBox(
|
||
height: 20,
|
||
width: 20,
|
||
child: CircularProgressIndicator(
|
||
strokeWidth: 2,
|
||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||
),
|
||
)
|
||
: Text(
|
||
_isLoading ? '取消下载' : '开始下载',
|
||
style: const TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
|
||
const SizedBox(height: 16),
|
||
|
||
// 清空按钮
|
||
SizedBox(
|
||
width: double.infinity,
|
||
child: OutlinedButton(
|
||
onPressed: _clearOfflineData,
|
||
style: OutlinedButton.styleFrom(
|
||
foregroundColor: Colors.red,
|
||
side: const BorderSide(color: Colors.red),
|
||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
),
|
||
child: const Text(
|
||
'清空缓存',
|
||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||
),
|
||
),
|
||
),
|
||
|
||
const SizedBox(height: 20),
|
||
|
||
// 进度条
|
||
if (_isLoading) ...[
|
||
Container(
|
||
padding: const EdgeInsets.all(20),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(16),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.black.withValues(alpha: 0.05),
|
||
blurRadius: 10,
|
||
offset: const Offset(0, 2),
|
||
),
|
||
],
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
_status,
|
||
style: const TextStyle(fontSize: 14, color: Colors.grey),
|
||
),
|
||
const SizedBox(height: 12),
|
||
LinearProgressIndicator(
|
||
value: _progress / 100,
|
||
backgroundColor: Colors.grey[200],
|
||
valueColor: AlwaysStoppedAnimation<Color>(
|
||
AppConstants.primaryColor,
|
||
),
|
||
borderRadius: BorderRadius.circular(10),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Align(
|
||
alignment: Alignment.centerRight,
|
||
child: Text(
|
||
'$_progress%',
|
||
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
|
||
const SizedBox(height: 20),
|
||
|
||
// 说明信息
|
||
Container(
|
||
padding: const EdgeInsets.all(16),
|
||
decoration: BoxDecoration(
|
||
color: Colors.blue.withValues(alpha: 0.05),
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: Colors.blue.withValues(alpha: 0.2)),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Icon(Icons.info_outline, color: Colors.blue[600], size: 16),
|
||
const SizedBox(width: 8),
|
||
Text(
|
||
'温馨提示',
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w600,
|
||
color: Colors.blue[600],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
const Text(
|
||
'• 开启离线模式后,将会循环加载本地的数据源',
|
||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||
),
|
||
const Text(
|
||
'• 下载的数据将保存在本地,可在无网络时使用',
|
||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||
),
|
||
const Text(
|
||
'• 下载过程中请保持网络连接',
|
||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||
),
|
||
const Text(
|
||
'• 缓存数据不会写入历史记录',
|
||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||
),
|
||
const Text(
|
||
'• 建议在WiFi环境下下载较多数据',
|
||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildTypeOption(DownloadType type, String label) {
|
||
final isSelected = _selectedType == type;
|
||
return GestureDetector(
|
||
onTap: () {
|
||
setState(() {
|
||
_selectedType = type;
|
||
// 重置为默认数量
|
||
_selectedCount = type == DownloadType.poetry ? 30 : 50;
|
||
});
|
||
// 重新加载缓存数量
|
||
_loadCachedCount();
|
||
},
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||
decoration: BoxDecoration(
|
||
color: isSelected ? AppConstants.primaryColor : Colors.grey[100],
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(
|
||
color: isSelected ? AppConstants.primaryColor : Colors.grey[300]!,
|
||
),
|
||
),
|
||
child: Text(
|
||
label,
|
||
style: TextStyle(
|
||
color: isSelected ? Colors.white : Colors.black,
|
||
fontSize: 14,
|
||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildCountOption(int count) {
|
||
final isSelected = _selectedCount == count;
|
||
return Stack(
|
||
children: [
|
||
GestureDetector(
|
||
onTap: () {
|
||
setState(() {
|
||
_selectedCount = count;
|
||
});
|
||
},
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||
decoration: BoxDecoration(
|
||
color: isSelected ? AppConstants.primaryColor : Colors.grey[100],
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(
|
||
color: isSelected
|
||
? AppConstants.primaryColor
|
||
: Colors.grey[300]!,
|
||
),
|
||
),
|
||
child: Text(
|
||
'$count条',
|
||
style: TextStyle(
|
||
color: isSelected ? Colors.white : Colors.black,
|
||
fontSize: 12,
|
||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
// 为100条添加标记(在卡片外面右上角)
|
||
if (count == 100)
|
||
Positioned(
|
||
top: -6,
|
||
right: -6,
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||
decoration: BoxDecoration(
|
||
color: Colors.orange,
|
||
borderRadius: BorderRadius.circular(6),
|
||
),
|
||
child: const Text(
|
||
'体验',
|
||
style: TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 8,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|