登录流程已全部重构

This commit is contained in:
zhutao
2025-09-23 11:47:29 +08:00
parent a4992a063b
commit 8988b3feea
71 changed files with 2036 additions and 901 deletions

View File

@@ -1,16 +0,0 @@
# food_health
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 537 KiB

5
l10n.yaml Normal file
View File

@@ -0,0 +1,5 @@
arb-dir: lib/l10n/arb
template-arb-file: app_zh.arb
output-dir: lib/l10n
synthetic-package: false
output-localization-file: app_localizations.dart

View File

@@ -1,7 +1,7 @@
import 'package:dio/dio.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import '../../providers/app_store.dart';
import '../../stores/app_store.dart';
import '../dto/base_dto.dart';
///请求拦截器

View File

@@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
abstract class AppColorsBase {
/// 品牌主色
Color get primary;
Color get secondary;
// 灰度
Color get textPrimary;
Color get textSecondary;
// 扩展颜色
Color get success;
Color get warning;
Color get info;
Color get danger;
// 容器色
Color get surfaceContainerLowest;
Color get surfaceContainerLow;
Color get surfaceContainer;
Color get surfaceContainerHigh; //白色卡片 / item
}

View File

@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'app_colors_base.dart'; // 假设你之前定义了 AppColorsBase
TextTheme buildTextTheme(AppColorsBase colors) {
return TextTheme(
// 标题层级
titleLarge: TextStyle(
fontSize: 22, // 比正文大6
fontWeight: FontWeight.w700,
color: colors.textPrimary,
),
titleMedium: TextStyle(
fontSize: 20, // 比正文大4
fontWeight: FontWeight.w700,
color: colors.textPrimary,
),
titleSmall: TextStyle(
fontSize: 18, // 比正文大2
fontWeight: FontWeight.w600,
color: colors.textPrimary,
),
// 正文字体
bodyLarge: TextStyle(
fontSize: 18, // 稍大正文
color: colors.textPrimary,
),
bodyMedium: TextStyle(
fontSize: 16, // 正文标准
color: colors.textPrimary,
),
bodySmall: TextStyle(
fontSize: 14, // 辅助正文
color: colors.textSecondary,
),
// 标签/提示文字
labelLarge: TextStyle(
fontSize: 14, // 比正文小一点
fontWeight: FontWeight.w500,
color: colors.textSecondary,
),
labelMedium: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: colors.textSecondary,
),
labelSmall: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w400,
color: colors.textSecondary,
),
);
}

View File

@@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
import 'app_colors_base.dart';
@immutable
class AppThemeExtension extends ThemeExtension<AppThemeExtension> {
final Color success;
final Color warning;
final Color danger;
final Color textSecondary;
const AppThemeExtension({
required this.success,
required this.warning,
required this.danger,
required this.textSecondary,
});
// 工厂方法,根据 AppColorsBase 创建
factory AppThemeExtension.fromColors(AppColorsBase colors) {
return AppThemeExtension(
success: colors.success,
warning: colors.warning,
danger: colors.danger,
textSecondary: colors.textSecondary,
);
}
@override
ThemeExtension<AppThemeExtension> copyWith({
Color? success,
Color? warning,
Color? danger,
Color? textSecondary,
}) {
return AppThemeExtension(
success: success ?? this.success,
warning: warning ?? this.warning,
danger: danger ?? this.danger,
textSecondary: textSecondary ?? this.textSecondary,
);
}
@override
AppThemeExtension lerp(AppThemeExtension? other, double t) {
if (other == null) return this;
return AppThemeExtension(
success: Color.lerp(success, other.success, t)!,
warning: Color.lerp(warning, other.warning, t)!,
danger: Color.lerp(danger, other.danger, t)!,
textSecondary: Color.lerp(textSecondary, other.textSecondary, t)!,
);
}
}
extension AppThemeExt on BuildContext {
AppThemeExtension get themeEx => Theme.of(this).extension<AppThemeExtension>()!;
Color get success => themeEx.success;
Color get warning => themeEx.warning;
Color get danger => themeEx.danger;
Color get textSecondary => themeEx.textSecondary;
}

View File

@@ -12,3 +12,4 @@ extension CustomColors on ColorScheme {
Color get primaryEnd => const Color(0xff06b6d4);
}

View File

@@ -1,30 +1,43 @@
import 'package:flutter/material.dart';
///颜色
final scheme = ColorScheme.fromSeed(
primary: Color(0xff3784f1),
seedColor: Color(0xff3885f2),
brightness: Brightness.light,
//卡片色
surface: Colors.white,
surfaceContainerLow: Color(0xFFF4F8FB),
surfaceContainer: Color(0xFFE9ECF3),
surfaceContainerHigh: Color(0xFFDDE2EA),
//颜色
onSurfaceVariant: Color(0xFF828282),
import 'base/app_colors_base.dart';
import 'base/app_text_style.dart';
import 'base/app_theme_ext.dart';
shadow: Color.fromRGBO(0, 0, 0, 0.1),
);
class AppTheme {
static ThemeData createTheme(AppColorsBase themeBase) {
final textTheme = buildTextTheme(themeBase);
return ThemeData(
useMaterial3: true,
primaryColor: themeBase.primary,
scaffoldBackgroundColor: themeBase.surfaceContainerHigh,
colorScheme: ColorScheme.fromSeed(
seedColor: themeBase.primary,
secondary: themeBase.secondary,
primary: themeBase.primary,
brightness: Brightness.light,
///字体
final textTheme = TextTheme(
titleLarge: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, color: scheme.onSurface),
titleMedium: TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: scheme.onSurface),
titleSmall: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: scheme.onSurface),
bodyLarge: TextStyle(fontSize: 18),
bodyMedium: TextStyle(fontSize: 16),
bodySmall: TextStyle(fontSize: 14),
labelLarge: TextStyle(fontSize: 16, color: scheme.onSurfaceVariant),
labelMedium: TextStyle(fontSize: 14, color: scheme.onSurfaceVariant),
labelSmall: TextStyle(fontSize: 12, color: scheme.onSurfaceVariant),
);
onSurfaceVariant: themeBase.textSecondary,
//背景色
surfaceContainerHigh: themeBase.surfaceContainerHigh,
surfaceContainer: themeBase.surfaceContainer,
surfaceContainerLow: themeBase.surfaceContainerLow,
surfaceContainerLowest: themeBase.surfaceContainerLowest,
),
textTheme: textTheme,
extensions: [AppThemeExtension.fromColors(themeBase)],
appBarTheme: AppBarTheme(
backgroundColor: Colors.white,
titleTextStyle: textTheme.titleMedium,
scrolledUnderElevation: 0,
),
);
}
}
// background = Color(0xFFFFFBFE); // 页面背景
// surface = Color(0xFFFFFFFF); // 卡片背景
// surfaceVariant = Color(0xFFFFF7E0); // 卡片高亮 / 强调背景
// surfaceTint = Color(0xFFFFEF97); // 可用于叠加高亮
// primaryContainer = Color(0xFFFFF8B0); // 小块强调背景
// secondaryContainer= Color(0xFFE3E5C0); // 次要强调

View File

@@ -0,0 +1,43 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import '../base/app_colors_base.dart';
class LightTheme extends AppColorsBase {
@override
Color get primary => const Color(0xff22BB9B);
@override
Color get secondary => const Color(0xffE8F5E9);
@override
Color get textPrimary => const Color(0xFF212121);
@override
Color get textSecondary => const Color(0xffa8aca4);
@override
Color get success => const Color(0xff57be80);
@override
Color get warning => const Color(0xffff9800);
@override
Color get info => const Color(0xff909399);
@override
Color get danger => const Color(0xfff44545);
@override
Color get surfaceContainerLowest => Color(0xffE0E0E0);
@override
Color get surfaceContainerLow => Color(0xffF0F0F0);
@override
Color get surfaceContainer => Color(0xffF5F5F5);
@override
Color get surfaceContainerHigh => Color(0xffFFFFFF);
}

View File

