细节优化

This commit is contained in:
Developer
2026-04-10 06:22:06 +08:00
parent 73036a8856
commit 0d8a5ecbda
40 changed files with 3098 additions and 934 deletions

View File

@@ -9,6 +9,7 @@ import '../../controllers/history_controller.dart';
import '../../services/network_listener_service.dart';
import '../../utils/http/poetry_api.dart';
import '../../services/get/theme_controller.dart';
import '../../services/get/favorites_controller.dart';
import 'collect_notes.dart';
import 'liked_poetry_manager.dart';
@@ -176,9 +177,12 @@ class AllListPageState extends State<AllListPage> {
@override
Widget build(BuildContext context) {
final favoritesController = Get.find<FavoritesController>();
return Obx(() {
final isDark = _themeController.isDarkMode;
final themeColor = _themeController.currentThemeColor;
final isGridView = favoritesController.isGridView.value;
if (_isLoading && _cards.isEmpty) {
return Center(
@@ -194,36 +198,320 @@ class AllListPageState extends State<AllListPage> {
return RefreshIndicator(
onRefresh: _loadAllData,
child: ListView.separated(
// 添加底部内边距,让内容延伸到导航栏下方,实现玻璃效果
padding: EdgeInsets.fromLTRB(
16,
8,
16,
AppConfig.liquidGlassTotalHeight + 16,
),
itemCount: _cards.length + 1,
separatorBuilder: (context, index) {
if (index == _cards.length) return const SizedBox.shrink();
return Container(
height: 1,
color: isDark
? Colors.white.withValues(alpha: 0.1)
: Colors.black.withValues(alpha: 0.1),
margin: const EdgeInsets.symmetric(vertical: 4),
);
},
itemBuilder: (context, index) {
if (index == _cards.length) {
return _buildBottomIndicator(isDark);
}
return _buildCard(_cards[index], isDark, themeColor);
},
),
child: isGridView ? _buildGridView(isDark, themeColor) : _buildListView(isDark, themeColor),
);
});
}
Widget _buildListView(bool isDark, Color themeColor) {
return ListView.separated(
padding: EdgeInsets.fromLTRB(
16,
8,
16,
AppConfig.liquidGlassTotalHeight + 16,
),
itemCount: _cards.length + 1,
separatorBuilder: (context, index) {
if (index == _cards.length) return const SizedBox.shrink();
return Container(
height: 1,
color: isDark
? Colors.white.withValues(alpha: 0.1)
: Colors.black.withValues(alpha: 0.1),
margin: const EdgeInsets.symmetric(vertical: 4),
);
},
itemBuilder: (context, index) {
if (index == _cards.length) {
return _buildBottomIndicator(isDark);
}
return _buildCard(_cards[index], isDark, themeColor);
},
);
}
Widget _buildGridView(bool isDark, Color themeColor) {
return GridView.builder(
padding: EdgeInsets.fromLTRB(
16,
8,
16,
AppConfig.liquidGlassTotalHeight + 16,
),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.85,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: _cards.length + 1,
itemBuilder: (context, index) {
if (index == _cards.length) {
return _buildBottomIndicator(isDark);
}
return _buildGridCard(_cards[index], isDark, themeColor);
},
);
}
Widget _buildGridCard(UnifiedCard card, bool isDark, Color themeColor) {
switch (card.type) {
case CardType.like:
return _buildGridLikeCard(card.data as PoetryData, isDark, themeColor);
case CardType.note:
return _buildGridNoteCard(card.data as Map<String, dynamic>, isDark);
}
}
Widget _buildGridLikeCard(PoetryData poetry, bool isDark, Color themeColor) {
return Container(
decoration: BoxDecoration(
color: isDark ? const Color(0xFF2A2A2A) : Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: themeColor.withValues(alpha: 0.2), width: 1),
boxShadow: [
BoxShadow(
color: isDark
? Colors.black.withValues(alpha: 0.2)
: Colors.black.withValues(alpha: 0.03),
blurRadius: 6,
offset: const Offset(0, 1),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(10, 8, 10, 4),
child: Row(
children: [
Expanded(
child: Text(
poetry.url,
style: TextStyle(
fontSize: 10,
color: isDark ? Colors.grey[400] : Colors.grey[600],
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 3,
),
decoration: BoxDecoration(
color: themeColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(6),
),
child: Icon(Icons.favorite, size: 10, color: themeColor),
),
],
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(10, 4, 10, 4),
child: Text(
poetry.name,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: isDark ? Colors.white : Colors.black87,
height: 1.4,
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(8, 0, 8, 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
InkWell(
onTap: () => _showPoetryDetails(poetry),
child: Icon(Icons.visibility, size: 16, color: themeColor),
),
InkWell(
onTap: () => _createNoteFromPoetry(poetry),
child: Icon(
Icons.note_add,
size: 16,
color: isDark ? Colors.orange[300] : Colors.orange[700],
),
),
InkWell(
onTap: () => _removeLikedPoetry(poetry.id.toString()),
child: Icon(
Icons.favorite_border,
size: 16,
color: isDark ? Colors.grey[400] : Colors.grey[600],
),
),
],
),
),
],
),
);
}
Widget _buildGridNoteCard(Map<String, dynamic> note, bool isDark) {
final title = note['title'] as String? ?? '';
final content = note['content'] as String? ?? '';
final category = note['category'] as String? ?? '';
final isPinned = note['isPinned'] == true;
final isLocked = note['isLocked'] == true;
String displayText;
bool hasTitle = title.isNotEmpty;
if (hasTitle) {
displayText = title;
} else {
displayText = content;
}
return Container(
decoration: BoxDecoration(
color: isDark ? const Color(0xFF2A2A2A) : Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: isDark
? Colors.black.withValues(alpha: 0.2)
: Colors.black.withValues(alpha: 0.06),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Stack(
children: [
InkWell(
onTap: () => _handleNoteTap(note, isLocked),
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 3,
),
decoration: BoxDecoration(
color: Colors.orange.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.note_outlined,
size: 10,
color: isDark ? Colors.orange[300] : Colors.orange[700],
),
const SizedBox(width: 2),
Text(
'笔记',
style: TextStyle(
fontSize: 9,
color: isDark ? Colors.orange[300] : Colors.orange[700],
fontWeight: FontWeight.w500,
),
),
],
),
),
),
if (isPinned)
Icon(
Icons.push_pin,
size: 12,
color: isDark ? Colors.orange[300] : Colors.orange[700],
),
if (isLocked)
Icon(
Icons.lock,
size: 12,
color: isDark ? Colors.orange[300] : Colors.orange[700],
),
],
),
const SizedBox(height: 8),
Expanded(
child: Text(
displayText,
style: TextStyle(
fontSize: hasTitle ? 13 : 12,
fontWeight: hasTitle
? FontWeight.w600
: FontWeight.normal,
color: isDark ? Colors.white : Colors.black87,
height: 1.4,
),
maxLines: 4,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(height: 8),
Text(
'${note['charCount'] ?? displayText.length}',
style: TextStyle(
fontSize: 10,
color: isDark
? Colors.grey[400]
: Colors.grey[500],
),
),
],
),
),
),
if (isLocked)
Positioned.fill(
child: GestureDetector(
onTap: () => _handleNoteTap(note, isLocked),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: Container(
color: (isDark ? Colors.black : Colors.white).withValues(
alpha: 0.4,
),
child: Center(
child: Icon(
Icons.lock,
size: 24,
color: isDark
? Colors.orange[300]
: Colors.orange[700],
),
),
),
),
),
),
),
],
),
),
);
}
Widget _buildBottomIndicator(bool isDark) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 24),
@@ -642,11 +930,12 @@ class AllListPageState extends State<AllListPage> {
try {
await HistoryController.removeLikedPoetry(poetryId);
await _loadAllData();
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('已取消点赞')));
}
Get.snackbar(
'成功',
'已取消点赞',
snackPosition: SnackPosition.BOTTOM,
colorText: _themeController.currentThemeColor,
);
} catch (e) {
debugPrint('取消点赞失败: $e');
}

