From 820d35fe163016d2038e3c94cc0f476861053f45 Mon Sep 17 00:00:00 2001 From: Developer Date: Mon, 30 Mar 2026 18:15:59 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8A=95=E7=A8=BF=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 26 +++ lib/views/profile/expand/manu-script.dart | 47 +++- lib/views/profile/expand/tougao.dart | 271 ++++++++++++++++++++++ 3 files changed, 340 insertions(+), 4 deletions(-) create mode 100644 lib/views/profile/expand/tougao.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 13aac48..76ea250 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,32 @@ All notable changes to this project will be documented in this file. --- +## [1.3.5] - 2026-03-30 + +### 新增 +- 📜 **投稿记录功能** + - 新增投稿记录页面 `lib/views/profile/expand/tougao.dart` + - 显示历史投稿记录列表,按时间倒序排列 + - 支持展开查看详细信息(分类、诗人和标题、关键词、平台、介绍) + - 提供清空所有记录功能(带确认提示) + - 修改 `lib/views/profile/expand/manu-script.dart` + - 在AppBar添加历史记录图标按钮,点击跳转至记录页面 + - 投稿成功后自动保存记录到SharedPreferences + - 最多保存50条记录,超出时自动删除最早的记录 + +--- + +## [1.3.4] - 2026-03-30 + +### 新增 +- 🚀 **Flutter请求免验证码验证** + - 修改后端PHP代码 `ht/api.php`,当img字段包含"Flutter"时自动跳过验证码验证 + - 修改Flutter前端 `lib/views/profile/expand/manu-script.dart`,完全移除人机验证相关功能 + - Flutter应用用户无需填写验证码,简化投稿流程 + - 保持非Flutter请求(如浏览器)仍需验证码验证的安全性 + +--- + ## [1.3.3] - 2026-03-30 ### 修复 diff --git a/lib/views/profile/expand/manu-script.dart b/lib/views/profile/expand/manu-script.dart index ef9e890..9b2fe3e 100644 --- a/lib/views/profile/expand/manu-script.dart +++ b/lib/views/profile/expand/manu-script.dart @@ -1,12 +1,15 @@ import 'dart:io' as io; +import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../../../constants/app_constants.dart'; import '../../../utils/http/http_client.dart'; +import 'tougao.dart'; /// 时间: 2026-03-30 /// 功能: 诗词投稿页面 -/// 介绍: 用户提交诗词收录申请,支持相似度检测和人机验证 -/// 最新变化: 新增投稿功能 +/// 介绍: 用户提交诗词收录申请,支持相似度检测和投稿记录 +/// 最新变化: 新增投稿记录功能 class ManuscriptPage extends StatefulWidget { const ManuscriptPage({super.key}); @@ -137,6 +140,29 @@ class _ManuscriptPageState extends State { } } + Future _saveManuscriptRecord() async { + try { + final prefs = await SharedPreferences.getInstance(); + final record = ManuscriptRecord( + name: _nameController.text.trim(), + catename: _selectedCategory ?? '', + url: _urlController.text.trim(), + keywords: _keywordsController.text.trim(), + introduce: _introduceController.text.trim(), + platform: _getPlatform(), + submitTime: DateTime.now(), + ); + + final recordsJson = prefs.getStringList('manuscript_records') ?? []; + recordsJson.insert(0, jsonEncode(record.toJson())); + + await prefs.setStringList( + 'manuscript_records', + recordsJson.take(50).toList(), + ); + } catch (e) {} + } + Future _submitForm() async { if (!_formKey.currentState!.validate()) return; @@ -172,6 +198,7 @@ class _ManuscriptPageState extends State { if (response.isSuccess) { final data = response.jsonData; if (data['ok'] == true) { + await _saveManuscriptRecord(); _showResultDialog(true, data['message'] ?? '✅ 提交成功!等待审核'); _resetForm(); } else { @@ -220,7 +247,7 @@ class _ManuscriptPageState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildConfirmItem('参考语句', _nameController.text), + _buildConfirmItem('投稿句子', _nameController.text), _buildConfirmItem('分类', _selectedCategory ?? ''), _buildConfirmItem('诗人和标题', _urlController.text), _buildConfirmItem('关键词', _keywordsController.text), @@ -346,6 +373,18 @@ class _ManuscriptPageState extends State { icon: const Icon(Icons.arrow_back, color: AppConstants.primaryColor), onPressed: () => Navigator.of(context).pop(), ), + actions: [ + IconButton( + icon: const Icon(Icons.history, color: AppConstants.primaryColor), + tooltip: '投稿记录', + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const TougaoPage()), + ); + }, + ), + ], ), body: Form( key: _formKey, @@ -704,7 +743,7 @@ class _ManuscriptPageState extends State { controller: _introduceController, maxLines: 5, decoration: InputDecoration( - hintText: '请输入诗词详细介绍...', + hintText: '会说就多说几句...', border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), contentPadding: const EdgeInsets.all(16), ), diff --git a/lib/views/profile/expand/tougao.dart b/lib/views/profile/expand/tougao.dart new file mode 100644 index 0000000..e6ac3ae --- /dev/null +++ b/lib/views/profile/expand/tougao.dart @@ -0,0 +1,271 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../../../constants/app_constants.dart'; + +class ManuscriptRecord { + final String name; + final String catename; + final String url; + final String keywords; + final String introduce; + final String platform; + final DateTime submitTime; + + ManuscriptRecord({ + required this.name, + required this.catename, + required this.url, + required this.keywords, + required this.introduce, + required this.platform, + required this.submitTime, + }); + + Map toJson() { + return { + 'name': name, + 'catename': catename, + 'url': url, + 'keywords': keywords, + 'introduce': introduce, + 'platform': platform, + 'submitTime': submitTime.toIso8601String(), + }; + } + + factory ManuscriptRecord.fromJson(Map json) { + return ManuscriptRecord( + name: json['name'] ?? '', + catename: json['catename'] ?? '', + url: json['url'] ?? '', + keywords: json['keywords'] ?? '', + introduce: json['introduce'] ?? '', + platform: json['platform'] ?? '', + submitTime: DateTime.parse( + json['submitTime'] ?? DateTime.now().toIso8601String(), + ), + ); + } +} + +class TougaoPage extends StatefulWidget { + const TougaoPage({super.key}); + + @override + State createState() => _TougaoPageState(); +} + +class _TougaoPageState extends State { + List _records = []; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadRecords(); + } + + Future _loadRecords() async { + try { + final prefs = await SharedPreferences.getInstance(); + final recordsJson = prefs.getStringList('manuscript_records') ?? []; + setState(() { + _records = + recordsJson + .map((json) => ManuscriptRecord.fromJson(jsonDecode(json))) + .toList() + ..sort((a, b) => b.submitTime.compareTo(a.submitTime)); + _isLoading = false; + }); + } catch (e) { + setState(() => _isLoading = false); + } + } + + Future _clearAllRecords() async { + final confirm = await showDialog( + context: context, + builder: (context) => AlertDialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + title: const Row( + children: [ + Icon(Icons.warning_amber_rounded, color: AppConstants.errorColor), + SizedBox(width: 8), + Text('确认清空'), + ], + ), + content: const Text('确定要清空所有投稿记录吗?此操作不可恢复。'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('取消'), + ), + ElevatedButton( + onPressed: () => Navigator.pop(context, true), + style: ElevatedButton.styleFrom( + backgroundColor: AppConstants.errorColor, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text('确认清空'), + ), + ], + ), + ); + + if (confirm == true) { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove('manuscript_records'); + await _loadRecords(); + if (mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('已清空所有记录'))); + } + } + } + + String _formatDate(DateTime date) { + return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')} ' + '${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}'; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('投稿记录'), + actions: [ + if (_records.isNotEmpty) + IconButton( + icon: const Icon(Icons.delete_sweep_outlined), + onPressed: _clearAllRecords, + tooltip: '清空记录', + ), + ], + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _records.isEmpty + ? _buildEmptyState() + : _buildRecordsList(), + ); + } + + Widget _buildEmptyState() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.history_outlined, size: 80, color: Colors.grey[300]), + const SizedBox(height: 16), + Text( + '暂无投稿记录', + style: TextStyle(fontSize: 16, color: Colors.grey[600]), + ), + ], + ), + ); + } + + Widget _buildRecordsList() { + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: _records.length, + itemBuilder: (context, index) { + final record = _records[index]; + return _buildRecordCard(record); + }, + ); + } + + Widget _buildRecordCard(ManuscriptRecord record) { + return Card( + elevation: 0, + color: Colors.grey[50], + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: ExpansionTile( + title: Row( + children: [ + Expanded( + child: Text( + record.name, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: AppConstants.primaryColor.withAlpha(20), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + record.catename, + style: TextStyle( + fontSize: 12, + color: AppConstants.primaryColor, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + subtitle: Padding( + padding: const EdgeInsets.only(top: 4), + child: Row( + children: [ + Icon(Icons.access_time, size: 14, color: Colors.grey[500]), + const SizedBox(width: 4), + Text( + _formatDate(record.submitTime), + style: TextStyle(fontSize: 12, color: Colors.grey[500]), + ), + ], + ), + ), + childrenPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + children: [ + _buildDetailItem('诗人和标题', record.url), + _buildDetailItem('关键词', record.keywords), + _buildDetailItem('平台', record.platform), + _buildDetailItem('诗词介绍', record.introduce, maxLines: 3), + ], + ), + ); + } + + Widget _buildDetailItem(String label, String value, {int maxLines = 1}) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 13, + color: Colors.grey[600], + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + Text( + value, + style: const TextStyle(fontSize: 14), + maxLines: maxLines, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ); + } +}