diff --git a/README.md b/README.md index cd17729..e69de29 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/assets/image/bg/intro_bg.png b/assets/image/bg/intro_bg.png new file mode 100644 index 0000000..dd7c7e3 Binary files /dev/null and b/assets/image/bg/intro_bg.png differ diff --git a/assets/image/bg_hushi.png b/assets/image/bg_hushi.png deleted file mode 100644 index 144c66c..0000000 Binary files a/assets/image/bg_hushi.png and /dev/null differ diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 0000000..3441963 --- /dev/null +++ b/l10n.yaml @@ -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 diff --git a/lib/api/network/interceptor.dart b/lib/api/network/interceptor.dart index 30aee7e..6239749 100644 --- a/lib/api/network/interceptor.dart +++ b/lib/api/network/interceptor.dart @@ -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'; ///请求拦截器 diff --git a/lib/config/theme/base/app_colors_base.dart b/lib/config/theme/base/app_colors_base.dart new file mode 100644 index 0000000..f94e915 --- /dev/null +++ b/lib/config/theme/base/app_colors_base.dart @@ -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 +} diff --git a/lib/config/theme/base/app_text_style.dart b/lib/config/theme/base/app_text_style.dart new file mode 100644 index 0000000..8a98f19 --- /dev/null +++ b/lib/config/theme/base/app_text_style.dart @@ -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, + ), + ); +} diff --git a/lib/config/theme/base/app_theme_ext.dart b/lib/config/theme/base/app_theme_ext.dart new file mode 100644 index 0000000..d2ac0ef --- /dev/null +++ b/lib/config/theme/base/app_theme_ext.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; + +import 'app_colors_base.dart'; + +@immutable +class AppThemeExtension extends ThemeExtension { + 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 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()!; + + Color get success => themeEx.success; + + Color get warning => themeEx.warning; + + Color get danger => themeEx.danger; + + Color get textSecondary => themeEx.textSecondary; +} diff --git a/lib/config/theme/custom_colors.dart b/lib/config/theme/color_ext.dart similarity index 99% rename from lib/config/theme/custom_colors.dart rename to lib/config/theme/color_ext.dart index b34b74b..4736776 100644 --- a/lib/config/theme/custom_colors.dart +++ b/lib/config/theme/color_ext.dart @@ -12,3 +12,4 @@ extension CustomColors on ColorScheme { Color get primaryEnd => const Color(0xff06b6d4); } + diff --git a/lib/config/theme/theme.dart b/lib/config/theme/theme.dart index 55f57bf..48b0505 100644 --- a/lib/config/theme/theme.dart +++ b/lib/config/theme/theme.dart @@ -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); // 次要强调 diff --git a/lib/config/theme/themes/light_theme.dart b/lib/config/theme/themes/light_theme.dart new file mode 100644 index 0000000..f582471 --- /dev/null +++ b/lib/config/theme/themes/light_theme.dart @@ -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); +} diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart new file mode 100644 index 0000000..67722ea --- /dev/null +++ b/lib/l10n/app_localizations.dart @@ -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, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s 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(context, AppLocalizations); + } + + static const LocalizationsDelegate 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> localizationsDelegates = + >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [ + 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 { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => ['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.', + ); +} diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart new file mode 100644 index 0000000..a4ff752 --- /dev/null +++ b/lib/l10n/app_localizations_en.dart @@ -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'; +} diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart new file mode 100644 index 0000000..36ff527 --- /dev/null +++ b/lib/l10n/app_localizations_zh.dart @@ -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 => '验证失败'; +} diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb new file mode 100644 index 0000000..a1db357 --- /dev/null +++ b/lib/l10n/arb/app_en.arb @@ -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" +} \ No newline at end of file diff --git a/lib/l10n/arb/app_zh.arb b/lib/l10n/arb/app_zh.arb new file mode 100644 index 0000000..6b65257 --- /dev/null +++ b/lib/l10n/arb/app_zh.arb @@ -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": "验证失败" +} \ No newline at end of file diff --git a/lib/l10n/arb/i18n.yaml b/lib/l10n/arb/i18n.yaml new file mode 100644 index 0000000..85fd2aa --- /dev/null +++ b/lib/l10n/arb/i18n.yaml @@ -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 diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart new file mode 100644 index 0000000..368f035 --- /dev/null +++ b/lib/l10n/l10n.dart @@ -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!; +} diff --git a/lib/layout/layout_page.dart b/lib/layout/layout_page.dart index cb21e75..8f0f119 100644 --- a/lib/layout/layout_page.dart +++ b/lib/layout/layout_page.dart @@ -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'; diff --git a/lib/main.dart b/lib/main.dart index 7ea405a..31e4188 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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(); 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); + }, ), ); } diff --git a/lib/page/system/login/login_code_page.dart b/lib/page/system/login/login_code_page.dart deleted file mode 100644 index fb291b5..0000000 --- a/lib/page/system/login/login_code_page.dart +++ /dev/null @@ -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 createState() => _LoginCodePageState(); -} - -class _LoginCodePageState extends State { - 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(); - 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, - ), - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/page/system/login/login_page.dart b/lib/page/system/login/login_page.dart deleted file mode 100644 index 9c6a735..0000000 --- a/lib/page/system/login/login_page.dart +++ /dev/null @@ -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 createState() => _LoginPageState(); -} - -class _LoginPageState extends State { - 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 _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.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; - }); - }, - ), - ), - ], - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/page/system/login/widget/agreement_box.dart b/lib/page/system/login/widget/agreement_box.dart deleted file mode 100644 index f14b54f..0000000 --- a/lib/page/system/login/widget/agreement_box.dart +++ /dev/null @@ -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"}, - ), - ), - ], - ), - ), - ), - ], - ); - } -} diff --git a/lib/page/system/login/widget/widget.dart b/lib/page/system/login/widget/widget.dart deleted file mode 100644 index cf8f757..0000000 --- a/lib/page/system/login/widget/widget.dart +++ /dev/null @@ -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, - ), - ], - ), - ), - ); - } -} diff --git a/lib/page/home/home_page.dart b/lib/pages/home/home_page.dart similarity index 79% rename from lib/page/home/home_page.dart rename to lib/pages/home/home_page.dart index 705c59d..ecd5514 100644 --- a/lib/page/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -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 { diff --git a/lib/page/home/widget/home_header.dart b/lib/pages/home/widget/home_header.dart similarity index 100% rename from lib/page/home/widget/home_header.dart rename to lib/pages/home/widget/home_header.dart diff --git a/lib/page/home/widget/upload_panel.dart b/lib/pages/home/widget/upload_panel.dart similarity index 99% rename from lib/page/home/widget/upload_panel.dart rename to lib/pages/home/widget/upload_panel.dart index 982c46b..24d1700 100644 --- a/lib/page/home/widget/upload_panel.dart +++ b/lib/pages/home/widget/upload_panel.dart @@ -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'; diff --git a/lib/page/profile/edit/data/state.dart b/lib/pages/profile/edit/data/state.dart similarity index 100% rename from lib/page/profile/edit/data/state.dart rename to lib/pages/profile/edit/data/state.dart diff --git a/lib/page/profile/edit/my_edit_page.dart b/lib/pages/profile/edit/my_edit_page.dart similarity index 97% rename from lib/page/profile/edit/my_edit_page.dart rename to lib/pages/profile/edit/my_edit_page.dart index 2bb85f5..cb22a29 100644 --- a/lib/page/profile/edit/my_edit_page.dart +++ b/lib/pages/profile/edit/my_edit_page.dart @@ -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'; diff --git a/lib/page/profile/edit/util/common.dart b/lib/pages/profile/edit/util/common.dart similarity index 100% rename from lib/page/profile/edit/util/common.dart rename to lib/pages/profile/edit/util/common.dart diff --git a/lib/page/profile/edit/widget/common.dart b/lib/pages/profile/edit/widget/common.dart similarity index 100% rename from lib/page/profile/edit/widget/common.dart rename to lib/pages/profile/edit/widget/common.dart diff --git a/lib/page/profile/edit/widget/dietary_preferences.dart b/lib/pages/profile/edit/widget/dietary_preferences.dart similarity index 100% rename from lib/page/profile/edit/widget/dietary_preferences.dart rename to lib/pages/profile/edit/widget/dietary_preferences.dart diff --git a/lib/page/profile/edit/widget/food_allergies.dart b/lib/pages/profile/edit/widget/food_allergies.dart similarity index 98% rename from lib/page/profile/edit/widget/food_allergies.dart rename to lib/pages/profile/edit/widget/food_allergies.dart index 6e0accf..a6c0979 100644 --- a/lib/page/profile/edit/widget/food_allergies.dart +++ b/lib/pages/profile/edit/widget/food_allergies.dart @@ -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'; diff --git a/lib/page/profile/edit/widget/health_profile.dart b/lib/pages/profile/edit/widget/health_profile.dart similarity index 98% rename from lib/page/profile/edit/widget/health_profile.dart rename to lib/pages/profile/edit/widget/health_profile.dart index 4233ce9..47716fc 100644 --- a/lib/page/profile/edit/widget/health_profile.dart +++ b/lib/pages/profile/edit/widget/health_profile.dart @@ -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'; diff --git a/lib/page/profile/my/my_page.dart b/lib/pages/profile/my/my_page.dart similarity index 97% rename from lib/page/profile/my/my_page.dart rename to lib/pages/profile/my/my_page.dart index 98d061a..f4ffa0e 100644 --- a/lib/page/profile/my/my_page.dart +++ b/lib/pages/profile/my/my_page.dart @@ -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'; diff --git a/lib/page/profile/my/widget/title_card.dart b/lib/pages/profile/my/widget/title_card.dart similarity index 100% rename from lib/page/profile/my/widget/title_card.dart rename to lib/pages/profile/my/widget/title_card.dart diff --git a/lib/page/profile/my/widget/user_card.dart b/lib/pages/profile/my/widget/user_card.dart similarity index 97% rename from lib/page/profile/my/widget/user_card.dart rename to lib/pages/profile/my/widget/user_card.dart index 2a1348a..8fd55b9 100644 --- a/lib/page/profile/my/widget/user_card.dart +++ b/lib/pages/profile/my/widget/user_card.dart @@ -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'; diff --git a/lib/page/record/detail/record_detail_page.dart b/lib/pages/record/detail/record_detail_page.dart similarity index 100% rename from lib/page/record/detail/record_detail_page.dart rename to lib/pages/record/detail/record_detail_page.dart diff --git a/lib/page/record/detail/widget/detailed_analysis.dart b/lib/pages/record/detail/widget/detailed_analysis.dart similarity index 53% rename from lib/page/record/detail/widget/detailed_analysis.dart rename to lib/pages/record/detail/widget/detailed_analysis.dart index 6c7e9d8..93d75cb 100644 --- a/lib/page/record/detail/widget/detailed_analysis.dart +++ b/lib/pages/record/detail/widget/detailed_analysis.dart @@ -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(), + ), + ), + ], + ), ), ], ), diff --git a/lib/page/record/detail/widget/health_recommend.dart b/lib/pages/record/detail/widget/health_recommend.dart similarity index 95% rename from lib/page/record/detail/widget/health_recommend.dart rename to lib/pages/record/detail/widget/health_recommend.dart index 9b858f9..85f78bd 100644 --- a/lib/page/record/detail/widget/health_recommend.dart +++ b/lib/pages/record/detail/widget/health_recommend.dart @@ -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'; diff --git a/lib/page/record/detail/widget/result_chip.dart b/lib/pages/record/detail/widget/result_chip.dart similarity index 98% rename from lib/page/record/detail/widget/result_chip.dart rename to lib/pages/record/detail/widget/result_chip.dart index cefd9b8..0d668a2 100644 --- a/lib/page/record/detail/widget/result_chip.dart +++ b/lib/pages/record/detail/widget/result_chip.dart @@ -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 { diff --git a/lib/page/record/list/record_list_page.dart b/lib/pages/record/list/record_list_page.dart similarity index 100% rename from lib/page/record/list/record_list_page.dart rename to lib/pages/record/list/record_list_page.dart diff --git a/lib/page/record/list/widget/record_list_card.dart b/lib/pages/record/list/widget/record_list_card.dart similarity index 96% rename from lib/page/record/list/widget/record_list_card.dart rename to lib/pages/record/list/widget/record_list_card.dart index cac024c..77760dc 100644 --- a/lib/page/record/list/widget/record_list_card.dart +++ b/lib/pages/record/list/widget/record_list_card.dart @@ -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]; diff --git a/lib/page/system/agree/agree_page.dart b/lib/pages/system/agree/agree_page.dart similarity index 100% rename from lib/page/system/agree/agree_page.dart rename to lib/pages/system/agree/agree_page.dart diff --git a/lib/pages/system/code/login_code_page.dart b/lib/pages/system/code/login_code_page.dart new file mode 100644 index 0000000..c46cae0 --- /dev/null +++ b/lib/pages/system/code/login_code_page.dart @@ -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 createState() => _LoginCodePageState(); +} + +class _LoginCodePageState extends State { + final List _focusNodes = List.generate(4, (_) => FocusNode()); + final List _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(); + 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, + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/system/intro/intro_page.dart b/lib/pages/system/intro/intro_page.dart new file mode 100644 index 0000000..e8e4405 --- /dev/null +++ b/lib/pages/system/intro/intro_page.dart @@ -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 createState() => _IntroPageState(); +} + +class _IntroPageState extends State { + 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), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/system/login/login_page.dart b/lib/pages/system/login/login_page.dart new file mode 100644 index 0000000..9076302 --- /dev/null +++ b/lib/pages/system/login/login_page.dart @@ -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 createState() => _LoginPageState(); +} + +class _LoginPageState extends State { + 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 _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.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", + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/system/login/widgets/login_agree.dart b/lib/pages/system/login/widgets/login_agree.dart new file mode 100644 index 0000000..c66cbcb --- /dev/null +++ b/lib/pages/system/login/widgets/login_agree.dart @@ -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", + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/system/login/widgets/login_input.dart b/lib/pages/system/login/widgets/login_input.dart new file mode 100644 index 0000000..ef57776 --- /dev/null +++ b/lib/pages/system/login/widgets/login_input.dart @@ -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, + ), + ), + ); + } +} diff --git a/lib/pages/system/login/widgets/login_other.dart b/lib/pages/system/login/widgets/login_other.dart new file mode 100644 index 0000000..41f911a --- /dev/null +++ b/lib/pages/system/login/widgets/login_other.dart @@ -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), + ), + ), + ); + } +} diff --git a/lib/page/system/splash/splash_page.dart b/lib/pages/system/splash/splash_page.dart similarity index 81% rename from lib/page/system/splash/splash_page.dart rename to lib/pages/system/splash/splash_page.dart index d3677a9..7bfba98 100644 --- a/lib/page/system/splash/splash_page.dart +++ b/lib/pages/system/splash/splash_page.dart @@ -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 { "assets/image/logo.png", width: 68.w, ), - Container( - margin: EdgeInsets.only(top: 16), - child: Text( - "Demacare", - style: Theme.of(context).textTheme.titleMedium, - ), - ), ], ), ), diff --git a/lib/pages/system/test_page.dart b/lib/pages/system/test_page.dart new file mode 100644 index 0000000..937b868 --- /dev/null +++ b/lib/pages/system/test_page.dart @@ -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 createState() => _TestPageState(); +} + +class _TestPageState extends State { + @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, + ), + ), + ), + ); + } +} diff --git a/lib/router/config/route_paths.dart b/lib/router/config/route_paths.dart index 341d893..c323187 100644 --- a/lib/router/config/route_paths.dart +++ b/lib/router/config/route_paths.dart @@ -4,6 +4,9 @@ class RoutePaths { ///闪烁页 static const splash = "/"; + ///引导页 + static const intro = "/intro"; + ///协议页 static const agreement = "/agreement"; diff --git a/lib/router/modules/base.dart b/lib/router/modules/base.dart index e39065d..195e061 100644 --- a/lib/router/modules/base.dart +++ b/lib/router/modules/base.dart @@ -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 baseRoutes = [ @@ -30,6 +32,12 @@ List baseRoutes = [ return LoginPage(); }, ), + RouteType( + path: RoutePaths.intro, + child: (state) { + return IntroPage(); + }, + ), RouteType( path: RoutePaths.loginCode, child: (state) { @@ -46,4 +54,10 @@ List baseRoutes = [ return LayoutPage(); }, ), + RouteType( + path: "/test", + child: (state) { + return TestPage(); + }, + ), ]; diff --git a/lib/router/modules/serve.dart b/lib/router/modules/serve.dart index f3a572f..e3930d7 100644 --- a/lib/router/modules/serve.dart +++ b/lib/router/modules/serve.dart @@ -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'; diff --git a/lib/router/routes.dart b/lib/router/routes.dart index 35077aa..0d1d024 100644 --- a/lib/router/routes.dart +++ b/lib/router/routes.dart @@ -22,7 +22,7 @@ List routes = routeConfigs.map((item) { //变量命名 GoRouter goRouter = GoRouter( - initialLocation: RoutePaths.splash, + initialLocation: RoutePaths.intro, routes: routes, navigatorKey: navigatorKey, ); diff --git a/lib/providers/app_store.dart b/lib/stores/app_store.dart similarity index 100% rename from lib/providers/app_store.dart rename to lib/stores/app_store.dart diff --git a/lib/stores/setting_store.dart b/lib/stores/setting_store.dart new file mode 100644 index 0000000..ac681fb --- /dev/null +++ b/lib/stores/setting_store.dart @@ -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(); + } +} diff --git a/lib/providers/user_store.dart b/lib/stores/user_store.dart similarity index 100% rename from lib/providers/user_store.dart rename to lib/stores/user_store.dart diff --git a/lib/widgets/common/async_image.dart b/lib/widgets/common/async_image.dart deleted file mode 100644 index e4d8dda..0000000 --- a/lib/widgets/common/async_image.dart +++ /dev/null @@ -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, - ), - ); - }, - ), - ); - } -} diff --git a/lib/widgets/common/app_backend.dart b/lib/widgets/shared/app_backend.dart similarity index 100% rename from lib/widgets/common/app_backend.dart rename to lib/widgets/shared/app_backend.dart diff --git a/lib/widgets/common/app_header.dart b/lib/widgets/shared/app_header.dart similarity index 100% rename from lib/widgets/common/app_header.dart rename to lib/widgets/shared/app_header.dart diff --git a/lib/widgets/shared/async_image.dart b/lib/widgets/shared/async_image.dart new file mode 100644 index 0000000..51e1f61 --- /dev/null +++ b/lib/widgets/shared/async_image.dart @@ -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(), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/ui_kit/button/app_button.dart b/lib/widgets/ui_kit/button/app_button.dart new file mode 100644 index 0000000..fe22327 --- /dev/null +++ b/lib/widgets/ui_kit/button/app_button.dart @@ -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, + ], + ), + ), + ); + } +} diff --git a/lib/widgets/ui_kit/button/custom_button.dart b/lib/widgets/ui_kit/button/custom_button.dart deleted file mode 100644 index 0e3c76c..0000000 --- a/lib/widgets/ui_kit/button/custom_button.dart +++ /dev/null @@ -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, - ], - ), - ), - ); - } -} diff --git a/lib/widgets/ui_kit/utils/enums/ui_theme.dart b/lib/widgets/ui_kit/utils/enums/ui_theme.dart new file mode 100644 index 0000000..a576210 --- /dev/null +++ b/lib/widgets/ui_kit/utils/enums/ui_theme.dart @@ -0,0 +1,7 @@ +// 主题风格 +enum UiThemeType { + primary, + success, + warning, + danger, +} diff --git a/lib/widgets/ui_kit/utils/enums/ui_variant.dart b/lib/widgets/ui_kit/utils/enums/ui_variant.dart new file mode 100644 index 0000000..fa189f8 --- /dev/null +++ b/lib/widgets/ui_kit/utils/enums/ui_variant.dart @@ -0,0 +1,5 @@ +//定义Variant +enum UiVariant { + solid, + plain, +} diff --git a/lib/widgets/ui_kit/utils/theme/ui_color.dart b/lib/widgets/ui_kit/utils/theme/ui_color.dart new file mode 100644 index 0000000..5fec9e9 --- /dev/null +++ b/lib/widgets/ui_kit/utils/theme/ui_color.dart @@ -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()!; + + //主题色映射 + 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, + ), + }; + } +} diff --git a/pubspec.lock b/pubspec.lock index 80246a1..853a70c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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" diff --git a/pubspec.yaml b/pubspec.yaml index 5f3bcb6..7cf5f8d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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/ \ No newline at end of file + - assets/image/ + - assets/image/bg/ \ No newline at end of file diff --git a/scripts/gen_arb.dart b/scripts/gen_arb.dart new file mode 100644 index 0000000..0aac6bf --- /dev/null +++ b/scripts/gen_arb.dart @@ -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> 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}'); + } +}