View File

@@ -36,6 +36,7 @@ class _CollectNotesPageState extends State<CollectNotesPage> {
bool _isPinned = false;
bool _isLocked = false;
String? _password;
String? _passwordHint;
String? _category;
Timer? _debounceTimer;
@@ -100,6 +101,7 @@ class _CollectNotesPageState extends State<CollectNotesPage> {
_isPinned = note['isPinned'] ?? false;
_isLocked = note['isLocked'] ?? false;
_password = note['password'];
_passwordHint = note['passwordHint'];
_category = note['category'] ?? _categoryOptions[0];
}
} catch (e) {
@@ -142,6 +144,7 @@ class _CollectNotesPageState extends State<CollectNotesPage> {
isPinned: _isPinned,
isLocked: _isLocked,
password: _password,
passwordHint: _passwordHint,
category: _category,
createTime: _createTime?.toIso8601String(),
);
@@ -169,9 +172,12 @@ class _CollectNotesPageState extends State<CollectNotesPage> {
final title = _titleController.text.trim();
final content = _contentController.text.trim();
if (title.isEmpty && content.isEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('请先输入内容')));
Get.snackbar(
'提示',
'请先输入内容',
snackPosition: SnackPosition.TOP,
colorText: _themeController.currentThemeColor,
);
return;
}
await _saveNote();
@@ -183,11 +189,12 @@ class _CollectNotesPageState extends State<CollectNotesPage> {
await _saveNote();
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(_isPinned ? '已置顶' : '已取消置顶')));
}
Get.snackbar(
'成功',
_isPinned ? '已置顶' : '已取消置顶',
snackPosition: SnackPosition.BOTTOM,
colorText: _themeController.currentThemeColor,
);
}
// 验证密码格式(只能数字和字母)
@@ -335,6 +342,9 @@ class _CollectNotesPageState extends State<CollectNotesPage> {
final TextEditingController passwordController = TextEditingController();
final TextEditingController confirmPasswordController =
TextEditingController();
final TextEditingController hintController = TextEditingController(
text: _passwordHint ?? '',
);
await showDialog(
context: context,
@@ -362,6 +372,15 @@ class _CollectNotesPageState extends State<CollectNotesPage> {
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: hintController,
decoration: const InputDecoration(
labelText: '密码提示',
hintText: '可作为找回密码的凭证',
border: OutlineInputBorder(),
),
),
],
),
actions: [
@@ -372,25 +391,39 @@ class _CollectNotesPageState extends State<CollectNotesPage> {
TextButton(
onPressed: () async {
if (passwordController.text.isEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('密码不能为空')));
Get.snackbar(
'提示',
'密码不能为空',
snackPosition: SnackPosition.TOP,
colorText: _themeController.currentThemeColor,
);
return;
}
if (!_isValidPassword(passwordController.text)) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('密码只能包含数字和字母')));
Get.snackbar(
'提示',
'密码只能包含数字和字母',
snackPosition: SnackPosition.TOP,
colorText: _themeController.currentThemeColor,
);
return;
}
if (passwordController.text != confirmPasswordController.text) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('两次密码不一致')));
Get.snackbar(
'提示',
'两次密码不一致',
snackPosition: SnackPosition.TOP,
colorText: _themeController.currentThemeColor,
);
return;
}
Navigator.of(context).pop();
setState(() {
_passwordHint = hintController.text.trim().isEmpty
? null
: hintController.text.trim();
});
await _setPassword(passwordController.text);
},
child: const Text('确定'),
@@ -405,6 +438,7 @@ class _CollectNotesPageState extends State<CollectNotesPage> {
final TextEditingController passwordController = TextEditingController();
final TextEditingController confirmPasswordController =
TextEditingController();
final TextEditingController hintController = TextEditingController();
await showDialog(
context: context,
@@ -432,6 +466,15 @@ class _CollectNotesPageState extends State<CollectNotesPage> {
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: hintController,
decoration: const InputDecoration(
labelText: '密码提示',
hintText: '可作为找回密码的凭证',
border: OutlineInputBorder(),
),
),
],
),
actions: [
@@ -442,25 +485,39 @@ class _CollectNotesPageState extends State<CollectNotesPage> {
TextButton(
onPressed: () async {
if (passwordController.text.isEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('密码不能为空')));
Get.snackbar(
'提示',
'密码不能为空',
snackPosition: SnackPosition.TOP,
colorText: _themeController.currentThemeColor,
);
return;
}
if (!_isValidPassword(passwordController.text)) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('密码只能包含数字和字母')));
Get.snackbar(
'提示',
'密码只能包含数字和字母',
snackPosition: SnackPosition.TOP,
colorText: _themeController.currentThemeColor,
);
return;
}
if (passwordController.text != confirmPasswordController.text) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('两次密码不一致')));
Get.snackbar(
'提示',
'两次密码不一致',
snackPosition: SnackPosition.TOP,
colorText: _themeController.currentThemeColor,
);
return;
}
Navigator.of(context).pop();
setState(() {
_passwordHint = hintController.text.trim().isEmpty
? null
: hintController.text.trim();
});
await _setPassword(passwordController.text);
},
child: const Text('确定'),
@@ -476,9 +533,12 @@ class _CollectNotesPageState extends State<CollectNotesPage> {
final title = _titleController.text.trim();
final content = _contentController.text.trim();
if (title.isEmpty && content.isEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('请先输入内容')));
Get.snackbar(
'提示',
'请先输入内容',
snackPosition: SnackPosition.TOP,
colorText: _themeController.currentThemeColor,
);
return;
}
await _saveNote();
@@ -491,11 +551,12 @@ class _CollectNotesPageState extends State<CollectNotesPage> {
await _saveNote();
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('密码设置成功,笔记已锁定')));
}
Get.snackbar(
'成功',
'密码设置成功,笔记已锁定',
snackPosition: SnackPosition.BOTTOM,
colorText: _themeController.currentThemeColor,
);
}
// 移除密码
@@ -505,15 +566,17 @@ class _CollectNotesPageState extends State<CollectNotesPage> {
setState(() {
_isLocked = false;
_password = null;
_passwordHint = null;
});
await _saveNote();
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('已取消锁定')));
}
Get.snackbar(
'成功',
'已取消锁定',
snackPosition: SnackPosition.BOTTOM,
colorText: _themeController.currentThemeColor,
);
}
// 显示删除确认对话框
@@ -546,9 +609,12 @@ class _CollectNotesPageState extends State<CollectNotesPage> {
);
if (mounted) {
Navigator.of(context).pop();
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('笔记已删除')));
Get.snackbar(
'成功',
'笔记已删除',
snackPosition: SnackPosition.BOTTOM,
colorText: _themeController.currentThemeColor,
);
}
}
}
@@ -749,9 +815,12 @@ class _CollectNotesPageState extends State<CollectNotesPage> {
onPressed: () {
final customCategory = customCategoryController.text.trim();
if (customCategory.isEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('分类名称不能为空')));
Get.snackbar(
'提示',
'分类名称不能为空',
snackPosition: SnackPosition.TOP,
colorText: _themeController.currentThemeColor,
);
return;
}
Navigator.of(context).pop();

