Files
wushu/lib/views/profile/expand/manu-script.dart
2026-03-30 07:32:12 +08:00

816 lines
24 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
),
),
],
),
);
}
}