feat: 新增多模块后端管理、数据同步工具与鸿蒙路由适配

本次提交新增了以下核心内容:
1. 后端管理模块:包含字体同步、插件元数据、插件用户设置、稍后读消息/共享列表的控制器、模型、验证器与多语言配置
2. Flutter数据同步模块:统一的事件总线与兼容层,替代分散的StreamController
3. 鸿蒙端路由适配:完整的路由定义、构建器与占位组件
4. 后端API接口:字体同步与插件更新的服务端API,支持自动建表与跨域请求
5. 鸿蒙权限校验脚本:用于校验module.json5与string.json的权限声明一致性
This commit is contained in:
Developer
2026-06-01 05:50:13 +08:00
parent 9ea8d3d606
commit 5a083bdbab
159 changed files with 15621 additions and 10565 deletions

View File

@@ -1,9 +1,9 @@
/// ============================================================
/// 闲言APP — 注册区域组件
/// 创建时间: 2026-05-10
/// 更新时间: 2026-05-23
/// 更新时间: 2026-06-01
/// 作用: 登录页面的注册区域,分步式注册流程
/// 上次更新: 添加Tips提醒卡片邮箱已注册跳转登录弹窗
/// 上次更新: 接入多语言翻译系统,替换硬编码中文文本
/// ============================================================
import 'package:flutter/cupertino.dart';
@@ -14,6 +14,8 @@ import '../../../../core/theme/app_spacing.dart';
import '../../../../core/theme/app_typography.dart';
import '../../../../core/theme/app_radius.dart';
import '../../../../core/utils/logger.dart';
import '../../../../l10n/translation_resolver.dart';
import '../../../../l10n/types/t.dart';
import '../../../../shared/widgets/feedback/app_toast.dart';
import '../../../../shared/widgets/containers/glass_container.dart';
import '../providers/auth_provider.dart';
@@ -94,16 +96,19 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
@override
Widget build(BuildContext context) {
final authState = ref.watch(authProvider);
final t = ref.watch(translationsProvider);
final auth = t.auth;
final common = t.common;
return Column(
children: [
_buildHeader(),
_buildHeader(auth),
_buildStepIndicator(),
const SizedBox(height: AppSpacing.md),
GlassContainer(
depth: GlassDepth.elevated,
padding: const EdgeInsets.all(AppSpacing.lg),
child: _buildRegStepContent(authState),
child: _buildRegStepContent(authState, auth, common),
),
const SizedBox(height: AppSpacing.md),
CupertinoButton(
@@ -112,7 +117,7 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
setState(() => _regStep = 1);
},
child: Text(
'已有账号?去登录',
auth.hasAccountLogin,
style: AppTypography.subhead.copyWith(color: ext.accent),
),
),
@@ -120,7 +125,7 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
);
}
Widget _buildHeader() {
Widget _buildHeader(TAuth auth) {
return Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.xl),
child: Row(
@@ -148,7 +153,7 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'创建账号',
auth.createAccount,
style: AppTypography.title2.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w700,
@@ -156,7 +161,7 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
),
const SizedBox(height: 2),
Text(
'注册一个新账号开始使用',
auth.registerNewAccount,
style: AppTypography.subhead.copyWith(
color: ext.textSecondary,
),
@@ -196,25 +201,25 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
);
}
Widget _buildRegStepContent(AuthState authState) {
Widget _buildRegStepContent(AuthState authState, TAuth auth, TCommon common) {
switch (_regStep) {
case 1:
return Column(
children: [
LabeledInputField(
label: '用户名',
label: auth.username,
labelIcon: CupertinoIcons.person_add,
controller: _usernameController,
placeholder: '3-30位字母/数字/下划线/中文',
placeholder: auth.usernameHint,
icon: CupertinoIcons.person_add,
ext: ext,
),
const SizedBox(height: AppSpacing.md),
LabeledInputField(
label: '邮箱(必填)',
label: auth.emailRequired,
labelIcon: CupertinoIcons.mail,
controller: _emailController,
placeholder: '用于验证和找回密码',
placeholder: auth.emailHint,
icon: CupertinoIcons.mail,
ext: ext,
keyboardType: TextInputType.emailAddress,
@@ -224,6 +229,8 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
width: double.infinity,
height: 50,
child: CupertinoButton(
padding: EdgeInsets.zero,
minimumSize: Size.zero,
color: ext.accent,
borderRadius: AppRadius.mdBorder,
onPressed:
@@ -242,7 +249,7 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
),
const SizedBox(width: 6),
Text(
'下一步',
auth.nextStep,
style: AppTypography.callout.copyWith(
color: CupertinoColors.white,
fontWeight: FontWeight.w600,
@@ -265,7 +272,7 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
Icon(CupertinoIcons.mail, size: 36, color: ext.accent),
const SizedBox(height: AppSpacing.xs),
Text(
'验证码已发送至 ${_emailController.text.trim()}',
'${auth.codeSentTo} ${_emailController.text.trim()}',
style: AppTypography.footnote.copyWith(
color: ext.textSecondary,
),
@@ -275,10 +282,10 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
),
),
LabeledInputField(
label: '验证码',
label: auth.verifyCode,
labelIcon: CupertinoIcons.number,
controller: _regCodeController,
placeholder: '输入6位验证码',
placeholder: auth.enterCodeHint,
icon: CupertinoIcons.number,
ext: ext,
keyboardType: TextInputType.number,
@@ -294,7 +301,7 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
child: _emsSending
? CupertinoActivityIndicator(radius: 6, color: ext.accent)
: Text(
_emsCountdown > 0 ? '${_emsCountdown}s' : '重新发送',
_emsCountdown > 0 ? '${_emsCountdown}s' : auth.resend,
style: AppTypography.caption1.copyWith(
color: ext.accent,
),
@@ -331,7 +338,7 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
),
const SizedBox(height: 2),
Text(
'若未收到验证码,请检查垃圾邮箱。如需帮助,请联系客服。',
auth.codeNotReceivedTip,
style: AppTypography.caption2.copyWith(
color: ext.textSecondary,
),
@@ -349,6 +356,8 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
child: SizedBox(
height: 50,
child: CupertinoButton(
padding: EdgeInsets.zero,
minimumSize: Size.zero,
color: ext.bgSecondary,
borderRadius: AppRadius.mdBorder,
onPressed: () => setState(() => _regStep = 1),
@@ -363,7 +372,7 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
),
const SizedBox(width: 6),
Text(
'上一步',
auth.prevStep,
style: AppTypography.callout.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
@@ -379,6 +388,8 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
child: SizedBox(
height: 50,
child: CupertinoButton(
padding: EdgeInsets.zero,
minimumSize: Size.zero,
color: ext.accent,
borderRadius: AppRadius.mdBorder,
onPressed: _regCodeController.text.trim().isNotEmpty
@@ -395,7 +406,7 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
),
const SizedBox(width: 6),
Text(
'下一步',
auth.nextStep,
style: AppTypography.callout.copyWith(
color: CupertinoColors.white,
fontWeight: FontWeight.w600,
@@ -415,10 +426,10 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
return Column(
children: [
LabeledInputField(
label: '设置密码',
label: auth.setPassword,
labelIcon: CupertinoIcons.lock,
controller: _regPasswordController,
placeholder: '6-30位密码',
placeholder: auth.passwordHint,
icon: CupertinoIcons.lock,
ext: ext,
obscureText: _obscureRegPassword,
@@ -427,7 +438,10 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
onTap: () =>
setState(() => _obscureRegPassword = !_obscureRegPassword),
child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 44, minHeight: 44),
constraints: const BoxConstraints(
minWidth: 44,
minHeight: 44,
),
child: Center(
child: Icon(
_obscureRegPassword
@@ -442,10 +456,10 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
),
const SizedBox(height: AppSpacing.md),
LabeledInputField(
label: '确认密码',
label: auth.confirmPassword,
labelIcon: CupertinoIcons.lock_shield,
controller: _regConfirmPasswordController,
placeholder: '再次输入密码',
placeholder: auth.confirmPasswordHint,
icon: CupertinoIcons.lock_shield,
ext: ext,
obscureText: true,
@@ -477,7 +491,7 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
),
const SizedBox(width: AppSpacing.sm),
Text(
'🛡️ 密保问题(选填)',
auth.secQuestionOptional,
style: AppTypography.subhead.copyWith(
color: _showSecQuestion
? ext.accent
@@ -486,7 +500,9 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
),
const Spacer(),
Text(
_selectedSecQuestionText.isNotEmpty ? '已选择' : '增强账号安全',
_selectedSecQuestionText.isNotEmpty
? auth.selected
: auth.enhanceSecurity,
style: AppTypography.caption1.copyWith(
color: ext.textHint,
),
@@ -516,7 +532,7 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
borderRadius: AppRadius.mdBorder,
onPressed: _secQuestions.isEmpty
? null
: () => _showSecQuestionPicker(context),
: () => _showSecQuestionPicker(context, auth, common),
child: Row(
children: [
Icon(
@@ -529,7 +545,7 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
child: Text(
_selectedSecQuestionText.isNotEmpty
? _selectedSecQuestionText
: '选择密保问题',
: auth.selectSecQuestion,
style: AppTypography.subhead.copyWith(
color: _selectedSecQuestionText.isNotEmpty
? ext.textPrimary
@@ -554,7 +570,7 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
),
child: CupertinoTextField(
controller: _secAnswerController,
placeholder: '输入密保答案1-50位',
placeholder: auth.enterSecAnswerHint,
placeholderStyle: AppTypography.body.copyWith(
color: ext.textHint,
),
@@ -598,7 +614,7 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
),
const SizedBox(width: AppSpacing.sm),
Text(
'订阅闲言团队的邮箱推送',
auth.subscribeEmail,
style: AppTypography.footnote.copyWith(
color: ext.textSecondary,
),
@@ -613,6 +629,8 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
child: SizedBox(
height: 50,
child: CupertinoButton(
padding: EdgeInsets.zero,
minimumSize: Size.zero,
color: ext.bgSecondary,
borderRadius: AppRadius.mdBorder,
onPressed: () => setState(() => _regStep = 2),
@@ -627,7 +645,7 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
),
const SizedBox(width: 6),
Text(
'上一步',
auth.prevStep,
style: AppTypography.callout.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
@@ -643,6 +661,8 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
child: SizedBox(
height: 50,
child: CupertinoButton(
padding: EdgeInsets.zero,
minimumSize: Size.zero,
color: ext.accent,
borderRadius: AppRadius.mdBorder,
onPressed:
@@ -666,7 +686,7 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
),
const SizedBox(width: 6),
Text(
'完成注册',
auth.completeRegister,
style: AppTypography.callout.copyWith(
color: CupertinoColors.white,
fontWeight: FontWeight.w600,
@@ -688,6 +708,7 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
}
Future<void> _handleRegister() async {
final t = ref.read(translationsProvider);
final username = _usernameController.text.trim();
final password = _regPasswordController.text;
final confirmPassword = _regConfirmPasswordController.text;
@@ -695,22 +716,22 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
final code = _regCodeController.text.trim();
if (username.isEmpty || password.isEmpty || email.isEmpty) {
AppToast.showWarning('请填写所有必填项');
AppToast.showWarning(t.auth.pleaseFillRequired);
return;
}
if (password != confirmPassword) {
AppToast.showError('两次密码不一致');
AppToast.showError(t.auth.passwordMismatch);
return;
}
if (password.length < 6) {
AppToast.showWarning('密码长度不能少于6位');
AppToast.showWarning(t.auth.passwordTooShort);
return;
}
if (code.isNotEmpty && !EmailService.verifyCode(email: email, code: code)) {
AppToast.showError('验证码错误');
AppToast.showError(t.auth.codeError);
return;
}
@@ -726,16 +747,17 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
: null,
);
if (success && mounted) {
AppToast.showSuccess('注册成功,欢迎加入!');
AppToast.showSuccess(t.auth.registerSuccess);
widget.onRegisterSuccess();
}
}
Future<void> _sendCode(String email) async {
final t = ref.read(translationsProvider);
if (_emsSending || _emsCountdown > 0 || email.isEmpty) return;
if (!email.contains('@')) {
AppToast.showWarning('请输入有效的邮箱地址');
AppToast.showWarning(t.auth.pleaseEnterValidEmail);
return;
}
@@ -751,13 +773,13 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
code: code,
);
if (!ok && mounted) {
AppToast.showError('验证码发送失败,请检查邮箱地址');
AppToast.showError(t.auth.codeSendFailed);
setState(() {
_emsSending = false;
_emsCountdown = 0;
});
} else if (mounted) {
AppToast.showSuccess('验证码已发送');
AppToast.showSuccess(t.auth.codeSent);
setState(() => _emsSending = false);
}
} catch (e) {
@@ -769,7 +791,7 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
errorMsg.contains('exists')) {
_showEmailRegisteredDialog(email);
} else {
AppToast.showError('验证码发送失败');
AppToast.showError(t.auth.codeSendFailedShort);
}
setState(() {
_emsSending = false;
@@ -789,19 +811,20 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
}
void _showEmailRegisteredDialog(String email) {
final t = ref.read(translationsProvider);
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: const Text('邮箱已注册'),
content: Text('该邮箱 ($email) 已被注册。是否跳转到登录页面?'),
title: Text(t.auth.emailRegistered),
content: Text(t.auth.emailRegisteredHint.replaceAll('{email}', email)),
actions: [
CupertinoDialogAction(
child: const Text('取消'),
child: Text(t.common.cancel),
onPressed: () => Navigator.pop(ctx),
),
CupertinoDialogAction(
isDefaultAction: true,
child: const Text('去登录'),
child: Text(t.auth.goLogin),
onPressed: () {
Navigator.pop(ctx);
widget.onSwitchToLogin();
@@ -812,7 +835,11 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
);
}
void _showSecQuestionPicker(BuildContext context) {
void _showSecQuestionPicker(
BuildContext context,
TAuth auth,
TCommon common,
) {
showCupertinoModalPopup<void>(
context: context,
builder: (ctx) => Container(
@@ -825,7 +852,7 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
children: [
CupertinoButton(
child: Text(
'取消',
common.cancel,
style: AppTypography.subhead.copyWith(
color: ext.textSecondary,
),
@@ -833,7 +860,7 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
onPressed: () => Navigator.pop(ctx),
),
Text(
'选择密保问题',
auth.selectSecQuestion,
style: AppTypography.headline.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
@@ -841,7 +868,7 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
),
CupertinoButton(
child: Text(
'确定',
common.confirm,
style: AppTypography.subhead.copyWith(
color: ext.accent,
fontWeight: FontWeight.w600,