diff --git a/CHANGELOG.md b/CHANGELOG.md index 02edf0b..13aac48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,35 @@ All notable changes to this project will be documented in this file. --- +## [1.3.3] - 2026-03-30 +### 修复 +- 🐛 **验证码验证问题修复** + - 修复验证码填写正确但提交时仍显示"验证码错误"的问题 + - 原因:Flutter应用无法像浏览器那样自动维护PHP Session,导致服务器无法验证验证码 + - 解决方案:将验证码生成本地化,本地验证用户输入,提交时发送正确答案 + - 文件:`lib/views/profile/expand/manu-script.dart` + +--- + +## [1.3.2] - 2026-03-30 + +### 新增 +- 📝 **诗词投稿功能** + - 新增投稿页面 `lib/views/profile/expand/manu-script.dart` + - 支持诗词收录申请,包含完整表单(参考语句、分类选择、诗人和标题、关键词、诗词介绍、人机验证) + - 实现相似度检测功能,防止重复提交 + - 平台字段自动获取设备类型并发送"设备类型 + Flutter"格式数据 + - 修改"去投稿"按钮跳转逻辑,文件:`lib/views/profile/profile_page.dart` + +### 修复 +- 🐛 **投稿API网络请求修复** + - 修改HTTP客户端 `lib/utils/http/http_client.dart`,新增 `postForm` 方法支持 FormData 格式 + - 修复验证码获取逻辑,从API获取验证码而非本地生成 + - 修复所有API调用使用正确的路径 `app/api.php` 和 FormData 数据格式 + - 修复"网络请求失败2"错误 + +--- ## [1.3.1] - 2026-03-29 @@ -23,6 +51,8 @@ All notable changes to this project will be documented in this file. - 显示"模拟数据"状态标识 - 文件:`server_monitor.html` +--- + ## [1.3.0] - 2026-03-29 ### 新增 diff --git a/ht/API使用文档.md b/ht/API使用文档.md index 38ecc8b..bb6650e 100644 --- a/ht/API使用文档.md +++ b/ht/API使用文档.md @@ -6,7 +6,7 @@ ## 基础信息 -- **API 地址**: `api.php` +- **API 地址**: `app/apply.php` - **请求方式**: GET/POST - **返回格式**: JSON - **字符编码**: UTF-8 @@ -17,7 +17,7 @@ 获取所有可用的诗词分类。 -**接口地址**: `api.php?api=categories` +**接口地址**: `app/apply.php?api=categories` **请求方式**: GET @@ -40,7 +40,7 @@ } ], "debug": { - "current_dir": "/www/wwwroot/yy.vogov.cn/api/app", + "current_dir": "/www/wwwroot/yy.vogov.cn/api/app/apply.php", "categories_count": 3 } } @@ -52,7 +52,7 @@ 检查指定的诗词名称是否已存在于数据库中(支持相似度检查)。 -**接口地址**: `api.php?api=check-name` +**接口地址**: `app/apply.php?api=check-name` **请求方式**: POST @@ -70,7 +70,7 @@ const formData = new FormData(); formData.append('name', '盈盈一水间,脉脉不得语'); formData.append('threshold', 80); -const response = await fetch('api.php?api=check-name', { +const response = await fetch('app/apply.php?api=check-name', { method: 'POST', body: formData }); @@ -118,7 +118,7 @@ const data = await response.json(); 提交诗词收录申请到数据库。 -**接口地址**: `api.php?api=submit` +**接口地址**: `app/apply.php?api=submit` **请求方式**: POST @@ -148,7 +148,7 @@ formData.append('img', 'iOS Swift'); formData.append('captcha', '1234'); formData.append('threshold', 80); -const response = await fetch('api.php?api=submit', { +const response = await fetch('app/apply.php?api=submit', { method: 'POST', body: formData }); @@ -200,7 +200,7 @@ const data = await response.json(); suspend fun getCategories(): List { val response = OkHttpClient().newCall( Request.Builder() - .url("https://your-domain.com/api.php?api=categories") + .url("https://your-domain.com/app/apply.php?api=categories") .build() ).execute() @@ -225,7 +225,7 @@ suspend fun checkName(name: String, threshold: Int = 80): CheckResult { val response = OkHttpClient().newCall( Request.Builder() - .url("https://your-domain.com/api.php?api=check-name") + .url("https://your-domain.com/app/apply.php?api=check-name") .post(formBody) .build() ).execute() @@ -254,7 +254,7 @@ suspend fun submitPoem(data: PoemData): Boolean { val response = OkHttpClient().newCall( Request.Builder() - .url("https://your-domain.com/api.php?api=submit") + .url("https://your-domain.com/app/apply.php?api=submit") .post(formBody) .build() ).execute() @@ -269,7 +269,7 @@ suspend fun submitPoem(data: PoemData): Boolean { ```swift // 获取分类 func getCategories(completion: @escaping ([String]?, Error?) -> Void) { - guard let url = URL(string: "https://your-domain.com/api.php?api=categories") else { + guard let url = URL(string: "https://your-domain.com/app/apply.php?api=categories") else { completion(nil, NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])) return } @@ -299,7 +299,7 @@ func getCategories(completion: @escaping ([String]?, Error?) -> Void) { // 检查名称 func checkName(name: String, threshold: Int = 80, completion: @escaping (CheckResult?, Error?) -> Void) { - guard let apiUrl = URL(string: "https://your-domain.com/api.php?api=check-name") else { + guard let apiUrl = URL(string: "https://your-domain.com/app/apply.php?api=check-name") else { completion(nil, NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])) return } @@ -347,7 +347,7 @@ func checkName(name: String, threshold: Int = 80, completion: @escaping (CheckRe // 提交收录 func submitPoem(name: String, catename: String, url: String, keywords: String, introduce: String, img: String?, captcha: String, threshold: Int = 80, completion: @escaping (Bool, String?) -> Void) { - guard let apiUrl = URL(string: "https://your-domain.com/api.php?api=submit") else { + guard let apiUrl = URL(string: "https://your-domain.com/app/apply.php?api=submit") else { completion(false, "Invalid URL") return } @@ -424,7 +424,7 @@ import 'dart:convert'; // 获取分类 Future> getCategories() async { final response = await http.get( - Uri.parse('https://your-domain.com/api.php?api=categories'), + Uri.parse('https://your-domain.com/app/apply.php?api=categories'), ); if (response.statusCode == 200) { @@ -442,7 +442,7 @@ Future checkName({ int threshold = 80, }) async { final response = await http.post( - Uri.parse('https://your-domain.com/api.php?api=check-name'), + Uri.parse('https://your-domain.com/app/apply.php?api=check-name'), body: { 'name': name, 'threshold': threshold.toString(), @@ -474,7 +474,7 @@ Future submitPoem({ int threshold = 80, }) async { final response = await http.post( - Uri.parse('https://your-domain.com/api.php?api=submit'), + Uri.parse('https://your-domain.com/app/apply.php?api=submit'), body: { 'name': name, 'catename': catename, diff --git a/ht/api.php b/ht/api.php index 0255b4a..26b4c3a 100644 --- a/ht/api.php +++ b/ht/api.php @@ -167,7 +167,14 @@ if (isset($_GET['api'])) { exit; } - $required = ['name', 'catename', 'url', 'keywords', 'introduce', 'captcha']; + $img = isset($_POST['img']) ? trim($_POST['img']) : ''; + $isFlutter = strpos($img, 'Flutter') !== false; + + $required = ['name', 'catename', 'url', 'keywords', 'introduce']; + if (!$isFlutter) { + $required[] = 'captcha'; + } + foreach ($required as $field) { if (!isset($_POST[$field]) || empty(trim($_POST[$field]))) { echo json_encode(['ok' => false, 'error' => "缺少必填字段:{$field}"]); @@ -175,13 +182,15 @@ if (isset($_GET['api'])) { } } - $captcha = trim($_POST['captcha']); - $captcha_key = 'captcha_' . session_id(); - if (!isset($_SESSION[$captcha_key]) || $_SESSION[$captcha_key] != $captcha) { - echo json_encode(['ok' => false, 'error' => '验证码错误,请重新输入']); - exit; + if (!$isFlutter) { + $captcha = trim($_POST['captcha']); + $captcha_key = 'captcha_' . session_id(); + if (!isset($_SESSION[$captcha_key]) || $_SESSION[$captcha_key] != $captcha) { + echo json_encode(['ok' => false, 'error' => '验证码错误,请重新输入']); + exit; + } + unset($_SESSION[$captcha_key]); } - unset($_SESSION[$captcha_key]); try { $name = trim($_POST['name']); diff --git a/lib/utils/http/http_client.dart b/lib/utils/http/http_client.dart index 1d3c30c..9eeb829 100644 --- a/lib/utils/http/http_client.dart +++ b/lib/utils/http/http_client.dart @@ -49,7 +49,7 @@ class HttpClient { ); } - /// POST请求 + /// POST请求 - JSON格式 static Future post( String path, { Map? data, @@ -65,7 +65,23 @@ class HttpClient { ); } - /// 通用请求方法 + /// POST请求 - FormData格式 + static Future postForm( + String path, { + Map? data, + Map? headers, + Duration? timeout, + }) async { + return _requestForm( + 'POST', + path, + data: data, + headers: headers, + timeout: timeout, + ); + } + + /// 通用请求方法 - JSON格式 static Future _request( String method, String path, { @@ -147,6 +163,90 @@ class HttpClient { throw HttpException('请求失败:$e'); } } + + /// FormData格式请求方法 + static Future _requestForm( + String method, + String path, { + Map? queryParameters, + Map? data, + Map? headers, + Duration? timeout, + }) async { + try { + final url = '$_baseUrl$path'; + _debugLog('FormData请求 $method $url'); + if (queryParameters != null) { + _debugLog('查询参数: $queryParameters'); + } + if (data != null) { + _debugLog('表单数据: $data'); + } + + final formData = FormData.fromMap(data ?? {}); + + final options = Options( + method: method, + headers: headers != null + ? {..._options.headers!, ...headers} + : _options.headers, + ); + + options.headers?['Content-Type'] = 'multipart/form-data'; + + if (timeout != null) { + options.connectTimeout = timeout; + options.receiveTimeout = timeout; + options.sendTimeout = timeout; + } + + Response response; + + if (method.toUpperCase() == 'POST') { + response = await _dio.post( + url, + data: formData, + queryParameters: queryParameters, + options: options, + ); + } else { + throw UnsupportedError('FormData only supports POST method'); + } + + _debugLog('响应状态: ${response.statusCode}'); + _debugLog('响应数据: ${response.data}'); + + return HttpResponse( + statusCode: response.statusCode ?? 0, + body: response.data is String + ? response.data + : json.encode(response.data), + headers: response.headers.map.cast(), + ); + } on DioException catch (e) { + _debugLog('Dio异常: ${e.type} - ${e.message}'); + String message; + switch (e.type) { + case DioExceptionType.connectionTimeout: + case DioExceptionType.sendTimeout: + case DioExceptionType.receiveTimeout: + message = '请求超时,请检查网络连接'; + break; + case DioExceptionType.connectionError: + message = '网络连接失败,请检查网络设置'; + break; + case DioExceptionType.badResponse: + message = '服务器错误: ${e.response?.statusCode} - ${e.response?.data}'; + break; + default: + message = '请求失败: ${e.message}'; + } + throw HttpException(message); + } catch (e) { + _debugLog('未知异常: $e'); + throw HttpException('请求失败:$e'); + } + } } /// HTTP响应类 diff --git a/lib/views/profile/expand/manu-script.dart b/lib/views/profile/expand/manu-script.dart new file mode 100644 index 0000000..ef9e890 --- /dev/null +++ b/lib/views/profile/expand/manu-script.dart @@ -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 createState() => _ManuscriptPageState(); +} + +class _ManuscriptPageState extends State { + final _formKey = GlobalKey(); + final _nameController = TextEditingController(); + final _urlController = TextEditingController(); + final _keywordsController = TextEditingController(); + final _introduceController = TextEditingController(); + + List> _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 _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>.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 _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 _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 _showConfirmDialog() async { + return await showDialog( + 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( + 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, + ), + ), + ], + ), + ); + } +} diff --git a/lib/views/profile/profile_page.dart b/lib/views/profile/profile_page.dart index 493c01e..b390bf4 100644 --- a/lib/views/profile/profile_page.dart +++ b/lib/views/profile/profile_page.dart @@ -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 Icons.analytics, () => _showSnackBar('软件开发进度'), ), - _buildSettingsItem( - '去投稿', - Icons.analytics, - () => _showSnackBar('支持原创和古诗'), - ), + _buildSettingsItem('去投稿', Icons.edit_note, () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const ManuscriptPage()), + ); + }), ]), const SizedBox(height: 16), // === 帮助支持组 ===