Files
wushu/lib/views/home/components/pre-page.dart
2026-04-09 02:23:27 +08:00

1230 lines
38 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.
/// 时间: 2026-04-09
/// 功能: 情景推荐页面
/// 介绍: 展示今日诗词SDK的详细信息包括诗词内容、环境信息等
/// 最新变化: 2026-04-09 添加骨架屏、分享功能
import 'dart:convert';
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import '../../../services/get/theme_controller.dart';
import '../../../services/jinrishici_service.dart';
import '../../../services/network_listener_service.dart';
import '../../../models/colors/app_colors.dart';
import '../../../controllers/history_controller.dart';
import './skeleton_widgets.dart';
class PrePage extends StatefulWidget {
const PrePage({super.key});
@override
State<PrePage> createState() => _PrePageState();
}
class _PrePageState extends State<PrePage> with SingleTickerProviderStateMixin {
final ThemeController _themeController = Get.find<ThemeController>();
final JinrishiciService _jinrishiciService = JinrishiciService();
final GlobalKey _repaintKey = GlobalKey();
late AnimationController _fadeController;
late Animation<double> _fadeAnimation;
Map<String, dynamic>? _poetryData;
Map<String, dynamic>? _userInfo;
bool _isLoading = false;
String? _errorMessage;
bool _isMetadataExpanded = false;
bool _isRefreshLocked = false;
@override
void initState() {
super.initState();
_fadeController = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_fadeAnimation = CurvedAnimation(
parent: _fadeController,
curve: Curves.easeIn,
);
_fadeController.forward();
_loadData();
}
Future<void> _loadData() async {
if (_isRefreshLocked) return;
setState(() {
_isLoading = true;
_errorMessage = null;
_isRefreshLocked = true;
});
try {
final poetry = await _jinrishiciService.getTodayPoetry();
setState(() {
_poetryData = poetry;
_isLoading = false;
});
try {
final userInfo = await _jinrishiciService.getUserInfo();
setState(() {
_userInfo = userInfo;
});
} catch (e) {
print('加载用户信息失败: $e');
}
} catch (e) {
setState(() {
_errorMessage = e.toString();
_isLoading = false;
});
}
Future.delayed(const Duration(seconds: 2), () {
if (mounted) {
setState(() {
_isRefreshLocked = false;
});
}
});
}
Future<void> _captureAndShare() async {
try {
Get.snackbar('提示', '正在生成图片...');
await Future.delayed(const Duration(milliseconds: 100));
if (!mounted) return;
final boundary =
_repaintKey.currentContext?.findRenderObject()
as RenderRepaintBoundary?;
if (boundary == null) {
Get.snackbar('错误', '生成图片失败');
return;
}
if (boundary.debugNeedsPaint) {
await Future.delayed(const Duration(milliseconds: 100));
}
final image = await boundary.toImage(pixelRatio: 3.0);
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
if (byteData == null) {
Get.snackbar('错误', '生成图片失败');
return;
}
final pngBytes = byteData.buffer.asUint8List();
final directory = await getTemporaryDirectory();
final file = File(
'${directory.path}/poetry_${DateTime.now().millisecondsSinceEpoch}.png',
);
await file.writeAsBytes(pngBytes);
final result = await Share.shareXFiles(
[XFile(file.path)],
subject: '情景诗词分享',
text: '来自情景诗词App的分享',
);
if (result.status == ShareResultStatus.success) {
Get.snackbar('成功', '分享成功');
} else if (result.status == ShareResultStatus.dismissed) {
Get.snackbar('提示', '分享已取消');
}
if (await file.exists()) {
await file.delete();
}
} catch (e) {
print('分享失败: $e');
Get.snackbar('错误', '分享失败: $e');
}
}
Future<void> _createNoteFromPoetry() async {
if (_poetryData == null) {
Get.snackbar('提示', '暂无诗词数据');
return;
}
try {
final data = _poetryData!['data'] as Map<String, dynamic>?;
final origin = data?['origin'] as Map<String, dynamic>?;
final title = origin?['title']?.toString() ?? '情景诗词笔记';
final category = '情景诗词';
final contentBuffer = StringBuffer();
if (origin != null) {
if (origin['title'] != null) {
contentBuffer.writeln('标题:${origin['title']}');
}
if (origin['dynasty'] != null) {
contentBuffer.writeln('朝代:${origin['dynasty']}');
}
if (origin['author'] != null) {
contentBuffer.writeln('作者:${origin['author']}');
}
if (data?['content'] != null) {
contentBuffer.writeln('诗句:${data!['content']}');
}
if (origin['content'] != null) {
final fullContent = (origin['content'] as List)
.map((e) => e.toString())
.join('\n');
contentBuffer.writeln('全文:\n$fullContent');
}
if (origin['translate'] != null) {
final translate = (origin['translate'] as List)
.map((e) => e.toString())
.join('\n');
contentBuffer.writeln('译文:\n$translate');
}
}
final noteId = await HistoryController.saveNote(
title: title,
content: contentBuffer.toString().trim(),
category: category,
);
if (noteId != null) {
NetworkListenerService().sendSuccessEvent(
NetworkEventType.noteUpdate,
data: noteId,
);
Get.snackbar(
'成功',
'已创建笔记',
snackPosition: SnackPosition.BOTTOM,
colorText: _themeController.currentThemeColor,
);
}
} catch (e) {
Get.snackbar(
'错误',
'创建笔记失败: $e',
snackPosition: SnackPosition.BOTTOM,
colorText: _themeController.currentThemeColor,
);
}
}
@override
void dispose() {
_fadeController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Obx(() {
final isDark = _themeController.isDarkMode;
final primaryColor = _themeController.currentThemeColor;
return Scaffold(
backgroundColor: isDark ? const Color(0xFF1A1A1A) : Colors.grey[50],
appBar: AppBar(
title: Text(
'情景推荐',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 17,
color: primaryColor,
),
),
backgroundColor: isDark ? const Color(0xFF2A2A2A) : Colors.white,
foregroundColor: primaryColor,
elevation: 0,
centerTitle: true,
actions: [
GestureDetector(
onTap: _isRefreshLocked ? null : _loadData,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
if (_isLoading)
SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
isDark ? Colors.grey[500]! : Colors.grey[400]!,
),
),
)
else
Icon(
Icons.refresh,
color: _isRefreshLocked
? (isDark ? Colors.grey[500] : Colors.grey[400])
: primaryColor,
size: 20,
),
const SizedBox(width: 4),
Text(
'刷新',
style: TextStyle(
fontSize: 14,
color: _isRefreshLocked
? (isDark ? Colors.grey[500] : Colors.grey[400])
: primaryColor,
),
),
],
),
),
),
],
),
body: FadeTransition(
opacity: _fadeAnimation,
child: _buildBody(isDark, primaryColor),
),
);
});
}
Widget _buildBody(bool isDark, Color primaryColor) {
if (_isLoading && _poetryData == null) {
return _buildSkeletonBody(isDark, primaryColor);
}
if (_errorMessage != null && _poetryData == null) {
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 64, color: Colors.red[400]),
const SizedBox(height: 16),
Text(
'加载失败',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: isDark ? Colors.white : Colors.black87,
),
),
const SizedBox(height: 8),
Text(
_errorMessage!,
style: TextStyle(
fontSize: 14,
color: isDark ? Colors.grey[400] : Colors.grey[600],
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: _loadData,
icon: const Icon(Icons.refresh),
label: const Text('重试'),
style: ElevatedButton.styleFrom(
backgroundColor: primaryColor,
foregroundColor: Colors.white,
),
),
],
),
),
);
}
return ListView(
padding: const EdgeInsets.all(16),
children: [
RepaintBoundary(
key: _repaintKey,
child: _buildPoetryCard(isDark, primaryColor),
),
if (_userInfo != null) ...[
const SizedBox(height: 16),
_buildUserInfoCard(isDark, primaryColor),
],
const SizedBox(height: 100),
],
);
}
Widget _buildSkeletonBody(bool isDark, Color primaryColor) {
final baseColor = isDark ? const Color(0xFF3A3A3A) : Colors.grey[300];
final highlightColor = isDark ? const Color(0xFF4A4A4A) : Colors.grey[100];
return ListView(
padding: const EdgeInsets.all(16),
children: [
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF2A2A2A) : Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(isDark ? 40 : 10),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
SkeletonContainer(
width: 40,
height: 40,
borderRadius: 12,
baseColor: baseColor,
highlightColor: highlightColor,
),
const SizedBox(width: 12),
SkeletonContainer(
width: 100,
height: 20,
borderRadius: 4,
baseColor: baseColor,
highlightColor: highlightColor,
),
],
),
const SizedBox(height: 16),
SkeletonContainer(
width: 180,
height: 24,
borderRadius: 4,
baseColor: baseColor,
highlightColor: highlightColor,
),
const SizedBox(height: 12),
SkeletonContainer(
width: 120,
height: 16,
borderRadius: 4,
baseColor: baseColor,
highlightColor: highlightColor,
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF1A1A1A) : Colors.grey[100],
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
SkeletonContainer(
width: double.infinity,
height: 20,
borderRadius: 4,
baseColor: baseColor,
highlightColor: highlightColor,
),
const SizedBox(height: 8),
SkeletonContainer(
width: double.infinity,
height: 20,
borderRadius: 4,
baseColor: baseColor,
highlightColor: highlightColor,
),
const SizedBox(height: 8),
SkeletonContainer(
width: 150,
height: 20,
borderRadius: 4,
baseColor: baseColor,
highlightColor: highlightColor,
),
],
),
),
const SizedBox(height: 16),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
SkeletonContainer(
width: 60,
height: 24,
borderRadius: 12,
baseColor: baseColor,
highlightColor: highlightColor,
),
SkeletonContainer(
width: 80,
height: 24,
borderRadius: 12,
baseColor: baseColor,
highlightColor: highlightColor,
),
SkeletonContainer(
width: 70,
height: 24,
borderRadius: 12,
baseColor: baseColor,
highlightColor: highlightColor,
),
],
),
],
),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF2A2A2A) : Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(isDark ? 40 : 10),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
SkeletonContainer(
width: 40,
height: 40,
borderRadius: 12,
baseColor: baseColor,
highlightColor: highlightColor,
),
const SizedBox(width: 12),
SkeletonContainer(
width: 80,
height: 20,
borderRadius: 4,
baseColor: baseColor,
highlightColor: highlightColor,
),
],
),
const SizedBox(height: 16),
SkeletonContainer(
width: double.infinity,
height: 16,
borderRadius: 4,
baseColor: baseColor,
highlightColor: highlightColor,
),
const SizedBox(height: 8),
SkeletonContainer(
width: double.infinity,
height: 16,
borderRadius: 4,
baseColor: baseColor,
highlightColor: highlightColor,
),
],
),
),
],
);
}
Widget _buildPoetryCard(bool isDark, Color primaryColor) {
if (_poetryData == null) {
return _buildSectionCard(
'情景诗词',
Icons.book,
[
Center(
child: Padding(
padding: const EdgeInsets.all(20),
child: Text(
'暂无数据',
style: TextStyle(
color: isDark ? Colors.grey[400] : Colors.grey[600],
),
),
),
),
],
isDark,
primaryColor,
);
}
final data = _poetryData!['data'] as Map<String, dynamic>?;
final origin = data?['origin'] as Map<String, dynamic>?;
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [primaryColor.withAlpha(30), primaryColor.withAlpha(10)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: primaryColor.withAlpha(50), width: 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: primaryColor.withAlpha(20),
borderRadius: BorderRadius.circular(12),
),
child: const Center(
child: Text('📜', style: TextStyle(fontSize: 20)),
),
),
const SizedBox(width: 12),
Text(
'情景诗词',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: isDark ? Colors.white : Colors.black87,
),
),
],
),
Row(
children: [
IconButton(
onPressed: _captureAndShare,
icon: Icon(Icons.share, color: primaryColor, size: 22),
tooltip: '分享',
),
IconButton(
onPressed: _createNoteFromPoetry,
icon: Icon(Icons.note_add, color: primaryColor, size: 22),
tooltip: '写入笔记',
),
],
),
],
),
const SizedBox(height: 16),
if (origin != null) ...[
Text(
origin['title'] ?? '未知标题',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: isDark ? Colors.white : Colors.black87,
),
),
const SizedBox(height: 8),
Row(
children: [
Text(
origin['dynasty'] ?? '未知朝代',
style: TextStyle(
fontSize: 14,
color: primaryColor,
fontWeight: FontWeight.w500,
),
),
const SizedBox(width: 8),
Text(
'·',
style: TextStyle(
fontSize: 14,
color: isDark ? Colors.grey[400] : Colors.grey[600],
),
),
const SizedBox(width: 8),
Text(
origin['author'] ?? '未知作者',
style: TextStyle(
fontSize: 14,
color: isDark ? Colors.grey[300] : Colors.grey[700],
),
),
],
),
const SizedBox(height: 16),
],
if (data != null) ...[
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isDark
? const Color(0xFF1A1A1A).withAlpha(50)
: Colors.white.withAlpha(50),
borderRadius: BorderRadius.circular(12),
),
child: Text(
data['content'] ?? '暂无内容',
style: TextStyle(
fontSize: 16,
color: isDark ? Colors.white : Colors.black87,
height: 1.8,
letterSpacing: 0.5,
),
textAlign: TextAlign.center,
),
),
],
if (origin != null && origin['content'] != null) ...[
const SizedBox(height: 16),
_buildContentBlock(
'完整诗词',
(origin['content'] as List).map((e) => e.toString()).join('\n'),
isDark,
primaryColor,
),
],
if (origin != null && origin['translate'] != null) ...[
const SizedBox(height: 12),
_buildContentBlock(
'翻译',
(origin['translate'] as List).map((e) => e.toString()).join('\n'),
isDark,
primaryColor,
),
],
if (data != null && data['matchTags'] != null) ...[
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: (data['matchTags'] as List)
.map<Widget>(
(tag) => Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: primaryColor.withAlpha(15),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: primaryColor.withAlpha(30)),
),
child: Text(
tag.toString(),
style: TextStyle(
fontSize: 12,
color: primaryColor,
fontWeight: FontWeight.w500,
),
),
),
)
.toList(),
),
],
const SizedBox(height: 16),
if (data != null) _buildPoetryMetadata(data, isDark, primaryColor),
],
),
);
}
Widget _buildPoetryMetadata(
Map<String, dynamic> data,
bool isDark,
Color primaryColor,
) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isDark
? const Color(0xFF1A1A1A).withAlpha(50)
: Colors.grey[100]!.withAlpha(128),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
setState(() {
_isMetadataExpanded = !_isMetadataExpanded;
});
},
behavior: HitTestBehavior.opaque,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text(
'流行度',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: primaryColor,
),
),
const SizedBox(width: 8),
Text(
data['popularity'] != null
? _formatNumber(data['popularity'])
: '-',
style: TextStyle(
fontSize: 13,
color: isDark ? Colors.grey[300] : Colors.grey[700],
),
),
],
),
Row(
children: [
Text(
'诗词信息',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: primaryColor,
),
),
const SizedBox(width: 4),
Icon(
_isMetadataExpanded
? Icons.keyboard_arrow_up
: Icons.keyboard_arrow_down,
color: primaryColor,
size: 20,
),
],
),
],
),
),
if (_isMetadataExpanded) ...[
const SizedBox(height: 8),
const Divider(height: 1),
const SizedBox(height: 8),
if (data['id'] != null)
_buildMetadataRow('ID', data['id'].toString(), isDark),
if (data['cacheAt'] != null)
_buildMetadataRow('缓存时间', data['cacheAt'].toString(), isDark),
if (data['recommendedReason'] != null &&
data['recommendedReason'].toString().isNotEmpty)
_buildMetadataRow(
'推荐理由',
data['recommendedReason'].toString(),
isDark,
),
if (_poetryData!['token'] != null)
_buildMetadataRow(
'Token',
_poetryData!['token'].toString().substring(
0,
(_poetryData!['token'].toString().length > 20
? 20
: _poetryData!['token'].toString().length),
) +
'...',
isDark,
),
if (_poetryData!['ipAddress'] != null)
_buildMetadataRow(
'IP 地址',
_poetryData!['ipAddress'].toString(),
isDark,
),
],
],
),
);
}
Widget _buildMetadataRow(String label, String value, bool isDark) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 80,
child: Text(
label,
style: TextStyle(
fontSize: 12,
color: isDark ? Colors.grey[400] : Colors.grey[600],
),
),
),
Expanded(
child: Text(
value,
style: TextStyle(
fontSize: 12,
color: isDark ? Colors.grey[300] : Colors.grey[700],
),
),
),
],
),
);
}
Widget _buildUserInfoCard(bool isDark, Color primaryColor) {
final data = _userInfo?['data'] as Map<String, dynamic>?;
if (data == null) return const SizedBox.shrink();
final weatherData = data['weatherData'] as Map<String, dynamic>?;
final tags = data['tags'] as List<dynamic>?;
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [primaryColor.withAlpha(25), primaryColor.withAlpha(10)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: primaryColor.withAlpha(40), width: 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: primaryColor.withAlpha(20),
borderRadius: BorderRadius.circular(12),
),
child: const Center(
child: Text('🌤️', style: TextStyle(fontSize: 20)),
),
),
const SizedBox(width: 12),
Text(
'环境信息',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: isDark ? Colors.white : Colors.black87,
),
),
],
),
const SizedBox(height: 16),
_buildUserInfoRow('IP 地址', data['ip']?.toString() ?? '未知', isDark),
_buildUserInfoRow('地区', data['region']?.toString() ?? '未知', isDark),
if (data['beijingTime'] != null)
_buildUserInfoRow('北京时间', data['beijingTime'].toString(), isDark),
if (weatherData != null) ...[
const SizedBox(height: 12),
_buildWeatherInfo(weatherData, isDark, primaryColor),
],
if (tags != null && tags.isNotEmpty) ...[
const SizedBox(height: 12),
_buildTags(tags, isDark, primaryColor),
],
],
),
);
}
Widget _buildUserInfoRow(String label, String value, bool isDark) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 80,
child: Text(
label,
style: TextStyle(
fontSize: 13,
color: isDark ? Colors.grey[400] : Colors.grey[600],
fontWeight: FontWeight.w500,
),
),
),
Expanded(
child: Text(
value,
style: TextStyle(
fontSize: 13,
color: isDark ? Colors.white : Colors.black87,
),
),
),
],
),
);
}
Widget _buildWeatherInfo(
Map<String, dynamic> weatherData,
bool isDark,
Color primaryColor,
) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isDark
? const Color(0xFF1A1A1A).withAlpha(50)
: Colors.white.withAlpha(50),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.cloud, color: primaryColor, size: 16),
const SizedBox(width: 6),
Text(
'天气信息',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: isDark ? Colors.white : Colors.black87,
),
),
],
),
const SizedBox(height: 8),
Wrap(
spacing: 12,
runSpacing: 8,
children: [
_buildWeatherItem(
'温度',
'${weatherData['temperature']?.toString() ?? '-'}°C',
isDark,
),
_buildWeatherItem(
'天气',
weatherData['weather']?.toString() ?? '-',
isDark,
),
_buildWeatherItem(
'湿度',
'${weatherData['humidity']?.toString() ?? '-'}%',
isDark,
),
_buildWeatherItem(
'风向',
weatherData['windDirection']?.toString() ?? '-',
isDark,
),
_buildWeatherItem(
'风力',
'${weatherData['windPower']?.toString() ?? '-'}',
isDark,
),
_buildWeatherItem(
'能见度',
weatherData['visibility']?.toString() ?? '-',
isDark,
),
if (weatherData['rainfall'] != null)
_buildWeatherItem(
'降雨量',
'${weatherData['rainfall']}mm',
isDark,
),
if (weatherData['pm25'] != null)
_buildWeatherItem('PM2.5', '${weatherData['pm25']}', isDark),
],
),
],
),
);
}
Widget _buildWeatherItem(String label, String value, bool isDark) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'$label: ',
style: TextStyle(
fontSize: 12,
color: isDark ? Colors.grey[400] : Colors.grey[600],
),
),
Text(
value,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: isDark ? Colors.white : Colors.black87,
),
),
],
);
}
Widget _buildTags(List<dynamic> tags, bool isDark, Color primaryColor) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.local_offer, color: primaryColor, size: 16),
const SizedBox(width: 6),
Text(
'推荐标签',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: isDark ? Colors.white : Colors.black87,
),
),
],
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: tags.map<Widget>((tag) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: primaryColor.withAlpha(15),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: primaryColor.withAlpha(30)),
),
child: Text(
tag.toString(),
style: TextStyle(
fontSize: 12,
color: primaryColor,
fontWeight: FontWeight.w500,
),
),
);
}).toList(),
),
],
);
}
Widget _buildSectionCard(
String title,
IconData icon,
List<Widget> children,
bool isDark,
Color primaryColor,
) {
return Container(
decoration: BoxDecoration(
color: isDark ? const Color(0xFF2A2A2A) : Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: isDark
? Colors.black.withAlpha(76)
: Colors.black.withAlpha(26),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Icon(icon, color: primaryColor, size: 20),
const SizedBox(width: 8),
Text(
title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: isDark ? Colors.white : Colors.black87,
),
),
],
),
),
...children,
],
),
);
}
Widget _buildContentBlock(
String title,
String content,
bool isDark,
Color primaryColor,
) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isDark
? const Color(0xFF1A1A1A).withAlpha(50)
: Colors.grey[100]!.withAlpha(128),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: primaryColor,
),
),
const SizedBox(height: 8),
...(content.split('\n')).map<Widget>(
(line) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
line,
style: TextStyle(
fontSize: 14,
color: isDark ? Colors.grey[300] : Colors.grey[700],
height: 1.6,
),
textAlign: TextAlign.center,
),
),
),
],
),
);
}
String _formatNumber(dynamic number) {
if (number is int) {
if (number >= 10000) {
return '${(number / 10000).toStringAsFixed(1)}';
}
return number.toString();
}
return number.toString();
}
}