@@ -0,0 +1,239 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart' as intl;
import 'app_localizations_en.dart';
import 'app_localizations_zh.dart';
// ignore_for_file: type=lint
/// Callers can lookup localized strings with an instance of AppLocalizations
/// returned by `AppLocalizations.of(context)`.
///
/// Applications need to include `AppLocalizations.delegate()` in their app's
/// `localizationDelegates` list, and the locales they support in the app's
/// `supportedLocales` list. For example:
///
/// ```dart
/// import 'l10n/app_localizations.dart';
///
/// return MaterialApp(
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
/// supportedLocales: AppLocalizations.supportedLocales,
/// home: MyApplicationHome(),
/// );
/// ```
///
/// ## Update pubspec.yaml
///
/// Please make sure to update your pubspec.yaml to include the following
/// packages:
///
/// ```yaml
/// dependencies:
/// # Internationalization support.
/// flutter_localizations:
/// sdk: flutter
/// intl: any # Use the pinned version from flutter_localizations
///
/// # Rest of dependencies
/// ```
///
/// ## iOS Applications
///
/// iOS applications define key application metadata, including supported
/// locales, in an Info.plist file that is built into the application bundle.
/// To configure the locales supported by your app, youll need to edit this
/// file.
///
/// First, open your projects ios/Runner.xcworkspace Xcode workspace file.
/// Then, in the Project Navigator, open the Info.plist file under the Runner
/// projects Runner folder.
///
/// Next, select the Information Property List item, select Add Item from the
/// Editor menu, then select Localizations from the pop-up menu.
///
/// Select and expand the newly-created Localizations item then, for each
/// locale your application supports, add a new item and select the locale
/// you wish to add from the pop-up menu in the Value field. This list should
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
/// property.
abstract class AppLocalizations {
AppLocalizations(String locale)
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
final String localeName;
static AppLocalizations? of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations);
}
static const LocalizationsDelegate<AppLocalizations> delegate = _AppLocalizationsDelegate();
/// A list of this localizations delegate along with the default localizations
/// delegates.
///
/// Returns a list of localizations delegates containing this delegate along with
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
/// and GlobalWidgetsLocalizations.delegate.
///
/// Additional delegates can be added by appending to this list in
/// MaterialApp. This list does not have to be used at all if a custom list
/// of delegates is preferred or required.
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
<LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
/// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[
Locale('en'),
Locale('zh'),
];
/// No description provided for @welcome_title.
///
/// In zh, this message translates to:
/// **'成分一目了然'**
String get welcome_title;
/// No description provided for @welcome_desc.
///
/// In zh, this message translates to:
/// **'自动识别食物成分,营养与风险一手掌握'**
String get welcome_desc;
/// No description provided for @welcome_button_text.
///
/// In zh, this message translates to:
/// **'开始使用'**
String get welcome_button_text;
/// No description provided for @login_title.
///
/// In zh, this message translates to:
/// **'欢迎来到FoodCura'**
String get login_title;
/// No description provided for @login_email_hint.
///
/// In zh, this message translates to:
/// **'请输入邮箱'**
String get login_email_hint;
/// No description provided for @login_password_hint.
///
/// In zh, this message translates to:
/// **'请输入密码'**
String get login_password_hint;
/// No description provided for @login_button.
///
/// In zh, this message translates to:
/// **'登录'**
String get login_button;
/// No description provided for @login_tip_start.
///
/// In zh, this message translates to:
/// **'注册或登录即表示您了解并同意'**
String get login_tip_start;
/// No description provided for @login_privacy.
///
/// In zh, this message translates to:
/// **'隐私协议'**
String get login_privacy;
/// No description provided for @login_terms.
///
/// In zh, this message translates to:
/// **'服务条款'**
String get login_terms;
/// No description provided for @login_and.
///
/// In zh, this message translates to:
/// **'和'**
String get login_and;
/// No description provided for @login_other_login.
///
/// In zh, this message translates to:
/// **'其他登录方式'**
String get login_other_login;
/// No description provided for @login_error_text.
///
/// In zh, this message translates to:
/// **'登录失败'**
String get login_error_text;
/// No description provided for @code_title.
///
/// In zh, this message translates to:
/// **'输入验证码'**
String get code_title;
/// No description provided for @code_tip.
///
/// In zh, this message translates to:
/// **'验证码已发送至'**
String get code_tip;
/// No description provided for @code_send_code.
///
/// In zh, this message translates to:
/// **'重新发送'**
String get code_send_code;
/// No description provided for @code_success.
///
/// In zh, this message translates to:
/// **'发送成功'**
String get code_success;
/// No description provided for @code_error.
///
/// In zh, this message translates to:
/// **'验证失败'**
String get code_error;
}
class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
const _AppLocalizationsDelegate();
@override
Future<AppLocalizations> load(Locale locale) {
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
}
@override
bool isSupported(Locale locale) => <String>['en', 'zh'].contains(locale.languageCode);
@override
bool shouldReload(_AppLocalizationsDelegate old) => false;
}
AppLocalizations lookupAppLocalizations(Locale locale) {
// Lookup logic when only language code is specified.
switch (locale.languageCode) {
case 'en':
return AppLocalizationsEn();
case 'zh':
return AppLocalizationsZh();
}
throw FlutterError(
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
'an issue with the localizations generation tool. Please file an issue '
'on GitHub with a reproducible sample app and the gen-l10n configuration '
'that was used.',
);
}

View File

@@ -0,0 +1,65 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for English (`en`).
class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale);
@override
String get welcome_title => 'Clear Ingredients at a Glance';
@override
String get welcome_desc =>
'Automatically identify food ingredients and get instant insights on nutrition and potential risks';
@override
String get welcome_button_text => 'Get Started';
@override
String get login_title => '欢迎来到FoodCura';
@override
String get login_email_hint => 'Please enter your email';
@override
String get login_password_hint => 'Please enter your password';
@override
String get login_button => 'Continue';
@override
String get login_tip_start => 'I agree to the';
@override
String get login_privacy => 'Privacy Policy';
@override
String get login_terms => 'Terms';
@override
String get login_and => '&';
@override
String get login_other_login => 'Or';
@override
String get login_error_text => 'Login Failed';
@override
String get code_title => 'Enter Verification Code';
@override
String get code_tip => 'Verification code has been sent to';
@override
String get code_send_code => 'Resend';
@override
String get code_success => 'Sent';
@override
String get code_error => 'Invalid Code';
}

View File

@@ -0,0 +1,64 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Chinese (`zh`).
class AppLocalizationsZh extends AppLocalizations {
AppLocalizationsZh([String locale = 'zh']) : super(locale);
@override
String get welcome_title => '成分一目了然';
@override
String get welcome_desc => '自动识别食物成分,营养与风险一手掌握';
@override
String get welcome_button_text => '开始使用';
@override
String get login_title => '欢迎来到FoodCura';
@override
String get login_email_hint => '请输入邮箱';
@override
String get login_password_hint => '请输入密码';
@override
String get login_button => '登录';
@override
String get login_tip_start => '注册或登录即表示您了解并同意';
@override
String get login_privacy => '隐私协议';
@override
String get login_terms => '服务条款';
@override
String get login_and => '';
@override
String get login_other_login => '其他登录方式';
@override
String get login_error_text => '登录失败';
@override
String get code_title => '输入验证码';
@override
String get code_tip => '验证码已发送至';
@override
String get code_send_code => '重新发送';
@override
String get code_success => '发送成功';
@override
String get code_error => '验证失败';
}

19
lib/l10n/arb/app_en.arb Normal file
View File

@@ -0,0 +1,19 @@
{
"welcome_title": "Clear Ingredients at a Glance",
"welcome_desc": "Automatically identify food ingredients and get instant insights on nutrition and potential risks",
"welcome_button_text": "Get Started",
"login_email_hint": "Please enter your email",
"login_password_hint": "Please enter your password",
"login_button": "Continue",
"login_tip_start": "I agree to the",
"login_privacy": "Privacy Policy",
"login_terms": "Terms",
"login_and": "&",
"login_other_login": "Or",
"login_error_text": "Login Failed",
"code_title": "Enter Verification Code",
"code_tip": "Verification code has been sent to",
"code_send_code": "Resend",
"code_success": "Sent",
"code_error": "Invalid Code"
}

20
lib/l10n/arb/app_zh.arb Normal file
View File

@@ -0,0 +1,20 @@
{
"welcome_title": "成分一目了然",
"welcome_desc": "自动识别食物成分,营养与风险一手掌握",
"welcome_button_text": "开始使用",
"login_title": "欢迎来到FoodCura",
"login_email_hint": "请输入邮箱",
"login_password_hint": "请输入密码",
"login_button": "登录",
"login_tip_start": "注册或登录即表示您了解并同意",
"login_privacy": "隐私协议",
"login_terms": "服务条款",
"login_and": "和",
"login_other_login": "其他登录方式",
"login_error_text": "登录失败",
"code_title": "输入验证码",
"code_tip": "验证码已发送至",
"code_send_code": "重新发送",
"code_success": "发送成功",
"code_error": "验证失败"
}