View File

@@ -9,7 +9,6 @@ import 'dart:async' show StreamSubscription;
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../models/poetry_model.dart';
import '../../../controllers/history_controller.dart';
import '../../../constants/app_constants.dart';
import '../../../utils/http/poetry_api.dart';
@@ -165,11 +164,12 @@ class _FootprintPageState extends State<FootprintPage>
// 加载图标(不可点击)
GestureDetector(
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('开发中'),
duration: Duration(seconds: 1),
),
Get.snackbar(
'提示',
'开发中',
snackPosition: SnackPosition.TOP,
duration: const Duration(seconds: 1),
colorText: _themeController.currentThemeColor,
);
},
child: Container(

View File

@@ -0,0 +1,381 @@
/// 时间: 2026-04-09
/// 功能: 公告信息页面
/// 介绍: 显示系统公告列表,支持下拉刷新
/// 最新变化: 添加底部"到底了"提示,防止被底部 tabs 遮住
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../utils/http/http_client.dart';
import '../../../constants/app_constants.dart';
import '../../../config/app_config.dart';
import '../../../services/get/theme_controller.dart';
/// 公告数据模型
class NoticeItem {
final String id;
final String content;
final String? author;
final String createTime;
final String updateTime;
NoticeItem({
required this.id,
required this.content,
this.author,
required this.createTime,
required this.updateTime,
});
factory NoticeItem.fromJson(Map<String, dynamic> json) {
return NoticeItem(
id: json['id'] as String? ?? '',
content: json['content'] as String? ?? '',
author: json['author'] as String?,
createTime: json['create_time'] as String? ?? '',
updateTime: json['update_time'] as String? ?? '',
);
}
}
class NoticePage extends StatefulWidget {
const NoticePage({super.key, this.showAppBar = true});
final bool showAppBar;
@override
State<NoticePage> createState() => _NoticePageState();
}
class _NoticePageState extends State<NoticePage> {
List<NoticeItem> _noticeList = [];
bool _isLoading = false;
bool _hasError = false;
String _errorMessage = '';
final ThemeController _themeController = Get.find<ThemeController>();
@override
void initState() {
super.initState();
_loadNotices();
}
Future<void> _loadNotices() async {
if (_isLoading) return;
setState(() {
_isLoading = true;
_hasError = false;
_errorMessage = '';
});
try {
final response = await HttpClient.get('app/notice_api.php');
if (response.isSuccess) {
final jsonData = response.jsonData;
if (jsonData['code'] == 0) {
final data = jsonData['data'];
if (data != null && data['list'] != null) {
final list = data['list'] as List;
if (mounted) {
setState(() {
_noticeList = list
.map(
(item) =>
NoticeItem.fromJson(item as Map<String, dynamic>),
)
.toList();
_isLoading = false;
});
}
} else {
if (mounted) {
setState(() {
_noticeList = [];
_isLoading = false;
});
}
}
} else {
throw Exception(jsonData['msg'] ?? '获取公告失败');
}
} else {
throw Exception('网络请求失败');
}
} catch (e) {
if (mounted) {
setState(() {
_hasError = true;
_errorMessage = e.toString();
_isLoading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return Obx(() {
final isDark = _themeController.isDarkMode;
final themeColor = _themeController.currentThemeColor;
if (widget.showAppBar) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
backgroundColor: isDark ? const Color(0xFF2A2A2A) : themeColor,
middle: Text(
'公告',
style: TextStyle(
color: Colors.white,
fontSize: 17,
fontWeight: FontWeight.w600,
),
),
leading: CupertinoButton(
padding: EdgeInsets.zero,
onPressed: () => Navigator.of(context).pop(),
child: Icon(CupertinoIcons.back, color: Colors.white, size: 24),
),
),
child: _buildBody(isDark, themeColor),
);
} else {
return _buildBody(isDark, themeColor);
}
});
}
Widget _buildBody(bool isDark, Color themeColor) {
if (_isLoading) {
return Center(
child: CupertinoActivityIndicator(
color: isDark ? Colors.white : themeColor,
),
);
}
if (_hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
CupertinoIcons.exclamationmark_circle,
size: 64,
color: isDark ? Colors.red[300] : AppConstants.errorColor,
),
const SizedBox(height: 16),
Text(
_errorMessage,
style: TextStyle(
fontSize: 16,
color: isDark ? Colors.red[300] : AppConstants.errorColor,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
CupertinoButton.filled(
onPressed: _loadNotices,
child: const Text('重试'),
),
],
),
);
}
if (_noticeList.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
CupertinoIcons.bell_slash,
size: 64,
color: isDark ? Colors.grey[600] : Colors.grey[400],
),
const SizedBox(height: 16),
Text(
'暂无公告',
style: TextStyle(
fontSize: 16,
color: isDark ? Colors.grey[400] : Colors.grey[600],
),
),
],
),
);
}
return RefreshIndicator(
onRefresh: _loadNotices,
color: themeColor,
child: ListView.builder(
padding: EdgeInsets.fromLTRB(
16,
16,
16,
AppConfig.liquidGlassTotalHeight + 16,
),
itemCount: _noticeList.length + 1,
itemBuilder: (context, index) {
if (index == _noticeList.length) {
return _buildBottomIndicator(isDark);
}
return _buildNoticeCard(_noticeList[index], isDark, themeColor);
},
),
);
}
Widget _buildBottomIndicator(bool isDark) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 24),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 40,
height: 1,
color: isDark ? Colors.grey[700] : Colors.grey[300],
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'到底了',
style: TextStyle(
fontSize: 12,
color: isDark ? Colors.grey[500] : Colors.grey[400],
),
),
),
Container(
width: 40,
height: 1,
color: isDark ? Colors.grey[700] : Colors.grey[300],
),
],
),
);
}
Widget _buildNoticeCard(NoticeItem notice, bool isDark, Color themeColor) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF2A2A2A) : Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: isDark
? Colors.black.withValues(alpha: 0.2)
: Colors.black.withValues(alpha: 0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
border: Border.all(color: themeColor.withValues(alpha: 0.2), width: 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: themeColor.withValues(alpha: 0.1),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
),
child: Row(
children: [
Icon(CupertinoIcons.bell, color: themeColor, size: 20),
const SizedBox(width: 8),
Expanded(
child: Text(
'公告 #${notice.id}',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: themeColor,
),
),
),
if (notice.author != null && notice.author!.isNotEmpty) ...[
Icon(
CupertinoIcons.person,
color: themeColor.withValues(alpha: 0.7),
size: 16,
),
const SizedBox(width: 4),
Text(
notice.author!,
style: TextStyle(
fontSize: 12,
color: themeColor.withValues(alpha: 0.7),
),
),
],
],
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Text(
notice.content,
style: TextStyle(
fontSize: 15,
color: isDark ? Colors.white : Colors.black87,
height: 1.6,
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: Row(
children: [
Icon(
CupertinoIcons.clock,
size: 14,
color: isDark ? Colors.grey[400] : Colors.grey[500],
),
const SizedBox(width: 6),
Text(
'创建时间: ${notice.createTime}',
style: TextStyle(
fontSize: 12,
color: isDark ? Colors.grey[400] : Colors.grey[500],
),
),
],
),
),
if (notice.updateTime != notice.createTime)
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: Row(
children: [
Icon(
CupertinoIcons.refresh,
size: 14,
color: isDark ? Colors.grey[400] : Colors.grey[500],
),
const SizedBox(width: 6),
Text(
'更新时间: ${notice.updateTime}',
style: TextStyle(
fontSize: 12,
color: isDark ? Colors.grey[400] : Colors.grey[500],
),
),
],
),
),
],
),
);
}
}

