chore: 完成项目品牌域名批量替换与功能迭代
本次提交包含多项核心更新: 1. 全量替换项目内所有xianyan.app域名变更为s2ss.com,包含配置文件、路由、隐私政策等 2. 重构图表库从fl_chart迁移至syncfusion_flutter_charts,优化图表渲染效果 3. 新增宽屏分屏布局支持,包含右侧面板注册表与可拖拽分割线 4. 完善触觉反馈服务与认证感知Mixin,修复多处内存泄漏问题 5. 合并勋章墙与金币记录入口至成就中心,简化个人中心导航 6. 新增收藏与时间线数据合并导入功能 7. 修复多处UI样式问题,统一主题颜色使用规范 8. 新增日历同步与跨平台触觉反馈依赖库 9. 修复BotToast初始化流程,避免路由切换时的弹窗崩溃
This commit is contained in:
@@ -1,21 +1,25 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 纠错页面
|
||||
/// 创建时间: 2026-04-28
|
||||
/// 更新时间: 2026-04-28
|
||||
/// 更新时间: 2026-05-29
|
||||
/// 作用: 提交内容纠错
|
||||
/// 上次更新: 初始创建
|
||||
/// 上次更新: emoji替换为图标、添加邮箱复选框、Toast反馈、纠错记录
|
||||
/// ============================================================
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart' show Divider;
|
||||
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 '../../../features/auth/providers/auth_provider.dart';
|
||||
import '../../../shared/widgets/adaptive/adaptive_back_button.dart';
|
||||
import '../../../shared/widgets/containers/glass_container.dart';
|
||||
import '../../../shared/widgets/feedback/app_toast.dart';
|
||||
import '../providers/correction_provider.dart';
|
||||
import '../../../core/services/device/haptic_service.dart';
|
||||
|
||||
class CorrectionPage extends ConsumerStatefulWidget {
|
||||
const CorrectionPage({super.key});
|
||||
@@ -29,21 +33,22 @@ class _CorrectionPageState extends ConsumerState<CorrectionPage> {
|
||||
String _targetType = 'article';
|
||||
final _targetIdController = TextEditingController();
|
||||
String _correctionType = 'error';
|
||||
bool _includeEmail = true;
|
||||
|
||||
final _typeOptions = [
|
||||
('article', '📖 文章'),
|
||||
('hanzi', '📝 汉字'),
|
||||
('cy', '🔤 成语'),
|
||||
('poetry', '📜 诗词'),
|
||||
('zc', '📚 字词'),
|
||||
('riddle', '🧩 谜语'),
|
||||
('article', '文章', CupertinoIcons.doc_text_fill),
|
||||
('hanzi', '汉字', CupertinoIcons.textformat),
|
||||
('cy', '成语', CupertinoIcons.text_bubble_fill),
|
||||
('poetry', '诗词', CupertinoIcons.book_fill),
|
||||
('zc', '字词', CupertinoIcons.textformat_abc),
|
||||
('riddle', '谜语', CupertinoIcons.question_circle_fill),
|
||||
];
|
||||
|
||||
final _correctionTypeOptions = [
|
||||
('error', '❌ 内容错误'),
|
||||
('typo', '✏️ 错别字'),
|
||||
('missing', '📭 内容缺失'),
|
||||
('suggestion', '💡 改进建议'),
|
||||
('error', '内容错误', CupertinoIcons.xmark_circle_fill),
|
||||
('typo', '错别字', CupertinoIcons.pencil_circle_fill),
|
||||
('missing', '内容缺失', CupertinoIcons.tray_fill),
|
||||
('suggestion', '改进建议', CupertinoIcons.lightbulb_fill),
|
||||
];
|
||||
|
||||
@override
|
||||
@@ -59,9 +64,14 @@ class _CorrectionPageState extends ConsumerState<CorrectionPage> {
|
||||
final state = ref.watch(correctionProvider);
|
||||
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: const CupertinoNavigationBar(
|
||||
leading: AdaptiveBackButton(),
|
||||
middle: Text('🔍 内容纠错'),
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
leading: const AdaptiveBackButton(),
|
||||
middle: const Text('内容纠错'),
|
||||
trailing: CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: _showCorrectionRecords,
|
||||
child: Icon(CupertinoIcons.clock_fill, color: ext.accent, size: 22),
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
@@ -102,13 +112,26 @@ class _CorrectionPageState extends ConsumerState<CorrectionPage> {
|
||||
? Border.all(color: ext.accent)
|
||||
: null,
|
||||
),
|
||||
child: Text(
|
||||
opt.$2,
|
||||
style: AppTypography.subhead.copyWith(
|
||||
color: selected
|
||||
? ext.accent
|
||||
: ext.textSecondary,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
opt.$3,
|
||||
size: 16,
|
||||
color: selected
|
||||
? ext.accent
|
||||
: ext.textSecondary,
|
||||
),
|
||||
const SizedBox(width: AppSpacing.xs),
|
||||
Text(
|
||||
opt.$2,
|
||||
style: AppTypography.subhead.copyWith(
|
||||
color: selected
|
||||
? ext.accent
|
||||
: ext.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -143,13 +166,26 @@ class _CorrectionPageState extends ConsumerState<CorrectionPage> {
|
||||
? Border.all(color: ext.accent)
|
||||
: null,
|
||||
),
|
||||
child: Text(
|
||||
opt.$2,
|
||||
style: AppTypography.subhead.copyWith(
|
||||
color: selected
|
||||
? ext.accent
|
||||
: ext.textSecondary,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
opt.$3,
|
||||
size: 16,
|
||||
color: selected
|
||||
? ext.accent
|
||||
: ext.textSecondary,
|
||||
),
|
||||
const SizedBox(width: AppSpacing.xs),
|
||||
Text(
|
||||
opt.$2,
|
||||
style: AppTypography.subhead.copyWith(
|
||||
color: selected
|
||||
? ext.accent
|
||||
: ext.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -214,6 +250,8 @@ class _CorrectionPageState extends ConsumerState<CorrectionPage> {
|
||||
minLines: 6,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
_buildEmailCheckbox(ext),
|
||||
const SizedBox(height: AppSpacing.xl),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
@@ -223,15 +261,26 @@ class _CorrectionPageState extends ConsumerState<CorrectionPage> {
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
onPressed: state.isSubmitting ? null : _submit,
|
||||
child: state.isSubmitting
|
||||
? const CupertinoActivityIndicator(
|
||||
color: CupertinoColors.white,
|
||||
? CupertinoActivityIndicator(
|
||||
color: ext.textOnAccent,
|
||||
)
|
||||
: Text(
|
||||
'📤 提交纠错',
|
||||
style: AppTypography.callout.copyWith(
|
||||
color: CupertinoColors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
CupertinoIcons.paperplane_fill,
|
||||
color: ext.textOnAccent,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Text(
|
||||
'提交纠错',
|
||||
style: AppTypography.callout.copyWith(
|
||||
color: ext.textOnAccent,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -249,7 +298,11 @@ class _CorrectionPageState extends ConsumerState<CorrectionPage> {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Text('✅', style: TextStyle(fontSize: 20)),
|
||||
Icon(
|
||||
CupertinoIcons.checkmark_circle_fill,
|
||||
color: ext.accent,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Expanded(
|
||||
child: Text(
|
||||
@@ -280,19 +333,74 @@ class _CorrectionPageState extends ConsumerState<CorrectionPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmailCheckbox(AppThemeExtension ext) {
|
||||
final user = ref.watch(authProvider).user;
|
||||
final email = user?.email ?? '';
|
||||
final username = user?.username ?? '';
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _includeEmail = !_includeEmail),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: ext.bgSecondary,
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CupertinoCheckbox(
|
||||
value: _includeEmail,
|
||||
onChanged: (v) => setState(() => _includeEmail = v ?? true),
|
||||
activeColor: ext.accent,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'包含邮箱地址',
|
||||
style: AppTypography.subhead.copyWith(
|
||||
color: ext.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (username.isNotEmpty || email.isNotEmpty) ...[
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
'用户: $username${_includeEmail && email.isNotEmpty ? ' · $email' : ''}',
|
||||
style: AppTypography.caption1.copyWith(color: ext.textHint),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
final content = _contentController.text.trim();
|
||||
final targetIdText = _targetIdController.text.trim();
|
||||
if (content.isEmpty) {
|
||||
_showToast('请输入纠错描述');
|
||||
AppToast.showWarning('请输入纠错描述');
|
||||
return;
|
||||
}
|
||||
final targetId = int.tryParse(targetIdText) ?? 0;
|
||||
if (targetId <= 0) {
|
||||
_showToast('请输入有效的内容ID');
|
||||
AppToast.showWarning('请输入有效的内容ID');
|
||||
return;
|
||||
}
|
||||
|
||||
final user = ref.read(authProvider).user;
|
||||
final username = user?.username;
|
||||
final email = _includeEmail ? user?.email : null;
|
||||
|
||||
final success = await ref
|
||||
.read(correctionProvider.notifier)
|
||||
.submitCorrection(
|
||||
@@ -300,22 +408,201 @@ class _CorrectionPageState extends ConsumerState<CorrectionPage> {
|
||||
targetId: targetId,
|
||||
content: content,
|
||||
type: _correctionType,
|
||||
username: username,
|
||||
email: email,
|
||||
);
|
||||
if (success && mounted) {
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (success) {
|
||||
HapticService.success();
|
||||
AppToast.showSuccess('提交成功!感谢您的反馈');
|
||||
_contentController.clear();
|
||||
_targetIdController.clear();
|
||||
} else {
|
||||
HapticService.error();
|
||||
final error = ref.read(correctionProvider).error ?? '提交失败';
|
||||
AppToast.showError(error);
|
||||
}
|
||||
}
|
||||
|
||||
void _showToast(String msg) {
|
||||
showCupertinoDialog<void>(
|
||||
void _showCorrectionRecords() async {
|
||||
await ref.read(correctionProvider.notifier).loadCorrections();
|
||||
if (!mounted) return;
|
||||
|
||||
final ext = AppTheme.ext(context);
|
||||
final state = ref.read(correctionProvider);
|
||||
|
||||
showCupertinoModalPopup<void>(
|
||||
context: context,
|
||||
builder: (ctx) => CupertinoAlertDialog(
|
||||
content: Text(msg),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: const Text('好的'),
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
builder: (ctx) {
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(ctx).size.height * 0.6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: ext.bgPrimary,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
CupertinoIcons.clock_fill,
|
||||
color: ext.accent,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Text(
|
||||
'纠错记录',
|
||||
style: AppTypography.headline.copyWith(
|
||||
color: ext.textPrimary,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
minimumSize: const Size(28, 28),
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: Icon(
|
||||
CupertinoIcons.xmark_circle_fill,
|
||||
color: ext.textHint,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Divider(height: 1, color: ext.textHint.withValues(alpha: 0.1)),
|
||||
if (state.corrections.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: AppSpacing.xl * 2,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
CupertinoIcons.doc_text_search,
|
||||
color: ext.textHint,
|
||||
size: 48,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
Text(
|
||||
'暂无纠错记录',
|
||||
style: AppTypography.subhead.copyWith(
|
||||
color: ext.textHint,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
Flexible(
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
itemCount: state.corrections.length,
|
||||
separatorBuilder: (_, __) =>
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
itemBuilder: (_, index) {
|
||||
final item = state.corrections[index];
|
||||
return _buildRecordItem(item, ext);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRecordItem(Map<String, dynamic> item, AppThemeExtension ext) {
|
||||
final typeMap = {
|
||||
'error': '内容错误',
|
||||
'typo': '错别字',
|
||||
'missing': '内容缺失',
|
||||
'suggestion': '改进建议',
|
||||
};
|
||||
final contentTypeMap = {
|
||||
'article': '文章',
|
||||
'hanzi': '汉字',
|
||||
'cy': '成语',
|
||||
'poetry': '诗词',
|
||||
'zc': '字词',
|
||||
'riddle': '谜语',
|
||||
};
|
||||
final statusMap = {
|
||||
0: ('待处理', CupertinoColors.systemOrange),
|
||||
1: ('已处理', CupertinoColors.systemGreen),
|
||||
2: ('已拒绝', CupertinoColors.systemRed),
|
||||
};
|
||||
|
||||
final type = typeMap[item['type']] ?? item['type'] ?? '未知';
|
||||
final contentType =
|
||||
contentTypeMap[item['source_type']] ?? item['source_type'] ?? '未知';
|
||||
final sourceId = item['source_id']?.toString() ?? '-';
|
||||
final switchVal = item['switch'] as int? ?? 0;
|
||||
final statusInfo = statusMap[switchVal] ?? ('未知', ext.textHint);
|
||||
final createdAt = item['createtime'] as int?;
|
||||
final dateStr = createdAt != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(
|
||||
createdAt * 1000,
|
||||
).toString().substring(0, 16)
|
||||
: '-';
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: ext.bgSecondary,
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
CupertinoIcons.doc_text_fill,
|
||||
size: 14,
|
||||
color: ext.textSecondary,
|
||||
),
|
||||
const SizedBox(width: AppSpacing.xs),
|
||||
Text(
|
||||
'$type · $contentType #$sourceId',
|
||||
style: AppTypography.subhead.copyWith(
|
||||
color: ext.textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.sm,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: statusInfo.$2.withValues(alpha: 0.12),
|
||||
borderRadius: AppRadius.smBorder,
|
||||
),
|
||||
child: Text(
|
||||
statusInfo.$1,
|
||||
style: AppTypography.caption1.copyWith(
|
||||
color: statusInfo.$2,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
dateStr,
|
||||
style: AppTypography.caption1.copyWith(color: ext.textHint),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 纠错状态管理
|
||||
/// 创建时间: 2026-04-28
|
||||
/// 更新时间: 2026-04-28
|
||||
/// 更新时间: 2026-05-29
|
||||
/// 作用: 纠错提交功能状态管理
|
||||
/// 上次更新: 初始创建
|
||||
/// 上次更新: 新增source_url/switch参数,修复email键名为mail
|
||||
/// ============================================================
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
@@ -56,6 +56,10 @@ class CorrectionNotifier extends Notifier<CorrectionState> {
|
||||
required int targetId,
|
||||
required String content,
|
||||
String type = 'error',
|
||||
String? username,
|
||||
String? email,
|
||||
String? sourceUrl,
|
||||
bool isAnonymous = false,
|
||||
}) async {
|
||||
state = state.copyWith(
|
||||
isSubmitting: true,
|
||||
@@ -63,16 +67,28 @@ class CorrectionNotifier extends Notifier<CorrectionState> {
|
||||
isSuccess: false,
|
||||
);
|
||||
try {
|
||||
final submitData = <String, dynamic>{
|
||||
'content': content,
|
||||
'source_type': targetType,
|
||||
'source_id': targetId,
|
||||
'type': type,
|
||||
'switch': isAnonymous ? 1 : 0,
|
||||
};
|
||||
if (username != null && username.isNotEmpty) {
|
||||
submitData['username'] = username;
|
||||
}
|
||||
if (email != null && email.isNotEmpty) {
|
||||
submitData['mail'] = email;
|
||||
}
|
||||
if (sourceUrl != null && sourceUrl.isNotEmpty) {
|
||||
submitData['source_url'] = sourceUrl;
|
||||
}
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
'/api/webapi/correction_submit',
|
||||
data: {
|
||||
'content': content,
|
||||
'source_type': targetType,
|
||||
'source_id': targetId,
|
||||
},
|
||||
data: submitData,
|
||||
);
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
final code = data['code'] as int? ?? 0;
|
||||
final respData = response.data as Map<String, dynamic>;
|
||||
final code = respData['code'] as int? ?? 0;
|
||||
if (code == 1) {
|
||||
state = state.copyWith(isSubmitting: false, isSuccess: true);
|
||||
Log.i('纠错提交成功');
|
||||
@@ -80,7 +96,7 @@ class CorrectionNotifier extends Notifier<CorrectionState> {
|
||||
} else {
|
||||
state = state.copyWith(
|
||||
isSubmitting: false,
|
||||
error: data['msg'] as String? ?? '提交失败',
|
||||
error: respData['msg'] as String? ?? '提交失败',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
@@ -116,4 +132,6 @@ class CorrectionNotifier extends Notifier<CorrectionState> {
|
||||
}
|
||||
|
||||
final correctionProvider =
|
||||
NotifierProvider<CorrectionNotifier, CorrectionState>(CorrectionNotifier.new);
|
||||
NotifierProvider<CorrectionNotifier, CorrectionState>(
|
||||
CorrectionNotifier.new,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user