56
lib/l10n/arb/i18n.yaml Normal file
View File

@@ -0,0 +1,56 @@
welcome:
title:
zh: 成分一目了然
en: Clear Ingredients at a Glance
desc:
zh: 自动识别食物成分,营养与风险一手掌握
en: Automatically identify food ingredients and get instant insights on nutrition and potential risks
button_text:
zh: 开始使用
en: Get Started
login:
title:
zh: 欢迎来到FoodCura
email_hint:
zh: 请输入邮箱
en: Please enter your email
password_hint:
zh: 请输入密码
en: Please enter your password
button:
zh: 登录
en: Continue
tip_start:
zh: 注册或登录即表示您了解并同意
en: I agree to the
privacy:
zh: 隐私协议
en: Privacy Policy
terms:
zh: 服务条款
en: Terms
and:
zh:
en: "&"
other_login:
zh: 其他登录方式
en: Or
error_text:
zh: 登录失败
en: Login Failed
code:
title:
zh: 输入验证码
en: Enter Verification Code
tip:
zh: 验证码已发送至
en: Verification code has been sent to
send_code:
zh: 重新发送
en: Resend
success:
zh: 发送成功
en: Sent
error:
zh: 验证失败
en: Invalid Code

12
lib/l10n/l10n.dart Normal file
View File

@@ -0,0 +1,12 @@
import 'package:flutter/cupertino.dart';
import 'package:food_health/l10n/app_localizations.dart';
class L10n {
static AppLocalizations? _instance;
static void init(BuildContext context) {
_instance = AppLocalizations.of(context);
}
static AppLocalizations get of => _instance!;
}

View File

@@ -4,10 +4,10 @@ import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:remixicon/remixicon.dart';
import '../page/home/home_page.dart';
import '../page/profile/my/my_page.dart';
import '../page/record/list/record_list_page.dart';
import '../providers/user_store.dart';
import '../pages/home/home_page.dart';
import '../pages/profile/my/my_page.dart';
import '../pages/record/list/record_list_page.dart';
import '../stores/user_store.dart';
import '../router/config/route_paths.dart';
import 'tabbar.dart';

View File

@@ -1,4 +1,9 @@
import 'package:food_health/providers/user_store.dart';
import 'dart:ui';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:food_health/stores/app_store.dart';
import 'package:food_health/stores/setting_store.dart';
import 'package:food_health/stores/user_store.dart';
import 'package:food_health/router/routes.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
@@ -6,7 +11,9 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:provider/provider.dart';
import 'config/theme/theme.dart';
import 'providers/app_store.dart';
import 'config/theme/themes/light_theme.dart';
import 'l10n/app_localizations.dart';
import 'l10n/l10n.dart';
void main() {
runApp(
@@ -14,6 +21,7 @@ void main() {
providers: [
ChangeNotifierProvider(create: (context) => AppStore()),
ChangeNotifierProvider(create: (context) => UserStore()),
ChangeNotifierProvider(create: (_) => SettingStore()),
],
child: MyApp(),
),
@@ -25,32 +33,28 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
//语言化
L10n.init(context);
final settingStore = context.watch<SettingStore>();
return ScreenUtilInit(
designSize: const Size(375, 694),
useInheritedMediaQuery: true,
child: MaterialApp.router(
debugShowCheckedModeBanner: false,
routerConfig: goRouter,
localizationsDelegates: [],
themeMode: ThemeMode.light,
theme: ThemeData(
useMaterial3: true,
colorScheme: scheme,
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: scheme.primary,
foregroundColor: scheme.onPrimary,
),
),
textTheme: textTheme,
scaffoldBackgroundColor: Color(0xffFAFAFE),
appBarTheme: AppBarTheme(
backgroundColor: scheme.surface,
scrolledUnderElevation: 0,
titleTextStyle: textTheme.titleMedium,
),
),
builder: EasyLoading.init(),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: [
Locale('en'),
Locale('zh', 'CN'),
],
theme: AppTheme.createTheme(LightTheme()),
locale: settingStore.locale,
builder: (context, child) {
// ⚡️ 在这里就能拿到 AppLocalizations
L10n.init(context);
return EasyLoading.init()(context, child);
},
),
);
}

View File

@@ -1,115 +0,0 @@
import 'package:food_health/api/endpoints/user_api.dart';
import 'package:food_health/api/network/safe.dart';
import 'package:food_health/page/system/login/widget/widget.dart';
import 'package:food_health/router/config/route_paths.dart';
import 'package:food_health/widgets/ui_kit/button/custom_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../../../providers/app_store.dart';
class LoginCodePage extends StatefulWidget {
final String email;
final String password;
const LoginCodePage({super.key, required this.email, required this.password});
@override
State<LoginCodePage> createState() => _LoginCodePageState();
}
class _LoginCodePageState extends State<LoginCodePage> {
final _codeController = TextEditingController();
var _subLoading = false;
@override
void initState() {
super.initState();
_handSendCode();
}
///发送验证码
void _handSendCode() {
sendEmailCodeApi(widget.email);
EasyLoading.showSuccess("Send success");
}
///提交
void _handSubmit() async {
if (_codeController.text.isNotEmpty) {
setState(() {
_subLoading = true;
});
var res = await safeRequest(
registerApi(
widget.email,
widget.password,
_codeController.text,
),
onError: (error) {
setState(() {
_subLoading = false;
});
},
);
var appStore = context.read<AppStore>();
await appStore.setInfo(res);
context.go(RoutePaths.layout);
setState(() {
_subLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Container(
width: double.infinity,
padding: EdgeInsets.only(left: 20, right: 20, top: 40),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"Check your inbox",
style: Theme.of(context).textTheme.titleLarge,
),
Container(
margin: EdgeInsets.only(top: 20, bottom: 40),
child: Text(
"Enter the verification code we just sent to ${widget.email}.",
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.labelMedium,
),
),
InputBox(hintText: "Code", controller: _codeController),
Container(
margin: EdgeInsets.only(top: 20),
child: CustomButton(
loading: _subLoading,
onPressed: _handSubmit,
child: Text("Continue"),
),
),
Container(
margin: EdgeInsets.only(top: 20),
child: TextButton(
onPressed: () {
_handSendCode();
},
child: Text(
"Resend code",
style: Theme.of(context).textTheme.labelSmall,
),
),
),
],
),
),
),
);
}
}

View File

