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:
Developer
2026-05-29 10:06:55 +08:00
parent 5b9a0320d5
commit 5a49d20c8a
93 changed files with 17861 additions and 3340 deletions

View File

@@ -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),
),
],
),

View File

@@ -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,
);