ui细节优化
This commit is contained in:
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
780
lib/views/active/tags/corr_page.dart
Normal file
780
lib/views/active/tags/corr_page.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import 'active/active_search_page.dart';
|
||||
import 'active/category_page.dart';
|
||||
import 'active/popular_page.dart';
|
||||
import 'active/rate.dart';
|
||||
import '../controllers/shared_preferences_storage_controller.dart';
|
||||
|
||||
/// 时间: 2025-03-21
|
||||
/// 功能: 发现页面
|
||||
@@ -21,26 +22,52 @@ class DiscoverPage extends StatefulWidget {
|
||||
|
||||
class _DiscoverPageState extends State<DiscoverPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
final List<String> _categories = ['热门', '分类', '搜索', '活跃'];
|
||||
TabController? _tabController;
|
||||
List<String> _categories = ['分类', '热门', '搜索'];
|
||||
bool _showTips = true;
|
||||
bool _isDeveloperMode = false;
|
||||
bool _isInitialized = false;
|
||||
OverlayEntry? _infoOverlayEntry;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: _categories.length, vsync: this);
|
||||
// 添加标签切换监听,以便更新UI
|
||||
_tabController.addListener(() {
|
||||
_removeInfoOverlay();
|
||||
setState(() {});
|
||||
_loadDeveloperMode();
|
||||
}
|
||||
|
||||
Future<void> _loadDeveloperMode() async {
|
||||
final isEnabled = await SharedPreferencesStorageController.getBool(
|
||||
'developer_mode_enabled',
|
||||
defaultValue: false,
|
||||
);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isDeveloperMode = isEnabled;
|
||||
_updateCategories();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _updateCategories() {
|
||||
setState(() {
|
||||
_categories = ['分类', '热门', '搜索'];
|
||||
if (_isDeveloperMode) {
|
||||
_categories.add('活跃');
|
||||
}
|
||||
_tabController?.dispose();
|
||||
_tabController = TabController(length: _categories.length, vsync: this);
|
||||
_tabController!.addListener(() {
|
||||
_removeInfoOverlay();
|
||||
setState(() {});
|
||||
});
|
||||
_isInitialized = true;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_removeInfoOverlay();
|
||||
_tabController.dispose();
|
||||
_tabController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -51,12 +78,16 @@ class _DiscoverPageState extends State<DiscoverPage>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isHotTab = _categories[_tabController.index] == '热门';
|
||||
if (!_isInitialized) {
|
||||
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
||||
}
|
||||
|
||||
final isHotTab = _categories[_tabController!.index] == '热门';
|
||||
|
||||
return Scaffold(
|
||||
appBar: TabbedNavAppBar.build(
|
||||
title: '发现',
|
||||
tabController: _tabController,
|
||||
tabController: _tabController!,
|
||||
tabLabels: _categories,
|
||||
leading: isHotTab ? _buildInfoButton(context) : null,
|
||||
actions: [
|
||||
@@ -66,7 +97,7 @@ class _DiscoverPageState extends State<DiscoverPage>
|
||||
body: Column(
|
||||
children: [
|
||||
// 只有非搜索标签时才显示话题chips
|
||||
if (_categories[_tabController.index] != '搜索' && _showTips)
|
||||
if (_categories[_tabController!.index] != '搜索' && _showTips)
|
||||
_buildTopicChips(),
|
||||
Expanded(
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
@@ -77,14 +108,10 @@ class _DiscoverPageState extends State<DiscoverPage>
|
||||
return false;
|
||||
},
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
controller: _tabController!,
|
||||
children: _categories.asMap().entries.map((entry) {
|
||||
final category = entry.value;
|
||||
// 搜索标签显示 ActiveSearchPage
|
||||
if (category == '搜索') {
|
||||
return const ActiveSearchPage();
|
||||
}
|
||||
// 分类标签跳转到分类页面
|
||||
// 分类标签显示分类页面
|
||||
if (category == '分类') {
|
||||
return const CategoryPage();
|
||||
}
|
||||
@@ -92,6 +119,10 @@ class _DiscoverPageState extends State<DiscoverPage>
|
||||
if (category == '热门') {
|
||||
return const PopularPage();
|
||||
}
|
||||
// 搜索标签显示 ActiveSearchPage
|
||||
if (category == '搜索') {
|
||||
return const ActiveSearchPage();
|
||||
}
|
||||
// 活跃标签显示活跃统计页面
|
||||
if (category == '活跃') {
|
||||
return const RatePage();
|
||||
@@ -283,9 +314,11 @@ class _DiscoverPageState extends State<DiscoverPage>
|
||||
|
||||
Future<void> _refreshContent() async {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('内容已刷新')));
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('内容已刷新')));
|
||||
}
|
||||
}
|
||||
|
||||
void _showSearch() {
|
||||
|
||||
@@ -48,7 +48,7 @@ class _FavoritesPageState extends State<FavoritesPage>
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: TabbedNavAppBar.build(
|
||||
title: '收藏',
|
||||
title: '足迹',
|
||||
tabController: _tabController,
|
||||
tabLabels: _categories,
|
||||
tabBarScrollable: true,
|
||||
|
||||
@@ -142,9 +142,17 @@ class _AllListPageState extends State<AllListPage> {
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: _loadAllData,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 80),
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 80),
|
||||
itemCount: _cards.length + 1,
|
||||
separatorBuilder: (context, index) {
|
||||
if (index == _cards.length) return const SizedBox.shrink();
|
||||
return Container(
|
||||
height: 1,
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
);
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
if (index == _cards.length) {
|
||||
return _buildBottomIndicator();
|
||||
@@ -210,7 +218,7 @@ class _AllListPageState extends State<AllListPage> {
|
||||
// 构建点赞卡片 - 简洁紧凑样式
|
||||
Widget _buildLikeCard(PoetryData poetry) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
margin: const EdgeInsets.only(bottom: 0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
@@ -372,7 +380,7 @@ class _AllListPageState extends State<AllListPage> {
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
margin: const EdgeInsets.only(bottom: 0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:platform_info/platform_info.dart';
|
||||
import 'package:flutter_udid/flutter_udid.dart';
|
||||
import '../../../constants/app_constants.dart';
|
||||
import '../../../controllers/shared_preferences_storage_controller.dart';
|
||||
|
||||
/// 时间: 2026-03-26
|
||||
/// 功能: 应用信息页面
|
||||
@@ -21,11 +22,15 @@ class AppInfoPage extends StatefulWidget {
|
||||
|
||||
class _AppInfoPageState extends State<AppInfoPage> {
|
||||
String _udid = '获取中...';
|
||||
bool _isDeveloperMode = false;
|
||||
int _tapCount = 0;
|
||||
DateTime? _lastTapTime;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadUdid();
|
||||
_loadDeveloperMode();
|
||||
}
|
||||
|
||||
Future<void> _loadUdid() async {
|
||||
@@ -45,6 +50,50 @@ class _AppInfoPageState extends State<AppInfoPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadDeveloperMode() async {
|
||||
final isEnabled = await SharedPreferencesStorageController.getBool(
|
||||
'developer_mode_enabled',
|
||||
defaultValue: false,
|
||||
);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isDeveloperMode = isEnabled;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveDeveloperMode(bool enabled) async {
|
||||
await SharedPreferencesStorageController.setBool(
|
||||
'developer_mode_enabled',
|
||||
enabled,
|
||||
);
|
||||
}
|
||||
|
||||
void _onFrameworkTap() {
|
||||
final now = DateTime.now();
|
||||
if (_lastTapTime != null && now.difference(_lastTapTime!).inSeconds > 2) {
|
||||
_tapCount = 0;
|
||||
}
|
||||
_lastTapTime = now;
|
||||
_tapCount++;
|
||||
|
||||
if (_tapCount >= 5 && !_isDeveloperMode) {
|
||||
setState(() {
|
||||
_isDeveloperMode = true;
|
||||
});
|
||||
_saveDeveloperMode(true);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('开发者模式激活'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
_tapCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -64,6 +113,20 @@ class _AppInfoPageState extends State<AppInfoPage> {
|
||||
icon: Icon(Icons.arrow_back, color: AppConstants.primaryColor),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
actions: [
|
||||
if (_isDeveloperMode)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.bug_report, color: Colors.green),
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('调试信息已激活'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@@ -177,9 +240,15 @@ class _AppInfoPageState extends State<AppInfoPage> {
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const Text(
|
||||
'框架 1.3',
|
||||
style: TextStyle(fontSize: 12, color: Colors.white),
|
||||
GestureDetector(
|
||||
onTap: _onFrameworkTap,
|
||||
child: const Text(
|
||||
'框架 1.3',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
@@ -188,14 +257,21 @@ class _AppInfoPageState extends State<AppInfoPage> {
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.2),
|
||||
color: _isDeveloperMode
|
||||
? Colors.green.withValues(alpha: 0.3)
|
||||
: Colors.white.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Text(
|
||||
child: Text(
|
||||
'软件版本 1.5',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white,
|
||||
color: _isDeveloperMode
|
||||
? Colors.green
|
||||
: Colors.white,
|
||||
fontWeight: _isDeveloperMode
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
1024
lib/views/profile/components/entire_page.dart
Normal file
1024
lib/views/profile/components/entire_page.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -148,7 +148,7 @@ class PopMenu extends StatelessWidget {
|
||||
}),
|
||||
_buildBottomSheetItem(
|
||||
context,
|
||||
'屏幕常亮',
|
||||
'使用教程',
|
||||
Icons.screen_lock_rotation,
|
||||
() => toggleScreenWake(context),
|
||||
),
|
||||
|
||||
359
lib/views/profile/components/server_info_dialog.dart
Normal file
359
lib/views/profile/components/server_info_dialog.dart
Normal file
@@ -0,0 +1,359 @@
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../constants/app_constants.dart';
|
||||
|
||||
class ServerInfoDialog {
|
||||
static Future<void> show(BuildContext context, {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'];
|
||||
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 340),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: AppConstants.primaryColor,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.cloud_outlined,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'广州 server-ls',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 2),
|
||||
Text(
|
||||
'服务器信息',
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildInfoCard(
|
||||
icon: Icons.schedule,
|
||||
iconColor: const Color(0xFF007AFF),
|
||||
title: '服务器时间',
|
||||
content: timestamp?['datetime'] ?? '--',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoCard(
|
||||
icon: Icons.speed,
|
||||
iconColor: const Color(0xFF34C759),
|
||||
title: '服务器负载',
|
||||
content:
|
||||
'1分钟: ${_formatLoad(load?['1min'])}\n5分钟: ${_formatLoad(load?['5min'])}\n15分钟: ${_formatLoad(load?['15min'])}',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoCard(
|
||||
icon: Icons.bolt,
|
||||
iconColor: const Color(0xFFFF9500),
|
||||
title: '服务器响应',
|
||||
content: '${serverResponseTime ?? '--'} ms',
|
||||
trailing: _buildResponseTimeIndicator(
|
||||
serverResponseTime,
|
||||
),
|
||||
),
|
||||
if (latency != null && latency.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF2F2F7),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.public,
|
||||
size: 16,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'网络延迟',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
...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?;
|
||||
final isOnline = status == 'online';
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: isOnline
|
||||
? const Color(0xFF34C759)
|
||||
: const Color(0xFFFF3B30),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
host ?? '--',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
ip ?? '--',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[500],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isOnline
|
||||
? const Color(
|
||||
0xFF34C759,
|
||||
).withValues(alpha: 0.1)
|
||||
: const Color(
|
||||
0xFFFF3B30,
|
||||
).withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(
|
||||
8,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
isOnline ? '$lat ms' : '离线',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isOnline
|
||||
? const Color(0xFF34C759)
|
||||
: const Color(0xFFFF3B30),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFF2F2F7),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'关闭',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF007AFF),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _buildInfoCard({
|
||||
required IconData icon,
|
||||
required Color iconColor,
|
||||
required String title,
|
||||
required String content,
|
||||
Widget? trailing,
|
||||
}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF2F2F7),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: iconColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(icon, color: iconColor, size: 18),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
content,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (trailing != null) trailing,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static String _formatLoad(dynamic value) {
|
||||
if (value == null) return '--';
|
||||
|
||||
double? loadValue;
|
||||
if (value is num) {
|
||||
loadValue = value.toDouble();
|
||||
} else if (value is String) {
|
||||
loadValue = double.tryParse(value);
|
||||
}
|
||||
|
||||
if (loadValue == null) return '--';
|
||||
|
||||
final percentage = (loadValue * 100).round();
|
||||
return '$percentage%';
|
||||
}
|
||||
|
||||
static Widget _buildResponseTimeIndicator(dynamic responseTime) {
|
||||
int? time;
|
||||
if (responseTime is int) {
|
||||
time = responseTime;
|
||||
} else if (responseTime is String) {
|
||||
time = int.tryParse(responseTime);
|
||||
}
|
||||
|
||||
if (time == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
Color color;
|
||||
String label;
|
||||
if (time < 100) {
|
||||
color = const Color(0xFF34C759);
|
||||
label = '极快';
|
||||
} else if (time < 300) {
|
||||
color = const Color(0xFF34C759);
|
||||
label = '快速';
|
||||
} else if (time < 500) {
|
||||
color = const Color(0xFFFF9500);
|
||||
label = '正常';
|
||||
} else {
|
||||
color = const Color(0xFFFF3B30);
|
||||
label = '较慢';
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import 'expand/vote.dart';
|
||||
import 'expand/manu-script.dart';
|
||||
import 'components/bug_list_page.dart';
|
||||
import 'components/pop-menu.dart';
|
||||
import 'components/entire_page.dart';
|
||||
|
||||
class ProfilePage extends StatefulWidget {
|
||||
const ProfilePage({super.key});
|
||||
@@ -612,7 +613,10 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
_buildSettingsItem(
|
||||
'查看全站统计',
|
||||
Icons.history,
|
||||
() => _showSnackBar('查看全站统计'),
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const EntirePage()),
|
||||
),
|
||||
),
|
||||
_buildSettingsItem(
|
||||
'开发计划',
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../../../constants/app_constants.dart';
|
||||
import '../../../utils/http/http_client.dart';
|
||||
import 'user-plan.dart';
|
||||
import '../components/server_info_dialog.dart';
|
||||
|
||||
/// 时间: 2026-03-29
|
||||
/// 功能: 离线数据管理页面
|
||||
@@ -504,95 +505,7 @@ class _OfflineDataPageState extends State<OfflineDataPage> {
|
||||
}
|
||||
|
||||
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)),
|
||||
],
|
||||
);
|
||||
ServerInfoDialog.show(context, data: data);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
Reference in New Issue
Block a user