@@ -1,290 +0,0 @@
import 'package:food_health/api/endpoints/user_api.dart';
import 'package:food_health/data/models/other_login_type.dart';
import 'package:food_health/router/config/route_paths.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:go_router/go_router.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:provider/provider.dart';
import 'package:remixicon/remixicon.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
import '../../../providers/app_store.dart';
import '../../../utils/common.dart';
import '../../../widgets/common/app_backend.dart';
import '../../../widgets/ui_kit/button/custom_button.dart';
import 'widget/agreement_box.dart';
import 'widget/widget.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
var _subLoading = false;
///协议
bool _agree = false;
///谷歌登陆
final GoogleSignIn _googleSignIn = GoogleSignIn.instance;
///邮箱输入框
final TextEditingController _emailController = TextEditingController(text: "");
final TextEditingController _passwordController = TextEditingController(text: "");
//显示密码
var _hidePassword = true;
@override
void initState() {
super.initState();
_ensureNetworkPermission();
_initGoogleSign();
}
/// 预触发 iOS 网络权限弹窗
Future<bool> _ensureNetworkPermission() async {
try {
await Dio().get(
'https://captive.apple.com/hotspot-detect.html',
options: Options(
sendTimeout: const Duration(seconds: 3),
receiveTimeout: const Duration(seconds: 3),
headers: {'Cache-Control': 'no-cache'},
),
);
return true;
} catch (_) {
return false;
}
}
void _initGoogleSign() {
if (isAndroid()) {
_googleSignIn.initialize(
clientId: null,
serverClientId: "512878764950-0bsl98c4q4p695mlmfn35qhmr2ld5n0o.apps.googleusercontent.com",
);
} else {
_googleSignIn.initialize(
clientId: "512878764950-1ke7slf0c6dlmchnuk0fqh3fe954gcf2.apps.googleusercontent.com",
serverClientId: "512878764950-0bsl98c4q4p695mlmfn35qhmr2ld5n0o.apps.googleusercontent.com",
);
}
_googleSignIn.authenticationEvents
.listen((_) {
print("登陆成功");
})
.onError((error) {
print('登录错误: $error');
});
}
///谷歌登录
void _handleGoogleSignIn() async {
if (!_agree) {
EasyLoading.showToast('Please read and agree to the terms first.');
return;
}
try {
// 如果用户未登录,则启动标准的 Google 登录
if (_googleSignIn.supportsAuthenticate()) {
// 使用 authenticate() 进行认证
GoogleSignInAccount? user = await _googleSignIn.authenticate();
var auth = user.authentication;
// var res = await Dio().get("https://oauth2.googleapis.com/tokeninfo?id_token=${auth.idToken}");
//登陆
EasyLoading.show(status: "Logging in...");
var res = await thirdLoginApi(auth.idToken!, OtherLoginType.google);
EasyLoading.dismiss();
_onLogin(res);
}
// } catch (e) {
// if (e is GoogleSignInException) {
// if (e.code == GoogleSignInExceptionCode.canceled) {
// // 用户取消登录
// print("User canceled login.");
// } else {
// // 其他错误
// print("Google Sign-In error: $e");
// }
// } else {
// print("Unknown error: $e");
// }
// }
} catch (e) {
EasyLoading.showError("Login failed");
print("登录错误: $e");
}
}
///apple登录
void _handAppleSignIn() async {
if (!_agree) {
EasyLoading.showToast('Please read and agree to the terms first.');
return;
}
try {
final credential = await SignInWithApple.getAppleIDCredential(
scopes: [
AppleIDAuthorizationScopes.email,
AppleIDAuthorizationScopes.fullName,
],
);
EasyLoading.show(status: "Logging in...");
var res = await thirdLoginApi(credential.identityToken!, OtherLoginType.apple);
EasyLoading.dismiss();
_onLogin(res);
print('Apple Credential: ${credential.identityToken}');
print('Apple Email: ${credential.email}');
} catch (e) {
print('Error during Apple sign-in: $e');
}
}
void _handSubmit() async {
if (!_agree) {
EasyLoading.showToast('Please read and agree to the terms first.');
return;
}
if (_emailController.text.isEmpty) {
//请输入邮箱
EasyLoading.showError("Please enter your email");
return;
} else if (_passwordController.text.isEmpty) {
EasyLoading.showError("Please enter your Password");
return;
}
try {
setState(() {
_subLoading = true;
});
var isRegister = await checkRegisterApi(_emailController.text);
if (!isRegister) {
context.push(
RoutePaths.loginCode,
extra: {
"email": _emailController.text,
"password": _passwordController.text,
},
);
} else {
var res = await loginApi(_emailController.text, _passwordController.text);
_onLogin(res);
}
setState(() {
_subLoading = false;
});
} catch (e) {
setState(() {
_subLoading = false;
});
}
}
///登陆的操作
void _onLogin(dynamic res) {
var appStore = context.read<AppStore>();
appStore.setInfo(res);
context.go(RoutePaths.layout);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: AppBackend(
child: Scaffold(
resizeToAvoidBottomInset: false,
body: SafeArea(
child: Container(
width: double.infinity,
padding: EdgeInsets.only(
top: 0.07.sh,
left: 20,
right: 20,
),
child: ListView(
children: [
LogoBox(),
PageHeader(),
InputBox(
hintText: "Email",
controller: _emailController,
),
SizedBox(height: 15),
InputBox(
obscureText: _hidePassword,
hintText: "Password",
controller: _passwordController,
suffix: InkWell(
onTap: () {
setState(() {
_hidePassword = !_hidePassword;
});
},
child: Icon(
_hidePassword ? RemixIcons.eye_off_fill : RemixIcons.eye_fill,
size: 20,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
Container(
margin: EdgeInsets.only(top: 20),
height: 45,
child: CustomButton(
loading: _subLoading,
round: false,
onPressed: _handSubmit,
child: Text("Continue"),
),
),
LoginDivider(),
OtherButton(
title: "Continue with Google",
icon: "assets/image/google.png",
onTap: () {
_handleGoogleSignIn();
},
),
SizedBox(height: 15),
OtherButton(
title: "Continue with Apple",
icon: "assets/image/apple.png",
onTap: () {
_handAppleSignIn();
},
),
Container(
width: double.infinity,
margin: EdgeInsets.only(top: 40),
alignment: Alignment.center,
child: AgreementBox(
checked: _agree,
onChanged: (value) {
setState(() {
_agree = value;
});
},
),
),
],
),
),
),
),
),
);
}
}

View File

@@ -1,72 +0,0 @@
import 'package:food_health/router/config/route_paths.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
///勾中协议
class AgreementBox extends StatelessWidget {
final bool checked;
final Function(bool) onChanged;
const AgreementBox({
super.key,
this.checked = false,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 25,
child: Transform.scale(
scale: 0.8,
child: Checkbox(
value: checked,
shape: CircleBorder(),
onChanged: (value) {
onChanged(value!);
},
),
),
),
GestureDetector(
onTap: () {
onChanged(!checked);
},
child: RichText(
text: TextSpan(
style: Theme.of(context).textTheme.labelSmall,
children: [
TextSpan(
text: "I agree to the ",
),
TextSpan(
text: "Terms",
style: TextStyle(color: Theme.of(context).primaryColor),
recognizer: TapGestureRecognizer()
..onTap = () => context.push(
RoutePaths.agreement,
extra: {"title": "Terms of Service", "url": "https://support.curain.ai/privacy/foodcura/terms_service.html"},
),
),
TextSpan(text: " & "),
TextSpan(
text: "Privacy Policy",
style: TextStyle(color: Theme.of(context).primaryColor),
recognizer: TapGestureRecognizer()
..onTap = () => context.push(
RoutePaths.agreement,
extra: {"title": "Privacy", "url": "https://support.curain.ai/privacy/foodcura/privacy_policy.html"},
),
),
],
),
),
),
],
);
}
}

View File

