ui细节优化

This commit is contained in:
Developer
2026-04-01 04:45:33 +08:00
parent 6517a78c7e
commit 79f7269319
23 changed files with 3299 additions and 885 deletions

View File

@@ -1,10 +1,11 @@
import 'package:flutter/material.dart';
import '../../constants/app_constants.dart';
import 'tags/corr_page.dart';
/// 时间: 2026-03-25
/// 时间: 2026-04-01
/// 功能: 分类页面
/// 介绍: 展示诗词分类,包括场景分类和朝代分类
/// 最新变化: 新建分类页面,支持场景和朝代两大分类
/// 最新变化: 重新设计iOS风格布局减少间距加大字体显示分类数量
class CategoryPage extends StatefulWidget {
const CategoryPage({super.key});
@@ -16,7 +17,10 @@ class CategoryPage extends StatefulWidget {
class _CategoryPageState extends State<CategoryPage>
with SingleTickerProviderStateMixin {
late TabController _tabController;
final List<String> _tabCategories = ['场景分类', '朝代分类'];
final List<Map<String, dynamic>> _tabCategories = [
{'label': '场景分类', 'icon': Icons.category},
{'label': '朝代分类', 'icon': Icons.history},
];
static const sceneData = {
"节日": ["七夕节", "中秋节", "元宵节", "寒食节", "清明节", "端午节", "重阳节", "春节", "节日"],
@@ -90,76 +94,131 @@ class _CategoryPageState extends State<CategoryPage>
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: Column(
children: [
// 自定义标题栏
// Tab栏
Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: TabBar(
controller: _tabController,
tabs: _tabCategories
.map((category) => Tab(text: category))
.toList(),
labelColor: AppConstants.primaryColor,
unselectedLabelColor: Colors.grey[600],
indicatorColor: AppConstants.primaryColor,
indicatorWeight: 3,
labelStyle: const TextStyle(fontWeight: FontWeight.bold),
return Column(
children: [
// Tab栏
Container(
color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: TabBar(
controller: _tabController,
tabs: _tabCategories
.map(
(category) => Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(category['icon'], size: 18),
const SizedBox(width: 6),
Text(category['label']),
],
),
),
)
.toList(),
labelColor: AppConstants.primaryColor,
unselectedLabelColor: Colors.grey[600],
indicatorColor: AppConstants.primaryColor,
indicatorWeight: 3,
labelStyle: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
),
unselectedLabelStyle: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 16,
),
),
// 内容区域
Expanded(
),
Container(height: 0.5, color: const Color(0xFFE5E5EA)),
// 内容区域
Expanded(
child: Container(
color: const Color(0xFFF2F2F7),
child: TabBarView(
controller: _tabController,
children: [
_buildCategoryGrid(sceneData, '场景分类'),
_buildCategoryGrid(dynastyData, '朝代分类'),
_buildCategoryList(sceneData, _tabCategories[0]['label']),
_buildCategoryList(dynastyData, _tabCategories[1]['label']),
],
),
),
],
),
),
],
);
}
Widget _buildCategoryGrid(
Widget _buildCategoryList(
Map<String, List<String>> data,
String categoryType,
) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: ListView.builder(
itemCount: data.keys.length,
itemBuilder: (context, index) {
final category = data.keys.elementAt(index);
final items = data[category]!;
return ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: data.keys.length,
separatorBuilder: (context, index) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final category = data.keys.elementAt(index);
final items = data[category]!;
return Card(
margin: const EdgeInsets.only(bottom: 16.0),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
category,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
),
Row(
children: [
Expanded(
child: Text(
category,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.black,
),
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration(
color: AppConstants.primaryColor.withValues(
alpha: 0.1,
),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${items.length}',
style: TextStyle(
fontSize: 13,
color: AppConstants.primaryColor,
fontWeight: FontWeight.w600,
),
),
),
],
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
spacing: 10,
runSpacing: 10,
children: items.map((item) {
return _buildCategoryChip(item, categoryType);
}).toList(),
@@ -167,38 +226,38 @@ class _CategoryPageState extends State<CategoryPage>
],
),
),
);
},
),
),
);
},
);
}
Widget _buildCategoryChip(String label, String categoryType) {
return GestureDetector(
onTap: () {
// TODO: 跳转到对应分类的诗词列表页面
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('点击了 $label'),
duration: const Duration(seconds: 1),
final searchType = categoryType == '朝代分类' ? 'alias' : 'keywords';
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => CorrPage(label: label, searchType: searchType),
),
);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16),
color: AppConstants.primaryColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Theme.of(context).primaryColor.withValues(alpha: 0.3),
color: AppConstants.primaryColor.withValues(alpha: 0.3),
width: 1,
),
),
child: Text(
label,
style: TextStyle(
color: Theme.of(context).primaryColor,
fontSize: 12,
color: AppConstants.primaryColor,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),

View File

@@ -3,6 +3,8 @@ import '../../constants/app_constants.dart';
import '../../utils/http/http_client.dart';
import '../../models/poetry_model.dart';
import '../../controllers/load/locally.dart';
import '../../controllers/history_controller.dart';
import '../../services/network_listener_service.dart';
/// 时间: 2026-03-25
/// 功能: 热门页面
@@ -19,7 +21,11 @@ class PopularPage extends StatefulWidget {
class _PopularPageState extends State<PopularPage>
with SingleTickerProviderStateMixin {
late TabController _tabController;
final List<String> _tabCategories = ['总榜', '日榜', '月榜'];
final List<Map<String, dynamic>> _tabCategories = [
{'label': '总榜', 'icon': Icons.bar_chart},
{'label': '日榜', 'icon': Icons.today},
{'label': '月榜', 'icon': Icons.calendar_today},
];
List<PoetryModel> _rankList = [];
bool _loading = false;
@@ -46,36 +52,46 @@ class _PopularPageState extends State<PopularPage>
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: Column(
children: [
// Tab栏
Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: TabBar(
controller: _tabController,
tabs: _tabCategories
.map((category) => Tab(text: category))
.toList(),
labelColor: AppConstants.primaryColor,
unselectedLabelColor: Colors.grey[600],
indicatorColor: AppConstants.primaryColor,
indicatorWeight: 3,
labelStyle: const TextStyle(fontWeight: FontWeight.bold),
),
return Column(
children: [
// 黑色分割线
Container(height: 1, color: Colors.black.withValues(alpha: 0.1)),
// Tab栏
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: TabBar(
controller: _tabController,
tabs: _tabCategories
.map(
(category) => Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(category['icon'], size: 16),
const SizedBox(width: 6),
Text(category['label']),
],
),
),
)
.toList(),
labelColor: AppConstants.primaryColor,
unselectedLabelColor: Colors.grey[600],
indicatorColor: AppConstants.primaryColor,
indicatorWeight: 3,
labelStyle: const TextStyle(fontWeight: FontWeight.bold),
),
// 内容区域
Expanded(
child: TabBarView(
controller: _tabController,
children: _tabCategories.map((category) {
return _buildRankContent(category);
}).toList(),
),
),
// 内容区域
Expanded(
child: TabBarView(
controller: _tabController,
children: _tabCategories.map((category) {
return _buildRankContent(category['label']);
}).toList(),
),
],
),
),
],
);
}
@@ -158,8 +174,50 @@ class _PopularPageState extends State<PopularPage>
);
}
Future<void> _createNoteFromPoetry(PoetryModel poetry) async {
try {
final title = poetry.name.isNotEmpty ? poetry.name : '诗词笔记';
final category = poetry.alias.isNotEmpty ? poetry.alias : '诗词';
final contentBuffer = StringBuffer();
if (poetry.url.isNotEmpty) {
contentBuffer.writeln('出处:${poetry.url}');
}
if (poetry.name.isNotEmpty) {
contentBuffer.writeln('诗句:${poetry.name}');
}
if (poetry.alias.isNotEmpty) {
contentBuffer.writeln('朝代:${poetry.alias}');
}
final noteId = await HistoryController.saveNote(
title: title,
content: contentBuffer.toString().trim(),
category: category,
);
if (noteId != null) {
NetworkListenerService().sendSuccessEvent(
NetworkEventType.noteUpdate,
data: noteId,
);
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('已创建笔记')));
}
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('创建笔记失败: $e')));
}
}
}
Widget _buildRankItem(PoetryModel poetry, int index) {
final rank = poetry.rank > 0 ? poetry.rank : index; // 优先使用API返回的rank
final rank = poetry.rank > 0 ? poetry.rank : index;
final isTopThree = rank <= 3;
return Card(
@@ -179,120 +237,177 @@ class _PopularPageState extends State<PopularPage>
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
child: Stack(
clipBehavior: Clip.none,
children: [
// 排名徽章
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: isTopThree
? AppConstants.primaryColor
: AppConstants.primaryColor.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(20),
border: isTopThree
? null
: Border.all(
color: AppConstants.primaryColor.withValues(
alpha: 0.2,
),
width: 1,
),
),
child: Center(
child: Text(
rank.toString(),
style: TextStyle(
Row(
children: [
// 排名徽章
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: isTopThree
? Colors.white
: AppConstants.primaryColor.withValues(alpha: 0.8),
fontWeight: FontWeight.bold,
fontSize: 16,
? AppConstants.primaryColor
: AppConstants.primaryColor.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(20),
border: isTopThree
? null
: Border.all(
color: AppConstants.primaryColor.withValues(
alpha: 0.2,
),
width: 1,
),
),
child: Center(
child: Text(
rank.toString(),
style: TextStyle(
color: isTopThree
? Colors.white
: AppConstants.primaryColor.withValues(
alpha: 0.8,
),
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
),
),
),
const SizedBox(width: 12),
const SizedBox(width: 12),
// 内容区域
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
Text(
poetry.name,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
// 朝代和作者
if (poetry.alias.isNotEmpty || poetry.url.isNotEmpty)
Row(
children: [
if (poetry.alias.isNotEmpty) ...[
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: AppConstants.primaryColor.withValues(
alpha: 0.1,
),
borderRadius: BorderRadius.circular(8),
),
child: Text(
poetry.alias,
style: TextStyle(
fontSize: 10,
color: AppConstants.primaryColor,
),
),
),
const SizedBox(width: 8),
],
if (poetry.url.isNotEmpty)
Expanded(
child: Text(
poetry.url,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 8),
// 统计数据
Row(
// 内容区域
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildStatItem(
'👁',
poetry.hitsTotal.toString(),
'总浏览',
// 标题
Text(
poetry.name,
style: Theme.of(context).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.bold),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(width: 16),
_buildStatItem('💖', poetry.like.toString(), '点赞'),
const SizedBox(width: 16),
if (_tabController.index == 1) // 日榜
_buildStatItem('📅', poetry.hitsDay.toString(), '今日'),
if (_tabController.index == 2) // 月榜
_buildStatItem(
'📊',
poetry.hitsMonth.toString(),
'本月',
const SizedBox(height: 4),
// 朝代和作者
if (poetry.alias.isNotEmpty || poetry.url.isNotEmpty)
Row(
children: [
if (poetry.alias.isNotEmpty) ...[
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: AppConstants.primaryColor.withValues(
alpha: 0.1,
),
borderRadius: BorderRadius.circular(8),
),
child: Text(
poetry.alias,
style: TextStyle(
fontSize: 10,
color: AppConstants.primaryColor,
),
),
),
const SizedBox(width: 8),
],
if (poetry.url.isNotEmpty)
Expanded(
child: Text(
poetry.url,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 8),
// 统计数据
Row(
children: [
_buildStatItem(
'👁',
poetry.hitsTotal.toString(),
'总浏览',
),
const SizedBox(width: 16),
_buildStatItem('💖', poetry.like.toString(), '点赞'),
const SizedBox(width: 16),
if (_tabController.index == 1)
_buildStatItem(
'📅',
poetry.hitsDay.toString(),
'今日',
),
if (_tabController.index == 2)
_buildStatItem(
'📊',
poetry.hitsMonth.toString(),
'本月',
),
],
),
],
),
],
),
],
),
// 添加笔记按钮 - 位于右下角,可遮挡字段
Positioned(
bottom: -8,
right: -8,
child: GestureDetector(
onTap: () => _createNoteFromPoetry(poetry),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
decoration: BoxDecoration(
color: Colors.orange[700],
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
bottomRight: Radius.circular(12),
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.15),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.note_add,
size: 14,
color: Colors.white,
),
const SizedBox(width: 4),
const Text(
'笔记',
style: TextStyle(
fontSize: 11,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
),
],

View File

@@ -0,0 +1,780 @@
/// 时间: 2026-04-01
/// 功能: 标签/朝代诗词列表页面
/// 介绍: 展示指定标签或朝代相关的诗词列表,支持搜索和浏览
/// 最新变化: 新建页面iOS风格设计集成搜索API
library;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../constants/app_constants.dart';
import '../../../utils/http/http_client.dart';
import '../../../controllers/history_controller.dart';
import '../../../services/network_listener_service.dart';
/// 标签诗词列表页面
/// [label] 标签名称或朝代名称
/// [searchType] 搜索类型:'keywords' 标签搜索, 'alias' 朝代搜索
class CorrPage extends StatefulWidget {
final String label;
final String searchType;
const CorrPage({
super.key,
required this.label,
this.searchType = 'keywords',
});
@override
State<CorrPage> createState() => _CorrPageState();
}
class _CorrPageState extends State<CorrPage>
with SingleTickerProviderStateMixin {
List<Map<String, dynamic>> _poetryList = [];
bool _isLoading = true;
bool _isLoadingMore = false;
bool _hasMore = true;
String? _errorMessage;
int _currentPage = 1;
int _totalCount = 0;
int _totalPages = 0;
final int _pageSize = 10;
final ScrollController _scrollController = ScrollController();
late AnimationController _skeletonAnimationController;
late Animation<double> _skeletonAnimation;
@override
void initState() {
super.initState();
_skeletonAnimationController = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
)..repeat(reverse: true);
_skeletonAnimation = Tween<double>(
begin: 0.3,
end: 1.0,
).animate(_skeletonAnimationController);
_loadData();
_scrollController.addListener(_onScroll);
}
@override
void dispose() {
_skeletonAnimationController.dispose();
_scrollController.dispose();
super.dispose();
}
void _onScroll() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200 &&
!_isLoadingMore &&
_hasMore) {
_loadMoreData();
}
}
Future<void> _loadData() async {
setState(() {
_isLoading = true;
_errorMessage = null;
_currentPage = 1;
_hasMore = true;
});
try {
final response = await HttpClient.get(
'searchs.php',
queryParameters: {
'keyword': widget.label,
'mode': 'exact',
'page': _currentPage.toString(),
'size': _pageSize.toString(),
'fields': widget.searchType,
'return_fields':
'id,name,url,alias,keywords,introduce,drtime,like,hits_total',
},
);
if (mounted) {
if (response.isSuccess && response.jsonData['code'] == 0) {
final data = response.jsonData['data'];
final results = data['results'] as List<dynamic>? ?? [];
final pagination = data['pagination'] as Map<String, dynamic>?;
setState(() {
_poetryList = results
.map((item) => item as Map<String, dynamic>)
.toList();
_totalCount = pagination?['total_count'] as int? ?? 0;
_totalPages = pagination?['total_pages'] as int? ?? 0;
_hasMore = results.length >= _pageSize;
_isLoading = false;
});
} else {
setState(() {
_errorMessage = response.jsonData['msg'] ?? '加载失败';
_isLoading = false;
});
}
}
} catch (e) {
if (mounted) {
setState(() {
_errorMessage = '网络错误: $e';
_isLoading = false;
});
}
}
}
Future<void> _loadMoreData() async {
if (_isLoadingMore || !_hasMore) return;
setState(() {
_isLoadingMore = true;
});
try {
final nextPage = _currentPage + 1;
final response = await HttpClient.get(
'searchs.php',
queryParameters: {
'keyword': widget.label,
'mode': 'exact',
'page': nextPage.toString(),
'size': _pageSize.toString(),
'fields': widget.searchType,
'return_fields':
'id,name,url,alias,keywords,introduce,drtime,like,hits_total',
},
);
if (mounted) {
if (response.isSuccess && response.jsonData['code'] == 0) {
final data = response.jsonData['data'];
final results = data['results'] as List<dynamic>? ?? [];
setState(() {
_poetryList.addAll(
results.map((item) => item as Map<String, dynamic>),
);
_currentPage = nextPage;
_hasMore = results.length >= _pageSize;
_isLoadingMore = false;
});
} else {
setState(() {
_isLoadingMore = false;
});
}
}
} catch (e) {
if (mounted) {
setState(() {
_isLoadingMore = false;
});
}
}
}
Future<void> _toggleLike(Map<String, dynamic> poetry) async {
final poetryId = poetry['id'].toString();
final isLiked = await HistoryController.isInLiked(poetryId);
if (isLiked) {
await HistoryController.removeLikedPoetry(poetryId);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('已取消点赞'),
duration: Duration(seconds: 1),
),
);
}
} else {
await HistoryController.addToLiked(poetry);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('已点赞'), duration: Duration(seconds: 1)),
);
}
}
if (mounted) {
setState(() {});
}
}
@override
Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.dark,
child: Scaffold(
backgroundColor: const Color(0xFFF2F2F7),
appBar: _buildAppBar(),
body: _buildBody(),
),
);
}
PreferredSizeWidget _buildAppBar() {
return AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(
Icons.arrow_back_ios,
color: AppConstants.primaryColor,
),
onPressed: () => Navigator.pop(context),
),
title: Column(
children: [
Text(
widget.label,
style: const TextStyle(
color: Colors.black,
fontSize: 17,
fontWeight: FontWeight.w600,
),
),
Text(
widget.searchType == 'alias' ? '朝代' : '标签',
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
fontWeight: FontWeight.normal,
),
),
],
),
centerTitle: true,
actions: [
if (!_isLoading)
Padding(
padding: const EdgeInsets.only(right: 16),
child: Center(
child: Text(
'$_totalCount 篇 / $_totalPages',
style: TextStyle(
color: Colors.grey[600],
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
),
),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(0.5),
child: Container(height: 0.5, color: const Color(0xFFE5E5EA)),
),
);
}
Widget _buildBody() {
if (_isLoading) {
return _buildSkeletonView();
}
if (_errorMessage != null) {
return _buildErrorView();
}
if (_poetryList.isEmpty) {
return _buildEmptyView();
}
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: RefreshIndicator(
key: const ValueKey('list'),
color: AppConstants.primaryColor,
onRefresh: _loadData,
child: ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.all(16),
itemCount: _poetryList.length + (_hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index >= _poetryList.length) {
return _buildLoadingMoreIndicator();
}
return _buildPoetryCard(_poetryList[index]);
},
),
),
);
}
Widget _buildSkeletonView() {
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: 5,
itemBuilder: (context, index) {
return _buildSkeletonCard();
},
);
}
Widget _buildSkeletonCard() {
return AnimatedBuilder(
animation: _skeletonAnimation,
builder: (context, child) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSkeletonLine(180, 20, _skeletonAnimation.value),
const SizedBox(height: 8),
_buildSkeletonLine(
double.infinity,
16,
_skeletonAnimation.value * 0.9,
),
const SizedBox(height: 6),
_buildSkeletonLine(250, 16, _skeletonAnimation.value * 0.8),
const SizedBox(height: 6),
_buildSkeletonLine(200, 16, _skeletonAnimation.value * 0.7),
const SizedBox(height: 12),
Row(
children: [
_buildSkeletonLine(60, 16, _skeletonAnimation.value * 0.6),
const SizedBox(width: 16),
_buildSkeletonLine(60, 16, _skeletonAnimation.value * 0.5),
],
),
],
),
),
);
},
);
}
Widget _buildSkeletonLine(double width, double height, double opacity) {
return Container(
width: width,
height: height,
decoration: BoxDecoration(
color: const Color(
0xFFE5E5EA,
).withValues(alpha: opacity.clamp(0.3, 0.8)),
borderRadius: BorderRadius.circular(4),
),
);
}
Widget _buildErrorView() {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, color: Colors.red[300], size: 64),
const SizedBox(height: 16),
Text(
_errorMessage ?? '加载失败',
style: TextStyle(color: Colors.grey[600], fontSize: 14),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _loadData,
style: ElevatedButton.styleFrom(
backgroundColor: AppConstants.primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
),
child: const Text('重试'),
),
],
),
),
);
}
Widget _buildEmptyView() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.inbox_outlined, color: Colors.grey[400], size: 64),
const SizedBox(height: 16),
Text(
'暂无相关诗词',
style: TextStyle(color: Colors.grey[600], fontSize: 14),
),
const SizedBox(height: 8),
Text(
'试试其他标签或朝代',
style: TextStyle(color: Colors.grey[400], fontSize: 12),
),
],
),
);
}
Widget _buildLoadingMoreIndicator() {
return Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
color: AppConstants.primaryColor,
),
),
),
);
}
Widget _buildPoetryCard(Map<String, dynamic> poetry) {
final name = poetry['name']?.toString() ?? '未知标题';
final url = poetry['url']?.toString() ?? '';
final alias = poetry['alias']?.toString() ?? '';
final introduce = poetry['introduce']?.toString() ?? '';
final like = poetry['like'] ?? 0;
final hitsTotal = poetry['hits_total'] ?? 0;
return Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(12),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () => _showPoetryDetail(poetry),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black,
),
),
),
if (alias.isNotEmpty)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: AppConstants.primaryColor.withValues(
alpha: 0.1,
),
borderRadius: BorderRadius.circular(8),
),
child: Text(
alias,
style: TextStyle(
fontSize: 12,
color: AppConstants.primaryColor,
fontWeight: FontWeight.w500,
),
),
),
],
),
if (url.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
url,
style: TextStyle(fontSize: 13, color: Colors.grey[600]),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
if (introduce.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
introduce,
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
height: 1.5,
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
],
const SizedBox(height: 12),
Row(
children: [
Icon(
Icons.remove_red_eye_outlined,
size: 16,
color: Colors.grey[400],
),
const SizedBox(width: 4),
Text(
'$hitsTotal',
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
),
const SizedBox(width: 16),
Icon(
Icons.favorite_outline,
size: 16,
color: Colors.grey[400],
),
const SizedBox(width: 4),
Text(
'$like',
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
),
const Spacer(),
FutureBuilder<bool>(
future: HistoryController.isInLiked(
poetry['id'].toString(),
),
builder: (context, snapshot) {
final isLiked = snapshot.data ?? false;
return GestureDetector(
onTap: () => _toggleLike(poetry),
child: Icon(
isLiked ? Icons.favorite : Icons.favorite_border,
size: 20,
color: isLiked ? Colors.red : Colors.grey[400],
),
);
},
),
],
),
],
),
),
),
),
);
}
Future<void> _createNoteFromPoetry(Map<String, dynamic> poetry) async {
try {
final url = poetry['url']?.toString() ?? '';
final alias = poetry['alias']?.toString() ?? '';
final name = poetry['name']?.toString() ?? '';
final drtime = poetry['drtime']?.toString() ?? '';
final introduce = poetry['introduce']?.toString() ?? '';
final title = url.isNotEmpty ? url : '诗词笔记';
final category = alias.isNotEmpty ? alias : '诗词';
final contentBuffer = StringBuffer();
if (name.isNotEmpty) {
contentBuffer.writeln(name);
contentBuffer.writeln('');
}
if (drtime.isNotEmpty) {
contentBuffer.writeln(drtime);
contentBuffer.writeln('');
}
if (introduce.isNotEmpty) {
contentBuffer.writeln('译文:');
contentBuffer.writeln(introduce);
}
final noteId = await HistoryController.saveNote(
title: title,
content: contentBuffer.toString().trim(),
category: category,
);
if (noteId != null) {
NetworkListenerService().sendSuccessEvent(
NetworkEventType.noteUpdate,
data: noteId,
);
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('已创建笔记')));
}
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('创建笔记失败: $e')));
}
}
}
void _showPoetryDetail(Map<String, dynamic> poetry) {
final drtime = poetry['drtime']?.toString() ?? '';
final introduce = poetry['introduce']?.toString() ?? '';
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => Container(
height: MediaQuery.of(context).size.height * 0.7,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(20),
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Expanded(
child: Text(
poetry['name']?.toString() ?? '诗词详情',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
],
),
),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (poetry['url']?.toString().isNotEmpty == true) ...[
Text(
poetry['url'].toString(),
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
const SizedBox(height: 16),
],
if (drtime.isNotEmpty) ...[
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFF8F8F8),
borderRadius: BorderRadius.circular(12),
),
child: Text(
drtime,
style: const TextStyle(
fontSize: 16,
height: 1.8,
fontFamily: 'serif',
),
),
),
const SizedBox(height: 20),
],
if (introduce.isNotEmpty) ...[
const Text(
'译文',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 12),
Text(
introduce,
style: TextStyle(
fontSize: 15,
color: Colors.grey[700],
height: 1.8,
),
),
],
const SizedBox(height: 20),
],
),
),
),
SafeArea(
child: Container(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.08),
blurRadius: 4,
offset: const Offset(0, -2),
),
],
),
child: SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () {
Navigator.pop(context);
_createNoteFromPoetry(poetry);
},
icon: const Icon(Icons.note_add, size: 18),
label: const Text('创建笔记'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange[700],
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
),
),
),
),
),
],
),
),
);
}
}