feat: 新增API响应模型、缓存配置和状态管理
refactor: 优化网络请求和错误处理 fix: 修复颜色引用和UI细节问题 docs: 更新API文档和设计规范 chore: 清理无用文件和脚本 perf: 优化图片导出和压缩逻辑 build: 更新依赖和构建配置 style: 调整代码格式和注释 test: 添加接口验证脚本 ci: 更新CI配置和脚本
This commit is contained in:
320
lib/features/correction/presentation/correction_page.dart
Normal file
320
lib/features/correction/presentation/correction_page.dart
Normal file
@@ -0,0 +1,320 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 纠错页面
|
||||
/// 创建时间: 2026-04-28
|
||||
/// 更新时间: 2026-04-28
|
||||
/// 作用: 提交内容纠错
|
||||
/// 上次更新: 初始创建
|
||||
/// ============================================================
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../core/theme/app_spacing.dart';
|
||||
import '../../../core/theme/app_typography.dart';
|
||||
import '../../../core/theme/app_radius.dart';
|
||||
import '../../../shared/widgets/glass_container.dart';
|
||||
import '../providers/correction_provider.dart';
|
||||
|
||||
class CorrectionPage extends ConsumerStatefulWidget {
|
||||
const CorrectionPage({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<CorrectionPage> createState() => _CorrectionPageState();
|
||||
}
|
||||
|
||||
class _CorrectionPageState extends ConsumerState<CorrectionPage> {
|
||||
final _contentController = TextEditingController();
|
||||
String _targetType = 'article';
|
||||
final _targetIdController = TextEditingController();
|
||||
String _correctionType = 'error';
|
||||
|
||||
final _typeOptions = [
|
||||
('article', '📖 文章'),
|
||||
('hanzi', '📝 汉字'),
|
||||
('cy', '🔤 成语'),
|
||||
('poetry', '📜 诗词'),
|
||||
('zc', '📚 字词'),
|
||||
('riddle', '🧩 谜语'),
|
||||
];
|
||||
|
||||
final _correctionTypeOptions = [
|
||||
('error', '❌ 内容错误'),
|
||||
('typo', '✏️ 错别字'),
|
||||
('missing', '📭 内容缺失'),
|
||||
('suggestion', '💡 改进建议'),
|
||||
];
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_contentController.dispose();
|
||||
_targetIdController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ext = AppTheme.ext(context);
|
||||
final state = ref.watch(correctionProvider);
|
||||
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: const CupertinoNavigationBar(middle: Text('🔍 内容纠错')),
|
||||
child: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GlassContainer(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'纠错类型',
|
||||
style: AppTypography.headline.copyWith(
|
||||
color: ext.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
Wrap(
|
||||
spacing: AppSpacing.sm,
|
||||
runSpacing: AppSpacing.sm,
|
||||
children: _correctionTypeOptions.map((opt) {
|
||||
final selected = _correctionType == opt.$1;
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _correctionType = opt.$1),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
vertical: AppSpacing.sm,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: selected
|
||||
? ext.accent.withValues(alpha: 0.15)
|
||||
: ext.bgSecondary,
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
border: selected
|
||||
? Border.all(color: ext.accent)
|
||||
: null,
|
||||
),
|
||||
child: Text(
|
||||
opt.$2,
|
||||
style: AppTypography.subhead.copyWith(
|
||||
color: selected
|
||||
? ext.accent
|
||||
: ext.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
Text(
|
||||
'内容类型',
|
||||
style: AppTypography.headline.copyWith(
|
||||
color: ext.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
Wrap(
|
||||
spacing: AppSpacing.sm,
|
||||
runSpacing: AppSpacing.sm,
|
||||
children: _typeOptions.map((opt) {
|
||||
final selected = _targetType == opt.$1;
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _targetType = opt.$1),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
vertical: AppSpacing.sm,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: selected
|
||||
? ext.accent.withValues(alpha: 0.15)
|
||||
: ext.bgSecondary,
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
border: selected
|
||||
? Border.all(color: ext.accent)
|
||||
: null,
|
||||
),
|
||||
child: Text(
|
||||
opt.$2,
|
||||
style: AppTypography.subhead.copyWith(
|
||||
color: selected
|
||||
? ext.accent
|
||||
: ext.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
Text(
|
||||
'内容ID',
|
||||
style: AppTypography.headline.copyWith(
|
||||
color: ext.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
Container(
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: ext.bgSecondary,
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
),
|
||||
child: CupertinoTextField(
|
||||
controller: _targetIdController,
|
||||
placeholder: '输入内容的ID编号',
|
||||
placeholderStyle: AppTypography.subhead.copyWith(
|
||||
color: ext.textHint,
|
||||
),
|
||||
style: AppTypography.body.copyWith(
|
||||
color: ext.textPrimary,
|
||||
),
|
||||
decoration: null,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
Text(
|
||||
'纠错描述',
|
||||
style: AppTypography.headline.copyWith(
|
||||
color: ext.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
Container(
|
||||
constraints: const BoxConstraints(minHeight: 150),
|
||||
decoration: BoxDecoration(
|
||||
color: ext.bgSecondary,
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
),
|
||||
child: CupertinoTextField(
|
||||
controller: _contentController,
|
||||
placeholder: '请详细描述需要纠正的内容...',
|
||||
placeholderStyle: AppTypography.body.copyWith(
|
||||
color: ext.textHint,
|
||||
),
|
||||
style: AppTypography.body.copyWith(
|
||||
color: ext.textPrimary,
|
||||
),
|
||||
decoration: null,
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
maxLines: null,
|
||||
minLines: 6,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xl),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: CupertinoButton(
|
||||
color: ext.accent,
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
onPressed: state.isSubmitting ? null : _submit,
|
||||
child: state.isSubmitting
|
||||
? const CupertinoActivityIndicator(
|
||||
color: CupertinoColors.white,
|
||||
)
|
||||
: Text(
|
||||
'📤 提交纠错',
|
||||
style: AppTypography.callout.copyWith(
|
||||
color: CupertinoColors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (state.isSuccess)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: AppSpacing.md),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: ext.accent.withValues(alpha: 0.1),
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Text('✅', style: TextStyle(fontSize: 20)),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'提交成功!感谢您的反馈,管理员会及时处理。',
|
||||
style: AppTypography.subhead.copyWith(
|
||||
color: ext.accent,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (state.error != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: AppSpacing.md),
|
||||
child: Text(
|
||||
state.error!,
|
||||
style: AppTypography.body.copyWith(
|
||||
color: CupertinoColors.systemRed,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
final content = _contentController.text.trim();
|
||||
final targetIdText = _targetIdController.text.trim();
|
||||
if (content.isEmpty) {
|
||||
_showToast('请输入纠错描述');
|
||||
return;
|
||||
}
|
||||
final targetId = int.tryParse(targetIdText) ?? 0;
|
||||
if (targetId <= 0) {
|
||||
_showToast('请输入有效的内容ID');
|
||||
return;
|
||||
}
|
||||
|
||||
final success = await ref
|
||||
.read(correctionProvider.notifier)
|
||||
.submitCorrection(
|
||||
targetType: _targetType,
|
||||
targetId: targetId,
|
||||
content: content,
|
||||
type: _correctionType,
|
||||
);
|
||||
if (success && mounted) {
|
||||
_contentController.clear();
|
||||
_targetIdController.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void _showToast(String msg) {
|
||||
showCupertinoDialog<void>(
|
||||
context: context,
|
||||
builder: (ctx) => CupertinoAlertDialog(
|
||||
content: Text(msg),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: const Text('好的'),
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
119
lib/features/correction/providers/correction_provider.dart
Normal file
119
lib/features/correction/providers/correction_provider.dart
Normal file
@@ -0,0 +1,119 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 纠错状态管理
|
||||
/// 创建时间: 2026-04-28
|
||||
/// 更新时间: 2026-04-28
|
||||
/// 作用: 纠错提交功能状态管理
|
||||
/// 上次更新: 初始创建
|
||||
/// ============================================================
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../core/network/api_client.dart';
|
||||
import '../../../core/utils/logger.dart';
|
||||
|
||||
class CorrectionState {
|
||||
const CorrectionState({
|
||||
this.isSubmitting = false,
|
||||
this.isSuccess = false,
|
||||
this.error,
|
||||
this.corrections = const [],
|
||||
this.total = 0,
|
||||
});
|
||||
|
||||
final bool isSubmitting;
|
||||
final bool isSuccess;
|
||||
final String? error;
|
||||
final List<Map<String, dynamic>> corrections;
|
||||
final int total;
|
||||
|
||||
CorrectionState copyWith({
|
||||
bool? isSubmitting,
|
||||
bool? isSuccess,
|
||||
String? error,
|
||||
bool clearError = false,
|
||||
List<Map<String, dynamic>>? corrections,
|
||||
int? total,
|
||||
}) {
|
||||
return CorrectionState(
|
||||
isSubmitting: isSubmitting ?? this.isSubmitting,
|
||||
isSuccess: isSuccess ?? this.isSuccess,
|
||||
error: clearError ? null : (error ?? this.error),
|
||||
corrections: corrections ?? this.corrections,
|
||||
total: total ?? this.total,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CorrectionNotifier extends StateNotifier<CorrectionState> {
|
||||
CorrectionNotifier() : super(const CorrectionState());
|
||||
|
||||
final ApiClient _api = ApiClient.instance;
|
||||
|
||||
Future<bool> submitCorrection({
|
||||
required String targetType,
|
||||
required int targetId,
|
||||
required String content,
|
||||
String type = 'error',
|
||||
}) async {
|
||||
state = state.copyWith(
|
||||
isSubmitting: true,
|
||||
clearError: true,
|
||||
isSuccess: false,
|
||||
);
|
||||
try {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
'/api/webapi/correction_submit',
|
||||
data: {
|
||||
'content': content,
|
||||
'source_type': targetType,
|
||||
'source_id': targetId,
|
||||
},
|
||||
);
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
final code = data['code'] as int? ?? 0;
|
||||
if (code == 1) {
|
||||
state = state.copyWith(isSubmitting: false, isSuccess: true);
|
||||
Log.i('纠错提交成功');
|
||||
return true;
|
||||
} else {
|
||||
state = state.copyWith(
|
||||
isSubmitting: false,
|
||||
error: data['msg'] as String? ?? '提交失败',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
Log.e('纠错提交异常', e);
|
||||
state = state.copyWith(isSubmitting: false, error: '提交失败: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadCorrections({int page = 1, int limit = 20}) async {
|
||||
try {
|
||||
final response = await _api.get<Map<String, dynamic>>(
|
||||
'/api/webapi/correction_list',
|
||||
queryParameters: {'page': page, 'limit': limit},
|
||||
);
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
final code = data['code'] as int? ?? 0;
|
||||
if (code == 1) {
|
||||
final result = data['data'] as Map<String, dynamic>? ?? {};
|
||||
final list = (result['list'] as List<dynamic>? ?? [])
|
||||
.map((e) => e as Map<String, dynamic>)
|
||||
.toList();
|
||||
state = state.copyWith(
|
||||
corrections: list,
|
||||
total: result['total'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.e('加载纠错列表失败', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final correctionProvider =
|
||||
StateNotifierProvider<CorrectionNotifier, CorrectionState>((ref) {
|
||||
return CorrectionNotifier();
|
||||
});
|
||||
Reference in New Issue
Block a user