@@ -1,166 +0,0 @@
import 'package:flutter/material.dart';
///登陆Box
class LogoBox extends StatelessWidget {
const LogoBox({super.key});
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(bottom: 40),
child: Column(
children: [
Image.asset(
"assets/image/logo.png",
width: 43,
),
Text(
"FoodCura",
style: Theme.of(context).textTheme.titleSmall,
),
],
),
);
}
}
///头部文案
class PageHeader extends StatelessWidget {
const PageHeader({super.key});
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(bottom: 30),
child: Column(
children: [
Text(
"Create an account",
style: TextStyle(fontWeight: FontWeight.w700),
),
Text(
"Use your email to get started",
style: Theme.of(context).textTheme.bodySmall,
),
],
),
);
}
}
///输入框
class InputBox extends StatelessWidget {
final bool obscureText;
final String hintText;
final TextEditingController controller;
final Widget? suffix;
const InputBox({
super.key,
this.obscureText = false,
required this.hintText,
required this.controller,
this.suffix,
});
@override
Widget build(BuildContext context) {
//边框
var inputBorder = OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.surfaceContainer,
),
);
return TextField(
controller: controller,
maxLength: 100,
obscureText: obscureText,
style: Theme.of(context).textTheme.bodyMedium,
decoration: InputDecoration(
hintText: hintText,
hintStyle: Theme.of(context).textTheme.labelMedium,
counterText: '',
border: inputBorder,
enabledBorder: inputBorder,
suffix: suffix,
suffixIconConstraints: BoxConstraints(
minWidth: 0,
minHeight: 0,
),
),
);
}
}
///分割线
class LoginDivider extends StatelessWidget {
const LoginDivider({super.key});
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(top: 20, bottom: 20),
child: Row(
spacing: 8,
children: [
Expanded(
child: Container(
height: 1,
color: Theme.of(context).colorScheme.surfaceContainer,
),
),
Text(
"or",
style: Theme.of(context).textTheme.labelMedium,
),
Expanded(
child: Container(
height: 1,
color: Theme.of(context).colorScheme.surfaceContainer,
),
),
],
),
);
}
}
///其他登陆按钮
class OtherButton extends StatelessWidget {
final Function() onTap;
final String title;
final String icon;
const OtherButton({
super.key,
required this.onTap,
required this.title,
required this.icon,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Container(
padding: EdgeInsets.all(15),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Theme.of(context).colorScheme.surfaceContainer,
),
child: Row(
spacing: 10,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(icon, width: 20),
Text(
title,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
);
}
}

View File

@@ -1,7 +1,7 @@
import 'package:food_health/page/home/widget/home_header.dart';
import 'package:food_health/pages/home/widget/home_header.dart';
import 'package:flutter/material.dart';
import '../../widgets/common/app_backend.dart';
import '../../widgets/common/app_header.dart';
import '../../widgets/shared/app_backend.dart';
import '../../widgets/shared/app_header.dart';
import 'widget/upload_panel.dart';
class HomePage extends StatefulWidget {

View File

@@ -6,7 +6,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:food_health/api/endpoints/food_api.dart';
import 'package:food_health/config/theme/custom_colors.dart';
import 'package:food_health/config/theme/color_ext.dart';
import 'package:food_health/router/config/route_paths.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';

View File

@@ -2,10 +2,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:food_health/api/dto/profile_options_dto.dart';
import 'package:food_health/api/endpoints/profile_api.dart';
import 'package:food_health/config/theme/custom_colors.dart';
import 'package:food_health/page/profile/edit/widget/food_allergies.dart';
import 'package:food_health/providers/user_store.dart';
import 'package:food_health/widgets/common/app_backend.dart';
import 'package:food_health/config/theme/color_ext.dart';
import 'package:food_health/pages/profile/edit/widget/food_allergies.dart';
import 'package:food_health/stores/user_store.dart';
import 'package:food_health/widgets/shared/app_backend.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:remixicon/remixicon.dart';

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:food_health/api/dto/profile_options_dto.dart';
import 'package:food_health/config/theme/custom_colors.dart';
import 'package:food_health/config/theme/color_ext.dart';
import 'package:remixicon/remixicon.dart';
import '../data/state.dart';

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:food_health/api/dto/profile_options_dto.dart';
import 'package:food_health/config/theme/custom_colors.dart';
import 'package:food_health/config/theme/color_ext.dart';
import 'package:remixicon/remixicon.dart';
import '../data/state.dart';

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:food_health/config/theme/custom_colors.dart';
import 'package:food_health/providers/user_store.dart';
import 'package:food_health/config/theme/color_ext.dart';
import 'package:food_health/stores/user_store.dart';
import 'package:provider/provider.dart';
import 'package:remixicon/remixicon.dart';

View File

@@ -3,9 +3,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:food_health/api/dto/user_profile_dto.dart';
import 'package:food_health/api/endpoints/user_api.dart';
import 'package:food_health/config/theme/custom_colors.dart';
import 'package:food_health/providers/app_store.dart';
import 'package:food_health/providers/user_store.dart';
import 'package:food_health/config/theme/color_ext.dart';
import 'package:food_health/stores/app_store.dart';
import 'package:food_health/stores/user_store.dart';
import 'package:food_health/router/config/route_paths.dart';
import 'package:food_health/utils/common.dart';
import 'package:go_router/go_router.dart';

View File

@@ -45,29 +45,39 @@ class DetailedAnalysis extends StatelessWidget {
physics: NeverScrollableScrollPhysics(),
data: detail.explanation ?? "",
),
Row(
spacing: 10,
children: [
Icon(RemixIcons.menu_2_line),
Text(
"Detected Ingredients",
style: Theme.of(context).textTheme.titleMedium,
),
],
),
Wrap(
spacing: 10,
runSpacing: 10,
children: detail.ingredientsList!.map((item) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(8),
Visibility(
visible: detail.ingredientsList!.isNotEmpty,
child: Column(
children: [
Row(
spacing: 10,
children: [
Icon(RemixIcons.menu_2_line),
Text(
"Detected Ingredients",
style: Theme.of(context).textTheme.titleMedium,
),
],
),
child: Text(item, style: Theme.of(context).textTheme.labelMedium),
);
}).toList(),
Container(
margin: EdgeInsets.only(top: 10),
child: Wrap(
spacing: 10,
runSpacing: 10,
children: detail.ingredientsList!.map((item) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(8),
),
child: Text(item, style: Theme.of(context).textTheme.labelMedium),
);
}).toList(),
),
),
],
),
),
],
),

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:food_health/config/theme/custom_colors.dart';
import 'package:food_health/config/theme/color_ext.dart';
import 'package:markdown_widget/widget/markdown.dart';
import 'package:remixicon/remixicon.dart';

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:food_health/api/dto/food_scan_dto.dart';
import 'package:food_health/config/theme/custom_colors.dart';
import 'package:food_health/config/theme/color_ext.dart';
import 'package:remixicon/remixicon.dart';
class ResultChip extends StatelessWidget {

View File

@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:food_health/api/dto/food_scan_dto.dart';
import 'package:food_health/config/theme/custom_colors.dart';
import 'package:food_health/config/theme/color_ext.dart';
import 'package:food_health/router/config/route_paths.dart';
import 'package:food_health/widgets/common/async_image.dart';
import 'package:food_health/widgets/shared/async_image.dart';
import 'package:food_health/widgets/ui_kit/empty/index.dart';
import 'package:go_router/go_router.dart';
import 'package:remixicon/remixicon.dart';
@@ -35,6 +35,7 @@ class RecordListCard extends StatelessWidget {
),
),
child: ListView.separated(
cacheExtent: 2000,
padding: EdgeInsets.symmetric(horizontal: 15, vertical: 15),
itemBuilder: (context, index) {
var item = records[index];

View File

@@ -0,0 +1,223 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:food_health/api/endpoints/user_api.dart';
import 'package:food_health/api/network/safe.dart';
import 'package:food_health/l10n/l10n.dart';
import 'package:food_health/router/config/route_paths.dart';
import 'package:food_health/stores/app_store.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
class LoginCodePage extends StatefulWidget {
final String email;
final String password;
const LoginCodePage({super.key, required this.email, required this.password});
@override
State<LoginCodePage> createState() => _LoginCodePageState();
}
class _LoginCodePageState extends State<LoginCodePage> {
final List<FocusNode> _focusNodes = List.generate(4, (_) => FocusNode());
final List<TextEditingController> _controllers = List.generate(
4,
(_) => TextEditingController(),
);
//倒计时
int _count = 60;
Timer? _timer;
@override
void initState() {
super.initState();
_handSendCode();
}
@override
void dispose() {
_timer?.cancel();
_handClear();
super.dispose();
}
///小输入框改变时
void _onChanged(String value, int index) async {
//一键复制
if (value.length == 4) {
_handlePaste(value);
}
//提交
if (value.isNotEmpty && index == 3) {
_handSubmit();
return;
}
// 自动跳到下一格
if (value.length == 1 && index < 3) {
_focusNodes[index + 1].requestFocus();
}
}
void _handlePaste(String pastedText) {
// 只取前4位数字
final digits = pastedText.replaceAll(RegExp(r'[^0-9]'), '');
for (int i = 0; i < 4; i++) {
_controllers[i].text = i < digits.length ? digits[i] : '';
}
if (digits.length >= 4) {
_focusNodes[3].requestFocus();
_handSubmit();
} else if (digits.isNotEmpty) {
_focusNodes[digits.length].requestFocus();
}
}
///删除键
void _onDelete(KeyEvent event, int index) {
if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.backspace) {
final currentController = _controllers[index];
if (currentController.text.isEmpty && index > 0) {
_focusNodes[index - 1].requestFocus();
_controllers[index - 1].clear();
}
}
}
///发送验证码
void _handSendCode() {
if (_count != 60) {
return;
}
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
setState(() {
_count--;
});
if (_count == 0) {
setState(() {
_count = 60;
});
timer.cancel();
}
});
sendEmailCodeApi(widget.email);
EasyLoading.showToast(L10n.of.code_success);
}
///提交
void _handSubmit() async {
String code = _controllers.map((controller) => controller.text).join();
if (code.length == 4) {
EasyLoading.show();
var res = await safeRequest(
registerApi(
widget.email,
widget.password,
code,
),
onError: (error) {
_handClear();
EasyLoading.showToast(L10n.of.code_error);
},
);
var appStore = context.read<AppStore>();
await appStore.setInfo(res);
context.go(RoutePaths.layout);
}
}
///清空
void _handClear() {
for (var controller in _controllers) {
controller.clear();
}
_focusNodes.first.requestFocus();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ListView(
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.all(20),
children: [
Container(
margin: EdgeInsets.only(bottom: 20),
child: Text(L10n.of.code_title, style: Theme.of(context).textTheme.titleLarge),
),
Container(
margin: EdgeInsets.only(bottom: 60),
child: Text(
"${L10n.of.code_tip} ${widget.email}",
style: Theme.of(context).textTheme.labelLarge,
),
),
Row(
spacing: 20,
children: List.generate(4, (index) {
return Expanded(
child: AspectRatio(
aspectRatio: 1,
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(10),
),
child: KeyboardListener(
focusNode: FocusNode(),
onKeyEvent: (event) {
_onDelete(event, index);
},
child: TextField(
controller: _controllers[index],
focusNode: _focusNodes[index],
textAlign: TextAlign.center,
keyboardType: TextInputType.number,
maxLength: 4,
style: TextStyle(fontSize: 32),
decoration: InputDecoration(
counterText: "",
border: InputBorder.none,
isCollapsed: true,
),
onChanged: (value) {
_onChanged(value, index);
},
),
),
),
),
);
}),
),
Container(
margin: EdgeInsets.only(top: 30),
child: Row(
children: [
GestureDetector(
onTap: _handSendCode,
child: Visibility(
visible: _count != 60,
replacement: Text(
L10n.of.code_send_code,
style: Theme.of(context).textTheme.bodySmall,
),
child: Text(
"${L10n.of.code_send_code}${_count}s",
style: Theme.of(context).textTheme.labelLarge,
),
),
),
],
),
),
],
),
);
}
}

