Files
wushu/lib/views/profile/settings/offline-data.dart
2026-03-30 02:35:31 +08:00

1049 lines
33 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: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,
),
),
),
),
],
);
}
}