1230 lines
38 KiB
Dart
1230 lines
38 KiB
Dart
/// 时间: 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();
|
||
}
|
||
}
|