View File

@@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import 'package:food_health/l10n/l10n.dart';
import 'package:food_health/router/config/route_paths.dart';
import 'package:food_health/widgets/ui_kit/button/app_button.dart';
import 'package:go_router/go_router.dart';
class IntroPage extends StatefulWidget {
const IntroPage({super.key});
@override
State<IntroPage> createState() => _IntroPageState();
}
class _IntroPageState extends State<IntroPage> {
void _onTap() {
context.go(RoutePaths.login);
}
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
backgroundColor: Colors.white,
body: SafeArea(
child: SizedBox(
width: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
padding: EdgeInsets.all(50),
child: Image.asset("assets/image/bg/intro_bg.png"),
),
Expanded(
child: Padding(
padding: EdgeInsets.all(20),
child: Column(
children: [
Container(
margin: EdgeInsets.only(bottom: 20),
child: Text(
L10n.of.welcome_title,
style: Theme.of(context).textTheme.titleMedium,
),
),
Text(
L10n.of.welcome_desc,
style: Theme.of(context).textTheme.labelMedium,
),
Container(
height: 50,
margin: EdgeInsets.only(top: 20),
child: AppButton(
onPressed: _onTap,
child: Text(L10n.of.welcome_button_text),
),
),
],
),
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,289 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:food_health/api/endpoints/user_api.dart';
import 'package:food_health/data/models/other_login_type.dart';
import 'package:food_health/l10n/l10n.dart';
import 'package:food_health/pages/system/login/widgets/login_input.dart';
import 'package:food_health/router/config/route_paths.dart';
import 'package:food_health/stores/app_store.dart';
import 'package:food_health/widgets/ui_kit/button/app_button.dart';
import 'package:go_router/go_router.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:provider/provider.dart';
import 'package:remixicon/remixicon.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
import '../../../utils/common.dart';
import 'widgets/login_agree.dart';
import 'widgets/login_other.dart';
import 'package:logger/logger.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
Logger logger = Logger();
var _subLoading = false;
///邮箱输入框
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
//显示密码
bool _hidePassword = true;
///谷歌登陆
final GoogleSignIn _googleSignIn = GoogleSignIn.instance;
@override
void initState() {
super.initState();
_emailController.addListener(() {
setState(() {});
});
_passwordController.addListener(() {
setState(() {});
});
_ensureNetworkPermission();
_initGoogleSign();
}
/// 预触发 iOS 网络权限弹窗
Future<bool> _ensureNetworkPermission() async {
try {
await Dio().get(
'https://captive.apple.com/hotspot-detect.html',
options: Options(
sendTimeout: const Duration(seconds: 3),
receiveTimeout: const Duration(seconds: 3),
headers: {'Cache-Control': 'no-cache'},
),
);
return true;
} catch (_) {
return false;
}
}
void _initGoogleSign() {
if (isAndroid()) {
_googleSignIn.initialize(
clientId: null,
serverClientId:
"512878764950-0bsl98c4q4p695mlmfn35qhmr2ld5n0o.apps.googleusercontent.com",
);
} else {
_googleSignIn.initialize(
clientId: "512878764950-1ke7slf0c6dlmchnuk0fqh3fe954gcf2.apps.googleusercontent.com",
serverClientId:
"512878764950-0bsl98c4q4p695mlmfn35qhmr2ld5n0o.apps.googleusercontent.com",
);
}
_googleSignIn.authenticationEvents
.listen((_) {
logger.d("登陆成功");
})
.onError((error) {
logger.e("登陆错误: $error");
});
}
///谷歌登录
void _handGoogleSignIn() async {
try {
// 如果用户未登录,则启动标准的 Google 登录
if (_googleSignIn.supportsAuthenticate()) {
// 使用 authenticate() 进行认证
GoogleSignInAccount? user = await _googleSignIn.authenticate();
var auth = user.authentication;
//登陆
EasyLoading.show();
var res = await thirdLoginApi(auth.idToken!, OtherLoginType.google);
EasyLoading.dismiss();
_onLogin(res);
}
} catch (e) {
EasyLoading.showError(L10n.of.login_error_text);
logger.e("登录错误: $e");
}
}
///apple登录
void _handAppleSignIn() async {
try {
final credential = await SignInWithApple.getAppleIDCredential(
scopes: [
AppleIDAuthorizationScopes.email,
AppleIDAuthorizationScopes.fullName,
],
);
EasyLoading.show();
var res = await thirdLoginApi(credential.identityToken!, OtherLoginType.apple);
EasyLoading.dismiss();
_onLogin(res);
logger.d('Apple Credential: ${credential.identityToken}');
logger.d('Apple Email: ${credential.email}');
} catch (e) {
logger.e("登录错误: $e");
}
}
void _handSubmit() async {
if (_emailController.text.isEmpty) {
//请输入邮箱
EasyLoading.showToast(L10n.of.login_email_hint);
return;
} else if (_passwordController.text.isEmpty) {
EasyLoading.showToast(L10n.of.login_password_hint);
return;
}
try {
setState(() {
_subLoading = true;
});
var isRegister = await checkRegisterApi(_emailController.text);
if (!isRegister && mounted) {
context.push(
RoutePaths.loginCode,
extra: {
"email": _emailController.text,
"password": _passwordController.text,
},
);
} else {
var res = await loginApi(_emailController.text, _passwordController.text);
_onLogin(res);
}
setState(() {
_subLoading = false;
});
} catch (e) {
setState(() {
_subLoading = false;
});
}
}
///登陆的操作
void _onLogin(dynamic res) {
var appStore = context.read<AppStore>();
appStore.setInfo(res);
context.go(RoutePaths.layout);
}
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
body: SafeArea(
child: Container(
padding: EdgeInsets.only(
left: 20,
right: 20,
top: 0.08.sh,
bottom: 40,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: EdgeInsets.only(bottom: 40),
child: Text(
L10n.of.login_title,
style: Theme.of(context).textTheme.titleLarge,
),
),
LoginInput(
hintText: L10n.of.login_email_hint,
controller: _emailController,
suffix: Visibility(
visible: _emailController.text.isNotEmpty,
child: InkWell(
onTap: () {
_emailController.clear();
},
child: Icon(
RemixIcons.close_circle_fill,
size: 20,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
),
SizedBox(height: 20),
LoginInput(
hintText: L10n.of.login_password_hint,
controller: _passwordController,
obscureText: _hidePassword,
suffix: InkWell(
onTap: () {
setState(() {
_hidePassword = !_hidePassword;
});
},
child: Icon(
_hidePassword ? RemixIcons.eye_off_fill : RemixIcons.eye_fill,
size: 20,
),
),
),
Container(
margin: EdgeInsets.only(top: 40),
height: 50,
child: AppButton(
disabled: _emailController.text.isEmpty || _passwordController.text.isEmpty,
loading: _subLoading,
onPressed: _handSubmit,
child: Text(L10n.of.login_button),
),
),
Container(
width: double.infinity,
margin: EdgeInsets.only(top: 20),
alignment: Alignment.center,
child: LoginAgree(),
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
LoginDivider(),
Row(
spacing: 20,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
OtherButton(
onTap: () {
_handGoogleSignIn();
},
icon: "assets/image/google.png",
),
OtherButton(
onTap: () {
_handAppleSignIn();
},
icon: "assets/image/apple.png",
),
],
),
],
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,50 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:food_health/l10n/l10n.dart';
import 'package:food_health/router/config/route_paths.dart';
import 'package:go_router/go_router.dart';
class LoginAgree extends StatelessWidget {
const LoginAgree({
super.key,
});
@override
Widget build(BuildContext context) {
return RichText(
text: TextSpan(
style: Theme.of(context).textTheme.labelMedium,
children: [
TextSpan(
text: "${L10n.of.login_tip_start} ",
),
TextSpan(
text: L10n.of.login_terms,
style: TextStyle(color: Theme.of(context).primaryColor),
recognizer: TapGestureRecognizer()
..onTap = () => context.push(
RoutePaths.agreement,
extra: {
"title": "Terms of Service",
"url": "https://support.curain.ai/privacy/foodcura/terms_service.html",
},
),
),
TextSpan(text: " ${L10n.of.login_and} "),
TextSpan(
text: L10n.of.login_privacy,
style: TextStyle(color: Theme.of(context).primaryColor),
recognizer: TapGestureRecognizer()
..onTap = () => context.push(
RoutePaths.agreement,
extra: {
"title": "Privacy",
"url": "https://support.curain.ai/privacy/foodcura/privacy_policy.html",
},
),
),
],
),
);
}
}

View File

@@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
class LoginInput extends StatelessWidget {
final bool obscureText;
final String hintText;
final TextEditingController controller;
final Widget? suffix;
const LoginInput({
super.key,
this.obscureText = false,
required this.hintText,
required this.controller,
this.suffix,
});
@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
obscureText: obscureText,
maxLength: 100,
decoration: InputDecoration(
hintText: hintText,
hintStyle: Theme.of(context).textTheme.labelLarge?.copyWith(fontSize: 16),
counterText: '',
filled: true,
fillColor: Theme.of(context).colorScheme.surfaceContainer,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(60),
borderSide: BorderSide.none, // 去掉边框
),
isCollapsed: true,
contentPadding: EdgeInsets.symmetric(vertical: 14, horizontal: 20),
suffixIcon: Container(
padding: EdgeInsets.only(right: 20),
child: suffix,
),
suffixIconConstraints: BoxConstraints(
minWidth: 0,
minHeight: 0,
),
),
);
}
}

View File

@@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import 'package:food_health/l10n/l10n.dart';
///分割线
class LoginDivider extends StatelessWidget {
const LoginDivider({super.key});
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(top: 20, bottom: 20),
child: Row(
spacing: 8,
children: [
Expanded(
child: Container(
height: 1,
color: Theme.of(context).colorScheme.surfaceContainer,
),
),
Text(
L10n.of.login_other_login,
style: Theme.of(context).textTheme.labelMedium,
),
Expanded(
child: Container(
height: 1,
color: Theme.of(context).colorScheme.surfaceContainer,
),
),
],
),
);
}
}
class OtherButton extends StatelessWidget {
final Function() onTap;
final String icon;
const OtherButton({
super.key,
required this.onTap,
required this.icon,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
width: 45,
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: Theme.of(context).colorScheme.surfaceContainer,
),
),
child: AspectRatio(
aspectRatio: 1,
child: Image.asset(icon),
),
),
);
}
}

