投稿功能

This commit is contained in:
Developer
2026-03-30 07:32:12 +08:00
parent 59d82c9029
commit aeddc200a7
6 changed files with 986 additions and 30 deletions

View File

@@ -0,0 +1,815 @@
import 'dart:io' as io;
import 'package:flutter/material.dart';
import '../../../constants/app_constants.dart';
import '../../../utils/http/http_client.dart';
/// 时间: 2026-03-30
/// 功能: 诗词投稿页面
/// 介绍: 用户提交诗词收录申请,支持相似度检测和人机验证
/// 最新变化: 新增投稿功能
class ManuscriptPage extends StatefulWidget {
const ManuscriptPage({super.key});
@override
State<ManuscriptPage> createState() => _ManuscriptPageState();
}
class _ManuscriptPageState extends State<ManuscriptPage> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _urlController = TextEditingController();
final _keywordsController = TextEditingController();
final _introduceController = TextEditingController();
List<Map<String, dynamic>> _categories = [];
String? _selectedCategory;
bool _isLoadingCategories = true;
bool _isCheckingName = false;
bool _isSubmitting = false;
bool _nameExists = false;
int _maxSimilarity = 0;
int _similarCount = 0;
bool _nameChecked = false;
@override
void initState() {
super.initState();
_loadCategories();
}
@override
void dispose() {
_nameController.dispose();
_urlController.dispose();
_keywordsController.dispose();
_introduceController.dispose();
super.dispose();
}
String _getPlatform() {
try {
final String osName = io.Platform.operatingSystem;
if (osName == 'ohos' ||
osName == 'harmonyos' ||
osName == 'openharmony') {
return 'HarmonyOS Flutter';
} else if (io.Platform.isAndroid) {
return 'Android Flutter';
} else if (io.Platform.isIOS) {
return 'iOS Flutter';
} else if (io.Platform.isWindows) {
return 'Windows Flutter';
} else if (io.Platform.isMacOS) {
return 'macOS Flutter';
} else if (io.Platform.isLinux) {
return 'Linux Flutter';
} else {
return 'Flutter';
}
} catch (e) {
return 'Flutter';
}
}
Future<void> _loadCategories() async {
try {
final response = await HttpClient.get(
'app/apply.php',
queryParameters: {'api': 'categories'},
);
if (response.isSuccess) {
final data = response.jsonData;
if (data['ok'] == true && data['categories'] != null) {
setState(() {
_categories = List<Map<String, dynamic>>.from(data['categories']);
_isLoadingCategories = false;
});
} else {
setState(() => _isLoadingCategories = false);
_showSnackBar('加载分类失败', isError: true);
}
} else {
setState(() => _isLoadingCategories = false);
_showSnackBar('加载分类失败', isError: true);
}
} catch (e) {
setState(() => _isLoadingCategories = false);
_showSnackBar('网络请求失败: $e', isError: true);
}
}
Future<void> _checkName() async {
final name = _nameController.text.trim();
if (name.isEmpty) {
setState(() {
_nameChecked = false;
_nameExists = false;
});
return;
}
setState(() => _isCheckingName = true);
try {
final response = await HttpClient.postForm(
'app/apply.php?api=check-name',
data: {'name': name, 'threshold': '80'},
);
if (response.isSuccess) {
final data = response.jsonData;
if (data['ok'] == true) {
setState(() {
_nameChecked = true;
_nameExists = data['exists'] ?? false;
_maxSimilarity = (data['max_similarity'] ?? 0).toInt();
_similarCount = (data['similar_count'] ?? 0).toInt();
});
}
}
} catch (e) {
_showSnackBar('检测失败,请重试', isError: true);
} finally {
setState(() => _isCheckingName = false);
}
}
Future<void> _submitForm() async {
if (!_formKey.currentState!.validate()) return;
if (!_nameChecked) {
await _checkName();
if (!_nameChecked) return;
}
if (_nameExists) {
_showSnackBar('该诗词已存在,请更换', isError: true);
return;
}
final confirm = await _showConfirmDialog();
if (!confirm) return;
setState(() => _isSubmitting = true);
try {
final response = await HttpClient.postForm(
'app/apply.php?api=submit',
data: {
'name': _nameController.text.trim(),
'catename': _selectedCategory,
'url': _urlController.text.trim(),
'keywords': _keywordsController.text.trim(),
'introduce': _introduceController.text.trim(),
'img': _getPlatform(),
'threshold': '80',
},
);
if (response.isSuccess) {
final data = response.jsonData;
if (data['ok'] == true) {
_showResultDialog(true, data['message'] ?? '✅ 提交成功!等待审核');
_resetForm();
} else {
_showResultDialog(false, data['error'] ?? '提交失败');
}
} else {
_showResultDialog(false, response.message);
}
} catch (e) {
_showResultDialog(false, '网络请求失败: $e');
} finally {
setState(() => _isSubmitting = false);
}
}
void _resetForm() {
_formKey.currentState?.reset();
_nameController.clear();
_urlController.clear();
_keywordsController.clear();
_introduceController.clear();
setState(() {
_selectedCategory = null;
_nameChecked = false;
_nameExists = false;
_maxSimilarity = 0;
_similarCount = 0;
});
}
Future<bool> _showConfirmDialog() async {
return await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: const Row(
children: [
Icon(Icons.help_outline, color: AppConstants.primaryColor),
SizedBox(width: 8),
Text('确认提交'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildConfirmItem('参考语句', _nameController.text),
_buildConfirmItem('分类', _selectedCategory ?? ''),
_buildConfirmItem('诗人和标题', _urlController.text),
_buildConfirmItem('关键词', _keywordsController.text),
_buildConfirmItem('平台', _getPlatform()),
_buildConfirmItem('介绍', _introduceController.text, maxLines: 2),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom(
backgroundColor: AppConstants.primaryColor,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text('确认提交'),
),
],
),
) ??
false;
}
Widget _buildConfirmItem(String label, String value, {int maxLines = 1}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 80,
child: Text(
'$label',
style: TextStyle(color: Colors.grey[600], fontSize: 14),
),
),
Expanded(
child: Text(
value,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
maxLines: maxLines,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
void _showResultDialog(bool success, String message) {
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: Row(
children: [
Icon(
success ? Icons.check_circle : Icons.error,
color: success
? AppConstants.successColor
: AppConstants.errorColor,
),
const SizedBox(width: 8),
Text(success ? '提交成功' : '提交失败'),
],
),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('确定'),
),
],
),
);
}
void _showSnackBar(String message, {bool isError = false}) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(
isError ? Icons.error_outline : Icons.check_circle_outline,
color: Colors.white,
),
const SizedBox(width: 8),
Expanded(child: Text(message)),
],
),
backgroundColor: isError
? AppConstants.errorColor
: AppConstants.successColor,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
margin: const EdgeInsets.all(16),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
appBar: AppBar(
title: const Text(
'📝 诗词投稿',
style: TextStyle(
color: AppConstants.primaryColor,
fontWeight: FontWeight.bold,
),
),
backgroundColor: Colors.white,
elevation: 0,
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: AppConstants.primaryColor),
onPressed: () => Navigator.of(context).pop(),
),
),
body: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
_buildHeaderCard(),
const SizedBox(height: 16),
_buildFormCard(),
const SizedBox(height: 24),
_buildSubmitButton(),
const SizedBox(height: 16),
_buildTipsCard(),
],
),
),
);
}
Widget _buildHeaderCard() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppConstants.primaryColor.withAlpha(30),
AppConstants.primaryColor.withAlpha(10),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.edit_note,
color: AppConstants.primaryColor,
size: 32,
),
),
const SizedBox(width: 16),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'收录经典诗词',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 4),
Text(
'支持原创和古诗,传承中华文化',
style: TextStyle(fontSize: 14, color: Colors.grey),
),
],
),
),
],
),
);
}
Widget _buildFormCard() {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(10),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
_buildSectionHeader('📝 投稿信息'),
const Divider(height: 1),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_buildNameField(),
const SizedBox(height: 16),
_buildCategoryField(),
const SizedBox(height: 16),
_buildUrlField(),
const SizedBox(height: 16),
_buildKeywordsField(),
const SizedBox(height: 16),
_buildIntroduceField(),
],
),
),
],
),
);
}
Widget _buildSectionHeader(String title) {
return Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Text(
title,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
],
),
);
}
Widget _buildNameField() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text(
'参考语句',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
const Text(' *', style: TextStyle(color: AppConstants.errorColor)),
const Spacer(),
if (_isCheckingName)
const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
),
],
),
const SizedBox(height: 8),
TextFormField(
controller: _nameController,
decoration: InputDecoration(
hintText: '如:纤云弄巧,飞星传恨',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
suffixIcon: _nameChecked
? Icon(
_nameExists ? Icons.error : Icons.check_circle,
color: _nameExists
? AppConstants.errorColor
: AppConstants.successColor,
)
: null,
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '请输入参考语句';
}
return null;
},
onChanged: (value) {
setState(() {
_nameChecked = false;
_nameExists = false;
});
},
onFieldSubmitted: (_) => _checkName(),
),
if (_nameChecked) _buildSimilarityInfo(),
const SizedBox(height: 8),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed: _isCheckingName ? null : _checkName,
icon: const Icon(Icons.search, size: 18),
label: const Text('检测是否存在'),
),
),
],
);
}
Widget _buildSimilarityInfo() {
return Container(
margin: const EdgeInsets.only(top: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: _nameExists
? AppConstants.errorColor.withAlpha(20)
: AppConstants.successColor.withAlpha(20),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
_nameExists ? Icons.warning : Icons.check_circle,
color: _nameExists
? AppConstants.errorColor
: AppConstants.successColor,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Text(
_nameExists
? '⚠️ 发现相似内容,相似 $_similarCount 条,最高相似度 $_maxSimilarity%'
: '✅ 未发现相似内容',
style: TextStyle(
color: _nameExists
? AppConstants.errorColor
: AppConstants.successColor,
fontSize: 13,
),
),
),
],
),
);
}
Widget _buildCategoryField() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Text(
'分类',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
Text(' *', style: TextStyle(color: AppConstants.errorColor)),
],
),
const SizedBox(height: 8),
DropdownButtonFormField<String>(
value: _selectedCategory,
decoration: InputDecoration(
hintText: '请选择分类',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
items: _categories.map((cat) {
return DropdownMenuItem(
value: cat['catename'] as String,
child: Text(cat['catename'] as String),
);
}).toList(),
onChanged: (value) {
setState(() => _selectedCategory = value);
},
validator: (value) {
if (value == null || value.isEmpty) {
return '请选择分类';
}
return null;
},
),
],
);
}
Widget _buildUrlField() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Text(
'诗人和标题',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
Text(' *', style: TextStyle(color: AppConstants.errorColor)),
],
),
const SizedBox(height: 8),
TextFormField(
controller: _urlController,
decoration: InputDecoration(
hintText: '如:李白 静夜思',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '请输入诗人和标题';
}
return null;
},
),
],
);
}
Widget _buildKeywordsField() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Text(
'关键词',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
Text(' *', style: TextStyle(color: AppConstants.errorColor)),
],
),
const SizedBox(height: 8),
TextFormField(
controller: _keywordsController,
decoration: InputDecoration(
hintText: '用逗号分隔,如:思乡,月亮,唐诗',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '请输入关键词';
}
return null;
},
),
],
);
}
Widget _buildIntroduceField() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Text(
'诗词介绍',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
Text(' *', style: TextStyle(color: AppConstants.errorColor)),
],
),
const SizedBox(height: 8),
TextFormField(
controller: _introduceController,
maxLines: 5,
decoration: InputDecoration(
hintText: '请输入诗词详细介绍...',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
contentPadding: const EdgeInsets.all(16),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '请输入诗词介绍';
}
return null;
},
),
],
);
}
Widget _buildSubmitButton() {
return Container(
height: 56,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppConstants.primaryColor,
AppConstants.primaryColor.withAlpha(200),
],
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppConstants.primaryColor.withAlpha(50),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: ElevatedButton(
onPressed: _isSubmitting ? null : _submitForm,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: _isSubmitting
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.send, color: Colors.white),
SizedBox(width: 8),
Text(
'提交收录',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
),
);
}
Widget _buildTipsCard() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue[50],
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.lightbulb_outline, color: Colors.blue[700], size: 20),
const SizedBox(width: 8),
Text(
'投稿提示',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.blue[700],
),
),
],
),
const SizedBox(height: 8),
Text(
'• 支持原创诗词和经典古诗\n• 相似度超过80%将无法提交\n• 提交后等待审核通过\n• 平台信息自动识别:${_getPlatform()}',
style: TextStyle(
fontSize: 13,
color: Colors.blue[700],
height: 1.5,
),
),
],
),
);
}
}

View File

@@ -28,6 +28,7 @@ import 'guide/permission.dart';
import 'guide/app-data.dart';
import 'theme/app-diy.dart';
import 'expand/vote.dart';
import 'expand/manu-script.dart';
class ProfilePage extends StatefulWidget {
const ProfilePage({super.key});
@@ -670,11 +671,12 @@ class _ProfilePageState extends State<ProfilePage>
Icons.analytics,
() => _showSnackBar('软件开发进度'),
),
_buildSettingsItem(
'去投稿',
Icons.analytics,
() => _showSnackBar('支持原创和古诗'),
),
_buildSettingsItem('去投稿', Icons.edit_note, () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const ManuscriptPage()),
);
}),
]),
const SizedBox(height: 16),
// === 帮助支持组 ===