Initial commit: Flutter 无书应用项目
This commit is contained in:
459
lib/views/active/active_search_page.dart
Normal file
459
lib/views/active/active_search_page.dart
Normal file
@@ -0,0 +1,459 @@
|
||||
// 时间: 2026-03-22
|
||||
// 功能: 全站诗词搜索页(收藏入口可跳转)
|
||||
// 介绍: 调用 search.php,配合 NetworkListenerService 上报加载与搜索完成事件
|
||||
// 最新变化: 初始版本
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../constants/app_constants.dart';
|
||||
import '../../services/network_listener_service.dart';
|
||||
import '../../utils/http/poetry_api.dart';
|
||||
import '../main_navigation.dart';
|
||||
|
||||
/// 诗词搜索页(独立路由栈页面)
|
||||
class ActiveSearchPage extends StatefulWidget {
|
||||
const ActiveSearchPage({super.key, this.initialQuery});
|
||||
|
||||
/// 从收藏页带入的预填关键词
|
||||
final String? initialQuery;
|
||||
|
||||
@override
|
||||
State<ActiveSearchPage> createState() => _ActiveSearchPageState();
|
||||
}
|
||||
|
||||
class _ActiveSearchPageState extends State<ActiveSearchPage>
|
||||
with NetworkListenerMixin {
|
||||
static const String _loadKey = 'active_search';
|
||||
|
||||
final TextEditingController _controller = TextEditingController();
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
/// 空=不限字段;name / keywords / introduce 见 API 文档
|
||||
String _field = '';
|
||||
int _page = 1;
|
||||
final int _pageSize = 20;
|
||||
List<PoetryData> _items = [];
|
||||
int _total = 0;
|
||||
String? _error;
|
||||
bool _initialized = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller.addListener(() => setState(() {}));
|
||||
if (widget.initialQuery != null && widget.initialQuery!.trim().isNotEmpty) {
|
||||
_controller.text = widget.initialQuery!.trim();
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!_initialized && _controller.text.trim().isNotEmpty) {
|
||||
_initialized = true;
|
||||
_runSearch(reset: true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _runSearch({required bool reset}) async {
|
||||
final q = _controller.text.trim();
|
||||
if (q.isEmpty) {
|
||||
setState(() {
|
||||
_error = '请输入关键词';
|
||||
_items = [];
|
||||
_total = 0;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final nextPage = reset ? 1 : _page + 1;
|
||||
if (!reset && !_hasMore) return;
|
||||
|
||||
setState(() => _error = null);
|
||||
startNetworkLoading(_loadKey);
|
||||
try {
|
||||
final result = await PoetryApi.searchPoetry(
|
||||
q: q,
|
||||
field: _field,
|
||||
page: nextPage,
|
||||
limit: _pageSize,
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
if (reset) {
|
||||
_items = List<PoetryData>.from(result.list);
|
||||
} else {
|
||||
_items = [..._items, ...result.list];
|
||||
}
|
||||
_page = result.page;
|
||||
_total = result.total;
|
||||
});
|
||||
sendSearchEvent(keyword: q);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() => _error = e.toString());
|
||||
} finally {
|
||||
endNetworkLoading(_loadKey);
|
||||
}
|
||||
}
|
||||
|
||||
bool get _hasMore => _items.length < _total;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loading = isNetworkLoading(_loadKey);
|
||||
|
||||
// 检查是否在 TabBarView 中显示(通过上下文判断)
|
||||
final bool isInTabBarView = ModalRoute.of(context)?.settings.name == null;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[50],
|
||||
// 当在 TabBarView 中时显示自定义标题栏,在单独页面中不显示
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// 自定义标题栏
|
||||
if (isInTabBarView)
|
||||
Container(
|
||||
height: 56,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
color: Colors.white,
|
||||
child: Row(
|
||||
children: [
|
||||
// 返回按钮
|
||||
//todo
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.black87),
|
||||
onPressed: () {
|
||||
// 检查是否可以返回,避免黑屏
|
||||
if (Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
// 如果无法返回(如在 TabBarView 中),跳转到主页
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const MainNavigation(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
tooltip: '返回上一页',
|
||||
),
|
||||
// 标题
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.travel_explore,
|
||||
size: 20,
|
||||
color: Colors.black87,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'诗词搜索',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 更多按钮
|
||||
IconButton(
|
||||
icon: const Icon(Icons.more_vert, color: Colors.black87),
|
||||
onPressed: () {
|
||||
// 更多按钮的点击事件
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.history),
|
||||
title: const Text('搜索历史'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
// 实现搜索历史功能
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.settings),
|
||||
title: const Text('搜索设置'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
// 实现搜索设置功能
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
tooltip: '更多',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 搜索内容区域
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
AppConstants.pageHorizontalPadding,
|
||||
isInTabBarView ? 16 : 8,
|
||||
AppConstants.pageHorizontalPadding,
|
||||
8,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Material(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
textInputAction: TextInputAction.search,
|
||||
onSubmitted: (_) => _runSearch(reset: true),
|
||||
decoration: InputDecoration(
|
||||
hintText: '输入关键词,搜标题 / 标签 / 译文…',
|
||||
prefixIcon: const Icon(
|
||||
Icons.search,
|
||||
color: Colors.grey,
|
||||
),
|
||||
suffixIcon: _controller.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_controller.clear();
|
||||
setState(() {
|
||||
_items = [];
|
||||
_total = 0;
|
||||
_error = null;
|
||||
});
|
||||
},
|
||||
)
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.arrow_forward),
|
||||
tooltip: '搜索',
|
||||
onPressed: () => _runSearch(reset: true),
|
||||
),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
onChanged: (_) => setState(() {}),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
const Text('', style: TextStyle(fontSize: 13)),
|
||||
// const Text('范围:', style: TextStyle(fontSize: 13)),
|
||||
ChoiceChip(
|
||||
label: const Text('全部'),
|
||||
selected: _field.isEmpty,
|
||||
onSelected: (_) {
|
||||
setState(() => _field = '');
|
||||
if (_controller.text.trim().isNotEmpty) {
|
||||
_runSearch(reset: true);
|
||||
}
|
||||
},
|
||||
),
|
||||
ChoiceChip(
|
||||
label: const Text('标题'),
|
||||
selected: _field == 'name',
|
||||
onSelected: (_) {
|
||||
setState(() => _field = 'name');
|
||||
if (_controller.text.trim().isNotEmpty) {
|
||||
_runSearch(reset: true);
|
||||
}
|
||||
},
|
||||
),
|
||||
ChoiceChip(
|
||||
label: const Text('标签'),
|
||||
selected: _field == 'keywords',
|
||||
onSelected: (_) {
|
||||
setState(() => _field = 'keywords');
|
||||
if (_controller.text.trim().isNotEmpty) {
|
||||
_runSearch(reset: true);
|
||||
}
|
||||
},
|
||||
),
|
||||
ChoiceChip(
|
||||
label: const Text('译文'),
|
||||
selected: _field == 'introduce',
|
||||
onSelected: (_) {
|
||||
setState(() => _field = 'introduce');
|
||||
if (_controller.text.trim().isNotEmpty) {
|
||||
_runSearch(reset: true);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_error != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
_error!,
|
||||
style: const TextStyle(
|
||||
color: Colors.red,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
color: AppConstants.primaryColor,
|
||||
onRefresh: () async {
|
||||
await _runSearch(reset: true);
|
||||
},
|
||||
child: _buildListBody(loading),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// 始终显示返回按钮
|
||||
// todo 二次黑屏处理 标记
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
// 检查是否可以返回,避免黑屏
|
||||
if (Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
// 如果无法返回,跳转到主页
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const MainNavigation()),
|
||||
);
|
||||
}
|
||||
},
|
||||
backgroundColor: AppConstants.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
child: const Icon(Icons.arrow_back),
|
||||
tooltip: '返回上一页',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildListBody(bool loading) {
|
||||
if (loading && _items.isEmpty) {
|
||||
return ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: const [
|
||||
SizedBox(height: 120),
|
||||
Center(child: CircularProgressIndicator()),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (_items.isEmpty && !loading) {
|
||||
return ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: [
|
||||
SizedBox(height: MediaQuery.of(context).size.height * 0.15),
|
||||
Icon(Icons.manage_search, size: 64, color: Colors.grey[400]),
|
||||
const SizedBox(height: 12),
|
||||
Center(
|
||||
child: Text(
|
||||
_controller.text.trim().isEmpty ? '输入关键词开始搜索' : '暂无结果',
|
||||
style: TextStyle(color: Colors.grey[600], fontSize: 16),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
AppConstants.pageHorizontalPadding,
|
||||
0,
|
||||
AppConstants.pageHorizontalPadding,
|
||||
24,
|
||||
),
|
||||
itemCount: _items.length + (_hasMore ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index >= _items.length) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Center(
|
||||
child: loading
|
||||
? const CircularProgressIndicator()
|
||||
: TextButton.icon(
|
||||
onPressed: () => _runSearch(reset: false),
|
||||
icon: const Icon(Icons.expand_more),
|
||||
label: const Text('加载更多'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final p = _items[index];
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
elevation: 1,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
title: Text(
|
||||
p.name.isNotEmpty ? p.name : p.url,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (p.alias.isNotEmpty)
|
||||
Text(
|
||||
'📜 ${p.alias}',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[700]),
|
||||
),
|
||||
if (p.introduce.isNotEmpty)
|
||||
Text(
|
||||
p.introduce,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontSize: 13, color: Colors.grey[800]),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'👍 ${p.like} · 🔥 ${p.hitsTotal}',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
isThreeLine: true,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
167
lib/views/active/category_page.dart
Normal file
167
lib/views/active/category_page.dart
Normal file
@@ -0,0 +1,167 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../constants/app_constants.dart';
|
||||
import '../../utils/responsive_layout.dart';
|
||||
|
||||
/// 时间: 2026-03-25
|
||||
/// 功能: 分类页面
|
||||
/// 介绍: 展示诗词分类,包括场景分类和朝代分类
|
||||
/// 最新变化: 新建分类页面,支持场景和朝代两大分类
|
||||
|
||||
class CategoryPage extends StatefulWidget {
|
||||
const CategoryPage({super.key});
|
||||
|
||||
@override
|
||||
State<CategoryPage> createState() => _CategoryPageState();
|
||||
}
|
||||
|
||||
class _CategoryPageState extends State<CategoryPage> with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
final List<String> _tabCategories = ['场景分类', '朝代分类'];
|
||||
|
||||
static const sceneData = {
|
||||
"节日": ["七夕节", "中秋节", "元宵节", "寒食节", "清明节", "端午节", "重阳节", "春节", "节日"],
|
||||
"季节": ["三月", "二月", "冬天", "夏天", "春天", "春季", "秋天"],
|
||||
"古籍": ["三国志", "三国演义", "三字经", "中庸", "列子", "史记", "后汉书", "吕氏春秋", "商君书", "围炉夜话", "增广贤文", "墨子", "孙子兵法", "孟子", "小窗幽记", "尚书", "左传", "幼学琼林", "庄子", "战国策", "文心雕龙", "易传", "晋书", "汉书", "淮南子", "礼记", "管子", "红楼梦", "老子", "荀子", "菜根谭", "警世通言", "论语", "资治通鉴", "韩非子", "鬼谷子", "古籍", "格言联璧"],
|
||||
"情感": ["伤感", "励志", "友情", "思乡", "思念", "感恩", "爱国", "爱情", "离别"],
|
||||
"景物": ["庐山", "泰山", "西湖", "长江", "黄河", "边塞", "田园", "山水", "夜景"],
|
||||
"天文气象": ["写云", "写雨", "写雪", "写风", "星星", "月亮", "流星"],
|
||||
"动植物": ["写鸟", "柳树", "桃花", "梅花", "竹子", "荷花", "菊花"],
|
||||
"语言文学": ["对联", "谚语", "一言", "读书", "哲理"],
|
||||
"其他": ["母亲", "老师", "户外", "礼物", "酒"]
|
||||
};
|
||||
|
||||
static const dynastyData = {
|
||||
"主要朝代": ["唐代", "宋代", "元代", "明代", "清代"],
|
||||
"古代朝代": ["南北朝", "五代", "隋代"],
|
||||
"近现代": ["近现代", "用户投稿", "管理员测试"],
|
||||
"其他": ["暂无朝代"]
|
||||
};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: _tabCategories.length, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDesktop = ResponsiveLayout.isDesktop(context);
|
||||
final isTablet = ResponsiveLayout.isTablet(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),
|
||||
),
|
||||
),
|
||||
// 内容区域
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildCategoryGrid(sceneData, '场景分类'),
|
||||
_buildCategoryGrid(dynastyData, '朝代分类'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCategoryGrid(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 Card(
|
||||
margin: const EdgeInsets.only(bottom: 16.0),
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
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,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: items.map((item) {
|
||||
return _buildCategoryChip(item, categoryType);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCategoryChip(String label, String categoryType) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
// TODO: 跳转到对应分类的诗词列表页面
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('点击了 $label'),
|
||||
duration: const Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).primaryColor.withValues(alpha: 0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
406
lib/views/active/popular_page.dart
Normal file
406
lib/views/active/popular_page.dart
Normal file
@@ -0,0 +1,406 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../constants/app_constants.dart';
|
||||
import '../../utils/responsive_layout.dart';
|
||||
import '../../utils/http/http_client.dart';
|
||||
import '../../models/poetry_model.dart';
|
||||
|
||||
/// 时间: 2026-03-25
|
||||
/// 功能: 热门页面
|
||||
/// 介绍: 展示诗词排行榜,包括总榜、日榜、月榜
|
||||
/// 最新变化: 新建热门页面,支持多种排行榜类型
|
||||
|
||||
class PopularPage extends StatefulWidget {
|
||||
const PopularPage({super.key});
|
||||
|
||||
@override
|
||||
State<PopularPage> createState() => _PopularPageState();
|
||||
}
|
||||
|
||||
class _PopularPageState extends State<PopularPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
final List<String> _tabCategories = ['总榜', '日榜', '月榜'];
|
||||
|
||||
List<PoetryModel> _rankList = [];
|
||||
bool _loading = false;
|
||||
String _errorMessage = '';
|
||||
bool _showBottomIndicator = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: _tabCategories.length, vsync: this);
|
||||
_tabController.addListener(() {
|
||||
if (mounted && !_tabController.indexIsChanging) {
|
||||
_loadRankList();
|
||||
}
|
||||
});
|
||||
_loadRankList();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@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),
|
||||
),
|
||||
),
|
||||
// 内容区域
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: _tabCategories.map((category) {
|
||||
return _buildRankContent(category);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRankContent(String category) {
|
||||
if (_loading) {
|
||||
return const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text('正在加载排行榜...'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_errorMessage.isNotEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.error_outline, size: 64, color: Colors.grey),
|
||||
const SizedBox(height: 16),
|
||||
Text(_errorMessage, style: const TextStyle(color: Colors.grey)),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: _loadRankList,
|
||||
child: const Text('🔄 重试'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_rankList.isEmpty) {
|
||||
return const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.bar_chart, size: 64, color: Colors.grey),
|
||||
SizedBox(height: 16),
|
||||
Text('暂无排行数据', style: TextStyle(color: Colors.grey)),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'换个时间段试试吧',
|
||||
style: TextStyle(color: Colors.grey, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async => await _loadRankList(),
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: (scrollNotification) {
|
||||
if (scrollNotification is ScrollEndNotification &&
|
||||
scrollNotification.metrics.extentAfter == 0) {
|
||||
_showBottomIndicatorMethod();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: _rankList.length + (_showBottomIndicator ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == _rankList.length) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Text('到底了', style: TextStyle(color: Colors.grey)),
|
||||
),
|
||||
);
|
||||
}
|
||||
return _buildRankItem(_rankList[index], index + 1);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRankItem(PoetryModel poetry, int index) {
|
||||
final rank = poetry.rank > 0 ? poetry.rank : index; // 优先使用API返回的rank
|
||||
final isTopThree = rank <= 3;
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
elevation: isTopThree ? 4 : 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(
|
||||
color: isTopThree
|
||||
? AppConstants.primaryColor
|
||||
: AppConstants.primaryColor.withValues(alpha: 0.2),
|
||||
width: isTopThree ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () => _onViewDetail(poetry),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
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(
|
||||
color: isTopThree
|
||||
? Colors.white
|
||||
: AppConstants.primaryColor.withValues(alpha: 0.8),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
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(
|
||||
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(),
|
||||
'本月',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatItem(String icon, String value, String label) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(icon, style: const TextStyle(fontSize: 12)),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
Text(label, style: TextStyle(fontSize: 10, color: Colors.grey[600])),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadRankList() async {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_errorMessage = '';
|
||||
_showBottomIndicator = false;
|
||||
});
|
||||
|
||||
try {
|
||||
final tabIndex = _tabController.index;
|
||||
final type = tabIndex == 0
|
||||
? 'total'
|
||||
: tabIndex == 1
|
||||
? 'day'
|
||||
: 'month';
|
||||
|
||||
print('正在请求排行榜数据: type=$type, period=$type');
|
||||
|
||||
final response = await HttpClient.get(
|
||||
'/rlist.php',
|
||||
queryParameters: {'type': type, 'limit': '20'},
|
||||
);
|
||||
|
||||
print('API响应状态: ${response.statusCode}');
|
||||
print('API响应成功: ${response.isSuccess}');
|
||||
print('API响应代码: ${response.code}');
|
||||
print('API响应消息: ${response.message}');
|
||||
print('API响应数据: ${response.data}');
|
||||
|
||||
if (response.isSuccess && response.code == 0) {
|
||||
final data = response.data;
|
||||
final rankData = data['list'] as List<dynamic>? ?? [];
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_rankList = rankData
|
||||
.map((item) => PoetryModel.fromJson(item))
|
||||
.toList();
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_errorMessage = response.message ?? '获取排行榜失败';
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('请求异常: $e');
|
||||
print('异常类型: ${e.runtimeType}');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_errorMessage = '网络请求失败,请检查网络连接';
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _showBottomIndicatorMethod() {
|
||||
if (_rankList.isNotEmpty && !_loading && mounted) {
|
||||
setState(() {
|
||||
_showBottomIndicator = true;
|
||||
});
|
||||
|
||||
Future.delayed(const Duration(seconds: 3), () {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_showBottomIndicator = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _onViewDetail(PoetryModel poetry) {
|
||||
// TODO: 跳转到诗词详情页面
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('查看诗词: ${poetry.name}'),
|
||||
duration: const Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
603
lib/views/active/rate.dart
Normal file
603
lib/views/active/rate.dart
Normal file
@@ -0,0 +1,603 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../constants/app_constants.dart';
|
||||
|
||||
/// 时间: 2026-03-26
|
||||
/// 功能: 活跃页面
|
||||
/// 介绍: 展示用户活跃度热力图,参考 GitHub 贡献图样式
|
||||
/// 最新变化: 添加调试选项,修复布局溢出,调整活跃阈值颜色
|
||||
|
||||
class RatePage extends StatefulWidget {
|
||||
const RatePage({super.key});
|
||||
|
||||
@override
|
||||
State<RatePage> createState() => _RatePageState();
|
||||
}
|
||||
|
||||
class _RatePageState extends State<RatePage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
final List<String> _tabs = ['周活跃', '月活跃'];
|
||||
|
||||
// 调试参数
|
||||
int _debugDayCount = 7;
|
||||
int _debugBarCount = 7;
|
||||
bool _showDebugPanel = false;
|
||||
|
||||
// 活跃度阈值配置
|
||||
final Map<String, int> _activityThresholds = {
|
||||
'low': 1, // 1-5
|
||||
'medium': 6, // 6-20
|
||||
'high': 21, // 21-100
|
||||
'veryHigh': 100, // 100+
|
||||
};
|
||||
|
||||
// 模拟活跃度数据 (实际条数)
|
||||
List<int> _weekData = [];
|
||||
List<List<int>> _monthData = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: _tabs.length, vsync: this);
|
||||
_generateMockData();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// 生成模拟数据
|
||||
void _generateMockData() {
|
||||
_weekData = List.generate(_debugDayCount, (index) {
|
||||
// 生成 0-150 的随机活跃值
|
||||
return [0, 3, 8, 15, 35, 80, 150][index % 7];
|
||||
});
|
||||
|
||||
_monthData = List.generate(4, (weekIndex) {
|
||||
return List.generate(_debugBarCount, (dayIndex) {
|
||||
return [0, 3, 8, 15, 35, 80, 150][(weekIndex * 7 + dayIndex) % 7];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: Column(
|
||||
children: [
|
||||
// Tab 切换栏
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(bottom: BorderSide(color: Colors.grey[200]!)),
|
||||
),
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
tabs: _tabs.map((tab) => Tab(text: tab)).toList(),
|
||||
labelColor: AppConstants.primaryColor,
|
||||
unselectedLabelColor: Colors.grey[600],
|
||||
indicatorColor: AppConstants.primaryColor,
|
||||
indicatorWeight: 2,
|
||||
labelStyle: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
// 调试开关
|
||||
_buildDebugToggle(),
|
||||
// 调试面板
|
||||
if (_showDebugPanel) _buildDebugPanel(),
|
||||
// 内容区域
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [_buildWeekView(), _buildMonthView()],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 调试开关
|
||||
Widget _buildDebugToggle() {
|
||||
return Container(
|
||||
color: Colors.orange[50],
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.bug_report, color: Colors.orange[700], size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'开发中 - 调试模式',
|
||||
style: TextStyle(
|
||||
color: Colors.orange[700],
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_showDebugPanel = !_showDebugPanel;
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
_showDebugPanel ? Icons.expand_less : Icons.expand_more,
|
||||
size: 18,
|
||||
),
|
||||
label: Text(_showDebugPanel ? '收起' : '展开'),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.orange[700],
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 调试面板
|
||||
Widget _buildDebugPanel() {
|
||||
return Container(
|
||||
color: Colors.orange[50],
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 天数调节
|
||||
_buildDebugSlider(
|
||||
label: '天数',
|
||||
value: _debugDayCount.toDouble(),
|
||||
min: 3,
|
||||
max: 31,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_debugDayCount = value.round();
|
||||
_generateMockData();
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 条数调节
|
||||
_buildDebugSlider(
|
||||
label: '条数',
|
||||
value: _debugBarCount.toDouble(),
|
||||
min: 3,
|
||||
max: 14,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_debugBarCount = value.round();
|
||||
_generateMockData();
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// 活跃阈值说明
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.orange[200]!),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'活跃阈值配置',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.orange[700],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildThresholdItem(
|
||||
'1-5',
|
||||
'浅色',
|
||||
AppConstants.primaryColor.withValues(alpha: 0.3),
|
||||
),
|
||||
_buildThresholdItem(
|
||||
'6-20',
|
||||
'中浅色',
|
||||
AppConstants.primaryColor.withValues(alpha: 0.5),
|
||||
),
|
||||
_buildThresholdItem(
|
||||
'21-100',
|
||||
'中深色',
|
||||
AppConstants.primaryColor.withValues(alpha: 0.7),
|
||||
),
|
||||
_buildThresholdItem('100+', '最深色', AppConstants.primaryColor),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 调试滑块
|
||||
Widget _buildDebugSlider({
|
||||
required String label,
|
||||
required double value,
|
||||
required double min,
|
||||
required double max,
|
||||
required ValueChanged<double> onChanged,
|
||||
}) {
|
||||
return Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 50,
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(fontSize: 14, color: Colors.orange[700]),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
value: value,
|
||||
min: min,
|
||||
max: max,
|
||||
divisions: (max - min).round(),
|
||||
label: value.round().toString(),
|
||||
activeColor: Colors.orange[700],
|
||||
onChanged: onChanged,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 40,
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
value.round().toString(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.orange[700],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 阈值项
|
||||
Widget _buildThresholdItem(String range, String label, Color color) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'$range: $label',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[700]),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 周活跃视图
|
||||
Widget _buildWeekView() {
|
||||
final weekDays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSectionTitle('本周活跃趋势'),
|
||||
const SizedBox(height: 16),
|
||||
// 热力图 - 使用 Wrap 防止溢出
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// 星期标签
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
alignment: WrapAlignment.center,
|
||||
children: List.generate(_debugDayCount, (index) {
|
||||
return SizedBox(
|
||||
width: 40,
|
||||
child: Text(
|
||||
weekDays[index % 7],
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[600],
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 活跃度方块
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
alignment: WrapAlignment.center,
|
||||
children: List.generate(_weekData.length, (index) {
|
||||
return _buildActivityBlock(_weekData[index], size: 40);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildStatsSection(),
|
||||
const SizedBox(height: 16),
|
||||
_buildLegend(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 月活跃视图
|
||||
Widget _buildMonthView() {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSectionTitle('本月活跃热力图'),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// 星期标签
|
||||
Wrap(
|
||||
spacing: 4,
|
||||
alignment: WrapAlignment.center,
|
||||
children: ['一', '二', '三', '四', '五', '六', '日']
|
||||
.map(
|
||||
(day) => SizedBox(
|
||||
width: 32,
|
||||
child: Text(
|
||||
day,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.grey[500],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// 四周热力图 - 使用 Wrap 防止溢出
|
||||
...List.generate(4, (weekIndex) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
child: Wrap(
|
||||
spacing: 4,
|
||||
alignment: WrapAlignment.center,
|
||||
children: List.generate(_monthData[weekIndex].length, (
|
||||
dayIndex,
|
||||
) {
|
||||
return _buildActivityBlock(
|
||||
_monthData[weekIndex][dayIndex],
|
||||
size: 28,
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildStatsSection(),
|
||||
const SizedBox(height: 16),
|
||||
_buildLegend(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 活跃度方块
|
||||
Widget _buildActivityBlock(int value, {required double size}) {
|
||||
return Tooltip(
|
||||
message: '活跃度: $value',
|
||||
child: Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
color: _getActivityColor(value),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: value == 0 ? Border.all(color: Colors.grey[200]!) : null,
|
||||
),
|
||||
child: value > 0
|
||||
? Center(
|
||||
child: value >= 100
|
||||
? Icon(
|
||||
Icons.local_fire_department,
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
size: size * 0.6,
|
||||
)
|
||||
: null,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 获取活跃度颜色 (根据新的阈值)
|
||||
Color _getActivityColor(int value) {
|
||||
if (value == 0) {
|
||||
return Colors.grey[100]!;
|
||||
} else if (value >= 1 && value <= 5) {
|
||||
// 1-5: 浅色
|
||||
return AppConstants.primaryColor.withValues(alpha: 0.3);
|
||||
} else if (value >= 6 && value <= 20) {
|
||||
// 6-20: 中浅色
|
||||
return AppConstants.primaryColor.withValues(alpha: 0.5);
|
||||
} else if (value >= 21 && value <= 100) {
|
||||
// 21-100: 中深色
|
||||
return AppConstants.primaryColor.withValues(alpha: 0.7);
|
||||
} else {
|
||||
// 100+: 最深色
|
||||
return AppConstants.primaryColor;
|
||||
}
|
||||
}
|
||||
|
||||
// 标题组件
|
||||
Widget _buildSectionTitle(String title) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 4,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: AppConstants.primaryColor,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 统计信息
|
||||
Widget _buildStatsSection() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildStatItem('活跃天数', '12', Icons.calendar_today),
|
||||
_buildStatItem('连续活跃', '5', Icons.local_fire_department),
|
||||
_buildStatItem('总活跃度', '89%', Icons.trending_up),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 统计项
|
||||
Widget _buildStatItem(String label, String value, IconData icon) {
|
||||
return Column(
|
||||
children: [
|
||||
Icon(icon, color: AppConstants.primaryColor, size: 24),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(label, style: TextStyle(fontSize: 12, color: Colors.grey[600])),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 图例说明
|
||||
Widget _buildLegend() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'活跃度说明',
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_buildLegendItem('无活跃', Colors.grey[100]!),
|
||||
_buildLegendItem(
|
||||
'1-5',
|
||||
AppConstants.primaryColor.withValues(alpha: 0.3),
|
||||
),
|
||||
_buildLegendItem(
|
||||
'6-20',
|
||||
AppConstants.primaryColor.withValues(alpha: 0.5),
|
||||
),
|
||||
_buildLegendItem(
|
||||
'21-100',
|
||||
AppConstants.primaryColor.withValues(alpha: 0.7),
|
||||
),
|
||||
_buildLegendItem('100+', AppConstants.primaryColor),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 图例项
|
||||
Widget _buildLegendItem(String label, Color color) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
border: label == '无活跃'
|
||||
? Border.all(color: Colors.grey[300]!)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(label, style: TextStyle(fontSize: 11, color: Colors.grey[600])),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user