View File

@@ -1,7 +1,6 @@
import 'package:food_health/providers/app_store.dart';
import 'package:food_health/stores/app_store.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:food_health/providers/user_store.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
@@ -51,13 +50,6 @@ class _SplashPageState extends State<SplashPage> {
"assets/image/logo.png",
width: 68.w,
),
Container(
margin: EdgeInsets.only(top: 16),
child: Text(
"Demacare",
style: Theme.of(context).textTheme.titleMedium,
),
),
],
),
),

View File

@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
import 'package:food_health/l10n/app_localizations.dart';
class TestPage extends StatefulWidget {
const TestPage({super.key});
@override
State<TestPage> createState() => _TestPageState();
}
class _TestPageState extends State<TestPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text(
AppLocalizations.of(context)?.login_title ?? "",
style: TextStyle(
fontSize: 50,
color: Theme.of(context).colorScheme.primary,
),
),
),
);
}
}

View File

@@ -4,6 +4,9 @@ class RoutePaths {
///闪烁页
static const splash = "/";
///引导页
static const intro = "/intro";
///协议页
static const agreement = "/agreement";

View File

@@ -1,10 +1,12 @@
import 'package:food_health/layout/layout_page.dart';
import 'package:food_health/page/system/login/login_code_page.dart';
import 'package:food_health/page/system/splash/splash_page.dart';
import 'package:food_health/pages/system/code/login_code_page.dart';
import 'package:food_health/pages/system/intro/intro_page.dart';
import 'package:food_health/pages/system/splash/splash_page.dart';
import 'package:food_health/pages/system/test_page.dart';
import 'package:food_health/router/config/route_paths.dart';
import '../../page/system/agree/agree_page.dart';
import '../../page/system/login/login_page.dart';
import '../../pages/system/agree/agree_page.dart';
import '../../pages/system/login/login_page.dart';
import '../config/route_type.dart';
List<RouteType> baseRoutes = [
@@ -30,6 +32,12 @@ List<RouteType> baseRoutes = [
return LoginPage();
},
),
RouteType(
path: RoutePaths.intro,
child: (state) {
return IntroPage();
},
),
RouteType(
path: RoutePaths.loginCode,
child: (state) {
@@ -46,4 +54,10 @@ List<RouteType> baseRoutes = [
return LayoutPage();
},
),
RouteType(
path: "/test",
child: (state) {
return TestPage();
},
),
];

View File

@@ -1,6 +1,6 @@
import 'package:food_health/page/profile/edit/my_edit_page.dart';
import 'package:food_health/pages/profile/edit/my_edit_page.dart';
import '../../page/record/detail/record_detail_page.dart';
import '../../pages/record/detail/record_detail_page.dart';
import '../config/route_paths.dart';
import '../config/route_type.dart';

View File

@@ -22,7 +22,7 @@ List<RouteBase> routes = routeConfigs.map((item) {
//变量命名
GoRouter goRouter = GoRouter(
initialLocation: RoutePaths.splash,
initialLocation: RoutePaths.intro,
routes: routes,
navigatorKey: navigatorKey,
);

View File

@@ -0,0 +1,20 @@
import 'dart:ui';
import 'package:flutter/cupertino.dart';
class SettingStore extends ChangeNotifier {
late Locale _locale;
Locale get locale => _locale;
SettingStore() {
final systemLocale = PlatformDispatcher.instance.locale;
_locale = systemLocale;
}
/// 设置语言
void setLocale(Locale locale) {
_locale = locale;
notifyListeners();
}
}

View File

@@ -1,45 +0,0 @@
import 'package:flutter/material.dart';
class AsyncImage extends StatelessWidget {
final String url;
final double? width;
const AsyncImage({
super.key,
required this.url,
this.width,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: width,
child: Image.network(
url,
fit: BoxFit.cover,
// 加载中的样式
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) {
return child; // 加载完成,直接返回图片
}
return Container(
color: Colors.grey[200],
alignment: Alignment.center,
child: CircularProgressIndicator(strokeWidth: 2),
);
},
// 加载失败的样式
errorBuilder: (context, error, stackTrace) {
return Container(
color: Colors.grey[200],
child: const Icon(
Icons.broken_image,
size: 30,
color: Colors.grey,
),
);
},
),
);
}
}

View File

@@ -0,0 +1,43 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
class AsyncImage extends StatelessWidget {
final String url;
final double? width;
const AsyncImage({
super.key,
required this.url,
this.width,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: width,
child: CachedNetworkImage(
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
imageUrl: url ?? "",
imageBuilder: (context, imageProvider) => Container(
decoration: BoxDecoration(
image: DecorationImage(
image: imageProvider,
fit: BoxFit.cover,
),
),
),
errorWidget: (context, url, error) => Icon(Icons.error),
placeholder: (context, url) => Container(
alignment: Alignment.center,
child: FractionallySizedBox(
widthFactor: 0.3,
heightFactor: 0.3,
child: CircularProgressIndicator(),
),
),
),
);
}
}

View File

@@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import '../utils/enums/ui_theme.dart';
import '../utils/enums/ui_variant.dart';
import '../utils/theme/ui_color.dart';
class AppButton extends StatelessWidget {
final Widget child;
final VoidCallback? onPressed;
final UiThemeType type;
final UiVariant variant;
final bool loading;
final bool round;
final bool disabled;
const AppButton({
super.key,
required this.child,
this.onPressed,
this.type = UiThemeType.primary,
this.variant = UiVariant.solid,
this.loading = false,
this.round = true,
this.disabled = false,
});
@override
Widget build(BuildContext context) {
//设置颜色
final scheme = UiColor.getColorScheme(
context,
type: type,
variant: variant,
);
return Opacity(
opacity: disabled ? 0.5 : 1,
child: ElevatedButton(
onPressed: () {
if (!loading && !disabled) {
onPressed?.call();
}
},
style: ElevatedButton.styleFrom(
backgroundColor: scheme.background,
// 应用自定义颜色
shadowColor: Colors.transparent,
foregroundColor: scheme.foreground,
shape: round
? const StadiumBorder()
: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 8,
children: [
Visibility(
visible: loading,
child: SizedBox.square(
dimension: 15,
child: CircularProgressIndicator(
color: scheme.foreground,
strokeWidth: 2,
),
),
),
child,
],
),
),
);
}
}

View File

@@ -1,84 +0,0 @@
import 'package:flutter/material.dart';
///自定义按钮
///包括loading功能
class CustomButton extends StatelessWidget {
final Widget child;
final VoidCallback? onPressed;
final bool loading;
final bool round;
final bool disabled;
const CustomButton({
super.key,
required this.child,
this.onPressed,
this.loading = false,
this.round = true,
this.disabled = false,
});
@override
Widget build(BuildContext context) {
///自定义颜色
// switch (size) {
// case ButtonSize.small:
// height = 28;
// loadingSize = 16;
// fontSize = 12;
// padding = const EdgeInsets.symmetric(horizontal: 12);
// break;
// case ButtonSize.large:
// height = 48;
// loadingSize = 24;
// fontSize = 18;
// padding = const EdgeInsets.symmetric(horizontal: 20);
// break;
// case ButtonSize.medium:
// height = 45;
// loadingSize = 15;
// fontSize = 16;
// padding = const EdgeInsets.symmetric(horizontal: 16);
// break;
// }
void handClick() {
if (!loading && !disabled) {
onPressed?.call();
}
}
return Opacity(
opacity: disabled ? 0.5 : 1,
child: ElevatedButton(
onPressed: handClick,
style: ElevatedButton.styleFrom(
shape: round
? const StadiumBorder()
: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Visibility(
visible: loading,
child: Container(
margin: EdgeInsets.only(right: 8),
child: SizedBox.square(
dimension: 15,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
),
),
),
child,
],
),
),
);
}
}