View File

@@ -9,11 +9,12 @@ import 'collect_notes.dart';
import '../../controllers/history_controller.dart';
import '../../services/network_listener_service.dart';
import '../../services/get/theme_controller.dart';
import '../../services/get/favorites_controller.dart';
/// 时间: 2026-03-26
/// 功能: 本地笔记列表组件
/// 介绍: 展示用户笔记列表,支持置顶、锁定、删除等功能
/// 最新变化: 从 favorites_page.dart 独立出来,支持实时更新
/// 最新变化: 从 favorites_page.dart 独立出来,支持实时更新,添加密码提示和找回密码功能,使用 GetX 消息气泡
class LocalNotesList extends StatefulWidget {
const LocalNotesList({super.key});
@@ -76,9 +77,12 @@ class _LocalNotesListState extends State<LocalNotesList> {
@override
Widget build(BuildContext context) {
final favoritesController = Get.find<FavoritesController>();
return Obx(() {
final isDark = _themeController.isDarkMode;
final themeColor = _themeController.currentThemeColor;
final isGridView = favoritesController.isGridView.value;
if (_isLoadingNotes && _notes.isEmpty) {
return Center(
@@ -94,27 +98,276 @@ class _LocalNotesListState extends State<LocalNotesList> {
return RefreshIndicator(
onRefresh: _loadNotes,
child: ListView.builder(
// 添加底部内边距,让内容延伸到导航栏下方,实现玻璃效果
padding: EdgeInsets.fromLTRB(
16,
16,
16,
AppConfig.liquidGlassTotalHeight + 16,
),
itemCount: _notes.length + 1,
itemBuilder: (context, index) {
if (index == _notes.length) {
return _buildBottomIndicator(isDark);
}
final note = _notes[index];
return _buildNoteCard(note, isDark, themeColor);
},
),
child: isGridView
? _buildGridView(isDark, themeColor)
: _buildListView(isDark, themeColor),
);
});
}
Widget _buildListView(bool isDark, Color themeColor) {
return ListView.builder(
padding: EdgeInsets.fromLTRB(
16,
16,
16,
AppConfig.liquidGlassTotalHeight + 16,
),
itemCount: _notes.length + 1,
itemBuilder: (context, index) {
if (index == _notes.length) {
return _buildBottomIndicator(isDark);
}
final note = _notes[index];
return _buildNoteCard(note, isDark, themeColor);
},
);
}
Widget _buildGridView(bool isDark, Color themeColor) {
return GridView.builder(
padding: EdgeInsets.fromLTRB(
16,
16,
16,
AppConfig.liquidGlassTotalHeight + 16,
),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.9,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: _notes.length + 1,
itemBuilder: (context, index) {
if (index == _notes.length) {
return _buildBottomIndicator(isDark);
}
final note = _notes[index];
return _buildGridNoteCard(note, isDark, themeColor);
},
);
}
Widget _buildGridNoteCard(
Map<String, dynamic> note,
bool isDark,
Color themeColor,
) {
final title = note['title'] as String? ?? '';
final content = note['content'] as String? ?? '';
final timeStr = note['time'] as String? ?? '';
final createTimeStr = note['createTime'] as String? ?? '';
final category = note['category'] as String? ?? '';
final isPinned = note['isPinned'] == true;
final isLocked = note['isLocked'] == true;
String displayText;
bool hasTitle = title.isNotEmpty;
bool hasCategory = category.isNotEmpty && category != '未分类';
if (hasTitle) {
displayText = title;
} else if (hasCategory) {
displayText = '[$category]';
} else {
displayText = content;
}
return Container(
decoration: BoxDecoration(
color: isDark ? const Color(0xFF2A2A2A) : Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: isDark
? Colors.black.withValues(alpha: 0.3)
: Colors.black.withValues(alpha: 0.08),
blurRadius: 12,
offset: const Offset(0, 4),
),
BoxShadow(
color: isDark
? Colors.black.withValues(alpha: 0.2)
: Colors.black.withValues(alpha: 0.04),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Stack(
children: [
InkWell(
onTap: () => _handleNoteTap(note, isLocked),
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
_formatDateTime(timeStr),
style: TextStyle(
fontSize: 10,
color: isDark
? Colors.grey[500]
: Colors.grey[400],
),
),
),
if (hasCategory)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: themeColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(6),
),
child: Text(
category,
style: TextStyle(fontSize: 9, color: themeColor),
),
),
],
),
const SizedBox(height: 8),
Expanded(
child: Text(
displayText,
style: TextStyle(
fontSize: hasTitle ? 14 : 12,
fontWeight: hasTitle
? FontWeight.w600
: FontWeight.normal,
color: isDark ? Colors.white : Colors.black87,
height: 1.4,
),
maxLines: 5,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(height: 8),
Row(
children: [
Text(
'${note['charCount'] ?? displayText.length}',
style: TextStyle(
fontSize: 10,
color: isDark ? Colors.grey[500] : Colors.grey[400],
),
),
const Spacer(),
if (isLocked)
Icon(Icons.lock, size: 14, color: themeColor),
const SizedBox(width: 8),
GestureDetector(
onTap: () => _togglePin(note['id'] as String?),
child: Icon(
isPinned ? Icons.push_pin : Icons.push_pin_outlined,
size: 14,
color: isPinned
? themeColor
: (isDark
? Colors.grey[500]
: Colors.grey[400]),
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: () =>
_showDeleteNoteDialog(note['id'] as String?),
child: Icon(
Icons.delete_outline,
size: 14,
color: isDark ? Colors.grey[500] : Colors.grey[400],
),
),
],
),
],
),
),
),
if (isPinned)
Positioned(
top: 8,
right: 8,
child: Icon(
Icons.push_pin,
size: 12,
color: isDark ? Colors.orange[300] : Colors.orange[700],
),
),
if (isLocked)
Positioned.fill(
child: GestureDetector(
onTap: () => _handleNoteTap(note, isLocked),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 8, sigmaY: 8),
child: Container(
color: (isDark ? Colors.black : Colors.white)
.withValues(alpha: 0.3),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
children: [
const Spacer(),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.lock, size: 24, color: themeColor),
const SizedBox(width: 8),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
'已锁定',
style: TextStyle(
fontSize: 12,
color: themeColor,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
'点击输入密码',
style: TextStyle(
fontSize: 10,
color: isDark
? Colors.grey[400]
: Colors.grey[600],
),
),
],
),
],
),
const Spacer(),
],
),
),
),
),
),
),
),
],
),
),
);
}
Widget _buildBottomIndicator(bool isDark) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 24),
@@ -177,7 +430,11 @@ class _LocalNotesListState extends State<LocalNotesList> {
);
}
Widget _buildNoteCard(Map<String, dynamic> note, bool isDark, Color themeColor) {
Widget _buildNoteCard(
Map<String, dynamic> note,
bool isDark,
Color themeColor,
) {
final title = note['title'] as String? ?? '';
final content = note['content'] as String? ?? '';
final timeStr = note['time'] as String? ?? '';
@@ -273,17 +530,12 @@ class _LocalNotesListState extends State<LocalNotesList> {
vertical: 2,
),
decoration: BoxDecoration(
color: themeColor.withValues(
alpha: 0.1,
),
color: themeColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(10),
),
child: Text(
category,
style: TextStyle(
fontSize: 10,
color: themeColor,
),
style: TextStyle(fontSize: 10, color: themeColor),
),
),
if (hasCategory) const SizedBox(width: 8),
@@ -416,11 +668,7 @@ class _LocalNotesListState extends State<LocalNotesList> {
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.lock,
size: 28,
color: themeColor,
),
Icon(Icons.lock, size: 28, color: themeColor),
const SizedBox(width: 12),
Column(
mainAxisSize: MainAxisSize.min,
@@ -482,28 +730,32 @@ class _LocalNotesListState extends State<LocalNotesList> {
final noteId = note['id'] as String?;
if (noteId == null) return;
bool canAccess = !isLocked;
if (isLocked) {
// 显示密码输入对话框
final password = await _showPasswordInputDialog(noteId);
if (password == null) return;
final result = await _showPasswordInputDialog(noteId);
if (result == null) return;
// 验证密码
final isValid = await HistoryController.verifyNotePassword(
noteId,
password,
);
if (!isValid) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('密码错误')));
if (result['type'] == 'hint_success') {
canAccess = true;
} else if (result['type'] == 'password') {
final isValid = await HistoryController.verifyNotePassword(
noteId,
result['value'] as String,
);
if (!isValid) {
Get.snackbar(
'错误',
'密码错误',
snackPosition: SnackPosition.TOP,
colorText: _themeController.currentThemeColor,
);
return;
}
return;
canAccess = true;
}
}
// 进入编辑页面
if (mounted) {
if (canAccess && mounted) {
Navigator.of(context)
.push(
MaterialPageRoute<void>(
@@ -517,41 +769,126 @@ class _LocalNotesListState extends State<LocalNotesList> {
}
// 显示密码输入对话框
Future<String?> _showPasswordInputDialog(String noteId) async {
Future<Map<String, dynamic>?> _showPasswordInputDialog(String noteId) async {
final TextEditingController passwordController = TextEditingController();
final TextEditingController hintController = TextEditingController();
bool showHintInput = false;
return showDialog<String>(
return showDialog<Map<String, dynamic>>(
context: context,
builder: (context) => AlertDialog(
title: const Row(
children: [
Icon(Icons.lock, size: 20),
SizedBox(width: 8),
Text('输入密码'),
],
),
content: TextField(
controller: passwordController,
obscureText: true,
autofocus: true,
decoration: const InputDecoration(
hintText: '请输入访问密码',
border: OutlineInputBorder(),
),
onSubmitted: (value) {
Navigator.of(context).pop(value);
},
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(null),
child: const Text('取消'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(passwordController.text),
child: const Text('确定'),
),
],
builder: (context) => StatefulBuilder(
builder: (context, setDialogState) {
return AlertDialog(
title: Row(
children: [
const Icon(Icons.lock, size: 20),
const SizedBox(width: 8),
Text(showHintInput ? '找回密码' : '输入密码'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (!showHintInput)
TextField(
controller: passwordController,
obscureText: true,
autofocus: true,
decoration: const InputDecoration(
hintText: '请输入访问密码',
border: OutlineInputBorder(),
),
onSubmitted: (value) {
Navigator.of(
context,
).pop({'type': 'password', 'value': value});
},
),
if (showHintInput)
TextField(
controller: hintController,
decoration: const InputDecoration(
hintText: '请输入密码提示',
border: OutlineInputBorder(),
),
),
],
),
actions: [
if (!showHintInput)
TextButton(
onPressed: () {
setDialogState(() {
showHintInput = true;
});
},
child: const Text('忘记密码?'),
),
if (showHintInput)
TextButton(
onPressed: () {
setDialogState(() {
showHintInput = false;
});
},
child: const Text('返回'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(null),
child: const Text('取消'),
),
if (!showHintInput)
TextButton(
onPressed: () {
Navigator.of(context).pop({
'type': 'password',
'value': passwordController.text,
});
},
child: const Text('确定'),
),
if (showHintInput)
TextButton(
onPressed: () async {
final hint = hintController.text.trim();
if (hint.isEmpty) {
Get.snackbar(
'提示',
'请输入密码提示',
snackPosition: SnackPosition.TOP,
colorText: _themeController.currentThemeColor,
);
return;
}
final success =
await HistoryController.resetNotePasswordByHint(
noteId,
hint,
);
if (success) {
Navigator.of(context).pop({'type': 'hint_success'});
Get.snackbar(
'成功',
'密码已重置,笔记已解锁',
snackPosition: SnackPosition.BOTTOM,
colorText: _themeController.currentThemeColor,
);
} else {
Get.snackbar(
'错误',
'密码提示错误',
snackPosition: SnackPosition.TOP,
colorText: _themeController.currentThemeColor,
);
}
},
child: const Text('验证'),
),
],
);
},
),
);
}
@@ -567,11 +904,12 @@ class _LocalNotesListState extends State<LocalNotesList> {
data: noteId,
);
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('已更新置顶状态')));
}
Get.snackbar(
'成功',
'已更新置顶状态',
snackPosition: SnackPosition.BOTTOM,
colorText: _themeController.currentThemeColor,
);
} catch (e) {
// 切换失败
}
@@ -649,17 +987,19 @@ class _LocalNotesListState extends State<LocalNotesList> {
data: noteId,
);
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('删除成功')));
}
Get.snackbar(
'成功',
'删除成功',
snackPosition: SnackPosition.BOTTOM,
colorText: _themeController.currentThemeColor,
);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('删除失败: $e')));
}
Get.snackbar(
'错误',
'删除失败: $e',
snackPosition: SnackPosition.TOP,
colorText: _themeController.currentThemeColor,
);
}
}
}