投稿功能
This commit is contained in:
26
CHANGELOG.md
26
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
|
||||
|
||||
### 修复
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
271
lib/views/profile/expand/tougao.dart
Normal file
271
lib/views/profile/expand/tougao.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user