细节优化
This commit is contained in:
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
381
lib/views/footprint/load/notice-page.dart
Normal file
381
lib/views/footprint/load/notice-page.dart
Normal 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],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user