投稿功能
This commit is contained in:
30
CHANGELOG.md
30
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
|
## [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`
|
- 文件:`server_monitor.html`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [1.3.0] - 2026-03-29
|
## [1.3.0] - 2026-03-29
|
||||||
|
|
||||||
### 新增
|
### 新增
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
## 基础信息
|
## 基础信息
|
||||||
|
|
||||||
- **API 地址**: `api.php`
|
- **API 地址**: `app/apply.php`
|
||||||
- **请求方式**: GET/POST
|
- **请求方式**: GET/POST
|
||||||
- **返回格式**: JSON
|
- **返回格式**: JSON
|
||||||
- **字符编码**: UTF-8
|
- **字符编码**: UTF-8
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
获取所有可用的诗词分类。
|
获取所有可用的诗词分类。
|
||||||
|
|
||||||
**接口地址**: `api.php?api=categories`
|
**接口地址**: `app/apply.php?api=categories`
|
||||||
|
|
||||||
**请求方式**: GET
|
**请求方式**: GET
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"debug": {
|
"debug": {
|
||||||
"current_dir": "/www/wwwroot/yy.vogov.cn/api/app",
|
"current_dir": "/www/wwwroot/yy.vogov.cn/api/app/apply.php",
|
||||||
"categories_count": 3
|
"categories_count": 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
|
|
||||||
检查指定的诗词名称是否已存在于数据库中(支持相似度检查)。
|
检查指定的诗词名称是否已存在于数据库中(支持相似度检查)。
|
||||||
|
|
||||||
**接口地址**: `api.php?api=check-name`
|
**接口地址**: `app/apply.php?api=check-name`
|
||||||
|
|
||||||
**请求方式**: POST
|
**请求方式**: POST
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ const formData = new FormData();
|
|||||||
formData.append('name', '盈盈一水间,脉脉不得语');
|
formData.append('name', '盈盈一水间,脉脉不得语');
|
||||||
formData.append('threshold', 80);
|
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',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
@@ -118,7 +118,7 @@ const data = await response.json();
|
|||||||
|
|
||||||
提交诗词收录申请到数据库。
|
提交诗词收录申请到数据库。
|
||||||
|
|
||||||
**接口地址**: `api.php?api=submit`
|
**接口地址**: `app/apply.php?api=submit`
|
||||||
|
|
||||||
**请求方式**: POST
|
**请求方式**: POST
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ formData.append('img', 'iOS Swift');
|
|||||||
formData.append('captcha', '1234');
|
formData.append('captcha', '1234');
|
||||||
formData.append('threshold', 80);
|
formData.append('threshold', 80);
|
||||||
|
|
||||||
const response = await fetch('api.php?api=submit', {
|
const response = await fetch('app/apply.php?api=submit', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
@@ -200,7 +200,7 @@ const data = await response.json();
|
|||||||
suspend fun getCategories(): List<Category> {
|
suspend fun getCategories(): List<Category> {
|
||||||
val response = OkHttpClient().newCall(
|
val response = OkHttpClient().newCall(
|
||||||
Request.Builder()
|
Request.Builder()
|
||||||
.url("https://your-domain.com/api.php?api=categories")
|
.url("https://your-domain.com/app/apply.php?api=categories")
|
||||||
.build()
|
.build()
|
||||||
).execute()
|
).execute()
|
||||||
|
|
||||||
@@ -225,7 +225,7 @@ suspend fun checkName(name: String, threshold: Int = 80): CheckResult {
|
|||||||
|
|
||||||
val response = OkHttpClient().newCall(
|
val response = OkHttpClient().newCall(
|
||||||
Request.Builder()
|
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)
|
.post(formBody)
|
||||||
.build()
|
.build()
|
||||||
).execute()
|
).execute()
|
||||||
@@ -254,7 +254,7 @@ suspend fun submitPoem(data: PoemData): Boolean {
|
|||||||
|
|
||||||
val response = OkHttpClient().newCall(
|
val response = OkHttpClient().newCall(
|
||||||
Request.Builder()
|
Request.Builder()
|
||||||
.url("https://your-domain.com/api.php?api=submit")
|
.url("https://your-domain.com/app/apply.php?api=submit")
|
||||||
.post(formBody)
|
.post(formBody)
|
||||||
.build()
|
.build()
|
||||||
).execute()
|
).execute()
|
||||||
@@ -269,7 +269,7 @@ suspend fun submitPoem(data: PoemData): Boolean {
|
|||||||
```swift
|
```swift
|
||||||
// 获取分类
|
// 获取分类
|
||||||
func getCategories(completion: @escaping ([String]?, Error?) -> Void) {
|
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"]))
|
completion(nil, NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"]))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -299,7 +299,7 @@ func getCategories(completion: @escaping ([String]?, Error?) -> Void) {
|
|||||||
|
|
||||||
// 检查名称
|
// 检查名称
|
||||||
func checkName(name: String, threshold: Int = 80, completion: @escaping (CheckResult?, 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"]))
|
completion(nil, NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"]))
|
||||||
return
|
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) {
|
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")
|
completion(false, "Invalid URL")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -424,7 +424,7 @@ import 'dart:convert';
|
|||||||
// 获取分类
|
// 获取分类
|
||||||
Future<List<String>> getCategories() async {
|
Future<List<String>> getCategories() async {
|
||||||
final response = await http.get(
|
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) {
|
if (response.statusCode == 200) {
|
||||||
@@ -442,7 +442,7 @@ Future<CheckResult> checkName({
|
|||||||
int threshold = 80,
|
int threshold = 80,
|
||||||
}) async {
|
}) async {
|
||||||
final response = await http.post(
|
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: {
|
body: {
|
||||||
'name': name,
|
'name': name,
|
||||||
'threshold': threshold.toString(),
|
'threshold': threshold.toString(),
|
||||||
@@ -474,7 +474,7 @@ Future<bool> submitPoem({
|
|||||||
int threshold = 80,
|
int threshold = 80,
|
||||||
}) async {
|
}) async {
|
||||||
final response = await http.post(
|
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: {
|
body: {
|
||||||
'name': name,
|
'name': name,
|
||||||
'catename': catename,
|
'catename': catename,
|
||||||
|
|||||||
23
ht/api.php
23
ht/api.php
@@ -167,7 +167,14 @@ if (isset($_GET['api'])) {
|
|||||||
exit;
|
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) {
|
foreach ($required as $field) {
|
||||||
if (!isset($_POST[$field]) || empty(trim($_POST[$field]))) {
|
if (!isset($_POST[$field]) || empty(trim($_POST[$field]))) {
|
||||||
echo json_encode(['ok' => false, 'error' => "缺少必填字段:{$field}"]);
|
echo json_encode(['ok' => false, 'error' => "缺少必填字段:{$field}"]);
|
||||||
@@ -175,13 +182,15 @@ if (isset($_GET['api'])) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$captcha = trim($_POST['captcha']);
|
if (!$isFlutter) {
|
||||||
$captcha_key = 'captcha_' . session_id();
|
$captcha = trim($_POST['captcha']);
|
||||||
if (!isset($_SESSION[$captcha_key]) || $_SESSION[$captcha_key] != $captcha) {
|
$captcha_key = 'captcha_' . session_id();
|
||||||
echo json_encode(['ok' => false, 'error' => '验证码错误,请重新输入']);
|
if (!isset($_SESSION[$captcha_key]) || $_SESSION[$captcha_key] != $captcha) {
|
||||||
exit;
|
echo json_encode(['ok' => false, 'error' => '验证码错误,请重新输入']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
unset($_SESSION[$captcha_key]);
|
||||||
}
|
}
|
||||||
unset($_SESSION[$captcha_key]);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$name = trim($_POST['name']);
|
$name = trim($_POST['name']);
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class HttpClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// POST请求
|
/// POST请求 - JSON格式
|
||||||
static Future<HttpResponse> post(
|
static Future<HttpResponse> post(
|
||||||
String path, {
|
String path, {
|
||||||
Map<String, dynamic>? data,
|
Map<String, dynamic>? data,
|
||||||
@@ -65,7 +65,23 @@ class HttpClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 通用请求方法
|
/// POST请求 - FormData格式
|
||||||
|
static Future<HttpResponse> postForm(
|
||||||
|
String path, {
|
||||||
|
Map<String, dynamic>? data,
|
||||||
|
Map<String, String>? headers,
|
||||||
|
Duration? timeout,
|
||||||
|
}) async {
|
||||||
|
return _requestForm(
|
||||||
|
'POST',
|
||||||
|
path,
|
||||||
|
data: data,
|
||||||
|
headers: headers,
|
||||||
|
timeout: timeout,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 通用请求方法 - JSON格式
|
||||||
static Future<HttpResponse> _request(
|
static Future<HttpResponse> _request(
|
||||||
String method,
|
String method,
|
||||||
String path, {
|
String path, {
|
||||||
@@ -147,6 +163,90 @@ class HttpClient {
|
|||||||
throw HttpException('请求失败:$e');
|
throw HttpException('请求失败:$e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// FormData格式请求方法
|
||||||
|
static Future<HttpResponse> _requestForm(
|
||||||
|
String method,
|
||||||
|
String path, {
|
||||||
|
Map<String, dynamic>? queryParameters,
|
||||||
|
Map<String, dynamic>? data,
|
||||||
|
Map<String, String>? 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<String, String>(),
|
||||||
|
);
|
||||||
|
} 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响应类
|
/// HTTP响应类
|
||||||
|
|||||||
815
lib/views/profile/expand/manu-script.dart
Normal file
815
lib/views/profile/expand/manu-script.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ import 'guide/permission.dart';
|
|||||||
import 'guide/app-data.dart';
|
import 'guide/app-data.dart';
|
||||||
import 'theme/app-diy.dart';
|
import 'theme/app-diy.dart';
|
||||||
import 'expand/vote.dart';
|
import 'expand/vote.dart';
|
||||||
|
import 'expand/manu-script.dart';
|
||||||
|
|
||||||
class ProfilePage extends StatefulWidget {
|
class ProfilePage extends StatefulWidget {
|
||||||
const ProfilePage({super.key});
|
const ProfilePage({super.key});
|
||||||
@@ -670,11 +671,12 @@ class _ProfilePageState extends State<ProfilePage>
|
|||||||
Icons.analytics,
|
Icons.analytics,
|
||||||
() => _showSnackBar('软件开发进度'),
|
() => _showSnackBar('软件开发进度'),
|
||||||
),
|
),
|
||||||
_buildSettingsItem(
|
_buildSettingsItem('去投稿', Icons.edit_note, () {
|
||||||
'去投稿',
|
Navigator.push(
|
||||||
Icons.analytics,
|
context,
|
||||||
() => _showSnackBar('支持原创和古诗'),
|
MaterialPageRoute(builder: (context) => const ManuscriptPage()),
|
||||||
),
|
);
|
||||||
|
}),
|
||||||
]),
|
]),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// === 帮助支持组 ===
|
// === 帮助支持组 ===
|
||||||
|
|||||||
Reference in New Issue
Block a user