投稿功能

This commit is contained in:
Developer
2026-03-30 18:15:59 +08:00
parent aeddc200a7
commit 820d35fe16
3 changed files with 340 additions and 4 deletions

View File

@@ -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
### 修复

View File

@@ -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<ManuscriptPage> {
}
}
Future<void> _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<void> _submitForm() async {
if (!_formKey.currentState!.validate()) return;
@@ -172,6 +198,7 @@ class _ManuscriptPageState extends State<ManuscriptPage> {
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<ManuscriptPage> {
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<ManuscriptPage> {
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<ManuscriptPage> {
controller: _introduceController,
maxLines: 5,
decoration: InputDecoration(
hintText: '请输入诗词详细介绍...',
hintText: '会说就多说几句...',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
contentPadding: const EdgeInsets.all(16),
),

View File

@@ -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<String, dynamic> toJson() {
return {
'name': name,
'catename': catename,
'url': url,
'keywords': keywords,
'introduce': introduce,
'platform': platform,
'submitTime': submitTime.toIso8601String(),
};
}
factory ManuscriptRecord.fromJson(Map<String, dynamic> 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<TougaoPage> createState() => _TougaoPageState();
}
class _TougaoPageState extends State<TougaoPage> {
List<ManuscriptRecord> _records = [];
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadRecords();
}
Future<void> _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<void> _clearAllRecords() async {
final confirm = await showDialog<bool>(
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,
),
],
),
);
}
}