feat: 新增API响应模型、缓存配置和状态管理

refactor: 优化网络请求和错误处理

fix: 修复颜色引用和UI细节问题

docs: 更新API文档和设计规范

chore: 清理无用文件和脚本

perf: 优化图片导出和压缩逻辑

build: 更新依赖和构建配置

style: 调整代码格式和注释

test: 添加接口验证脚本

ci: 更新CI配置和脚本
This commit is contained in:
Developer
2026-04-29 01:39:48 +08:00
parent b6441a8919
commit a4b7105999
158 changed files with 45166 additions and 6450 deletions

View 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),
),
],
),
);
}
}

View 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();
});