View File

@@ -0,0 +1,7 @@
// 主题风格
enum UiThemeType {
primary,
success,
warning,
danger,
}

View File

@@ -0,0 +1,5 @@
//定义Variant
enum UiVariant {
solid,
plain,
}

View File

@@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:food_health/config/theme/base/app_theme_ext.dart';
import '../enums/ui_theme.dart';
import '../enums/ui_variant.dart';
class UiColorScheme {
final Color background;
final Color foreground;
UiColorScheme({
required this.background,
required this.foreground,
});
}
class UiColor {
static UiColorScheme getColorScheme(
BuildContext context, {
UiThemeType type = UiThemeType.primary,
UiVariant variant = UiVariant.solid,
}) {
final colorScheme = Theme.of(context).colorScheme;
final themeExt = Theme.of(context).extension<AppThemeExtension>()!;
//主题色映射
final baseColor = switch (type) {
UiThemeType.primary => colorScheme.primary,
UiThemeType.success => themeExt.success,
UiThemeType.warning => themeExt.warning,
UiThemeType.danger => themeExt.danger,
};
// 风格区分
return switch (variant) {
UiVariant.solid => UiColorScheme(
background: baseColor,
foreground: Colors.white,
),
UiVariant.plain => UiColorScheme(
background: baseColor.withValues(alpha: 0.3),
foreground: baseColor,
),
};
}
}

View File

@@ -25,6 +25,30 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.2"
cached_network_image:
dependency: "direct main"
description:
name: cached_network_image
sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.4.1"
cached_network_image_platform_interface:
dependency: transitive
description:
name: cached_network_image_platform_interface
sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.1.1"
cached_network_image_web:
dependency: transitive
description:
name: cached_network_image_web
sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.1"
characters:
dependency: transitive
description:
@@ -57,6 +81,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.3.4+2"
crypto:
dependency: transitive
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.6"
cupertino_icons:
dependency: "direct main"
description:
@@ -153,11 +185,27 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.9.3+4"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_cache_manager:
dependency: transitive
description:
name: flutter_cache_manager
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.4.1"
flutter_easyloading:
dependency: "direct main"
description:
@@ -230,6 +278,11 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@@ -408,6 +461,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.2"
intl:
dependency: "direct main"
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.20.2"
leak_tracker:
dependency: transitive
description:
@@ -512,6 +573,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.0"
octo_image:
dependency: transitive
description:
name: octo_image
sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.0"
path:
dependency: transitive
description:
@@ -520,6 +589,30 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.9.1"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "993381400e94d18469750e5b9dcb8206f15bc09f9da86b9e44a9b0092a0066db"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.18"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.2"
path_provider_linux:
dependency: transitive
description:
@@ -584,6 +677,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.1"
rxdart:
dependency: transitive
description:
name: rxdart
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.28.0"
scroll_to_index:
dependency: transitive
description:
@@ -693,6 +794,54 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.10.1"
sprintf:
dependency: transitive
description:
name: sprintf
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.0.0"
sqflite:
dependency: transitive
description:
name: sqflite
sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.2"
sqflite_android:
dependency: transitive
description:
name: sqflite_android
sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.1"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.5.6"
sqflite_darwin:
dependency: transitive
description:
name: sqflite_darwin
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.2"
sqflite_platform_interface:
dependency: transitive
description:
name: sqflite_platform_interface
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.0"
stack_trace:
dependency: transitive
description:
@@ -717,6 +866,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.1"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.4.0"
term_glyph:
dependency: transitive
description:
@@ -805,6 +962,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.4"
uuid:
dependency: transitive
description:
name: uuid
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.5.1"
vector_math:
dependency: transitive
description:
@@ -893,6 +1058,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.6.1"
yaml:
dependency: "direct main"
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.8.1 <4.0.0"
flutter: ">=3.31.0-0.0.pre"

View File

@@ -10,7 +10,8 @@ environment:
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
cupertino_icons: ^1.0.8
shared_preferences: ^2.5.3
dio: ^5.8.0+1
@@ -28,7 +29,9 @@ dependencies:
markdown_widget: ^2.3.2+8
sign_in_with_apple: ^7.0.1
flutter_image_compress: ^2.4.0
cached_network_image: ^3.4.1
yaml: ^3.1.3
intl: any
dev_dependencies:
flutter_test:
@@ -39,5 +42,7 @@ dev_dependencies:
flutter:
uses-material-design: true
generate: true
assets:
- assets/image/
- assets/image/
- assets/image/bg/

67
scripts/gen_arb.dart Normal file
View File

@@ -0,0 +1,67 @@
import 'dart:convert';
import 'dart:io';
import 'package:yaml/yaml.dart';
void main() {
// 输入文件和输出目录
var inputFile = File("./lib/l10n/arb/i18n.yaml");
var outDir = Directory("./lib/l10n/arb");
if (!inputFile.existsSync()) {
print('❌ i18n.yaml 文件不存在:$inputFile');
exit(1);
}
// 读取并解析 yaml
final yamlMap = loadYaml(inputFile.readAsStringSync());
Map<String, Map<String, dynamic>> arbMaps = {};
// 递归处理嵌套的 key
void processYaml(Map yaml, {String? prefix}) {
yaml.forEach((key, value) {
if (value is Map && value.values.any((v) => v is Map)) {
// 说明还有子层级,递归处理
processYaml(value, prefix: prefix == null ? key : "${prefix}_$key");
} else if (value is Map) {
// 叶子节点:包含 zh/en/description
final description = value['description'] ?? '';
value.forEach((lang, text) {
if (lang == 'description') return;
final arbKey = prefix == null ? key : "${prefix}_$key";
arbMaps.putIfAbsent(lang, () => {});
arbMaps[lang]![arbKey] = text;
if (description.toString().isNotEmpty) {
arbMaps[lang]!['@$arbKey'] = {'description': description};
}
});
}
});
}
processYaml(yamlMap);
// 创建 arb 文件
arbMaps.forEach((lang, json) {
final file = File("${outDir.path}/app_$lang.arb");
final encoder = JsonEncoder.withIndent(' ');
file.writeAsStringSync(encoder.convert(json));
print('✅ 生成 ${file.path}');
});
// 执行 flutter gen-l10n
final result = Process.runSync(
"flutter",
["gen-l10n"],
runInShell: true, // 确保跨平台可用
);
if (result.exitCode == 0) {
print('✅ flutter gen-l10n 成功');
} else {
print('❌ flutter gen-l10n 失败:${result.stderr}');
}
}