登录流程已全部重构
This commit is contained in:
16
README.md
16
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.
|
||||
|
||||
BIN
assets/image/bg/intro_bg.png
Normal file
BIN
assets/image/bg/intro_bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 337 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 537 KiB |
5
l10n.yaml
Normal file
5
l10n.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
arb-dir: lib/l10n/arb
|
||||
template-arb-file: app_zh.arb
|
||||
output-dir: lib/l10n
|
||||
synthetic-package: false
|
||||
output-localization-file: app_localizations.dart
|
||||
@@ -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';
|
||||
|
||||
///请求拦截器
|
||||
|
||||
31
lib/config/theme/base/app_colors_base.dart
Normal file
31
lib/config/theme/base/app_colors_base.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
abstract class AppColorsBase {
|
||||
/// 品牌主色
|
||||
Color get primary;
|
||||
|
||||
Color get secondary;
|
||||
|
||||
// 灰度
|
||||
Color get textPrimary;
|
||||
|
||||
Color get textSecondary;
|
||||
|
||||
// 扩展颜色
|
||||
Color get success;
|
||||
|
||||
Color get warning;
|
||||
|
||||
Color get info;
|
||||
|
||||
Color get danger;
|
||||
|
||||
// 容器色
|
||||
Color get surfaceContainerLowest;
|
||||
|
||||
Color get surfaceContainerLow;
|
||||
|
||||
Color get surfaceContainer;
|
||||
|
||||
Color get surfaceContainerHigh; //白色卡片 / item
|
||||
}
|
||||
54
lib/config/theme/base/app_text_style.dart
Normal file
54
lib/config/theme/base/app_text_style.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'app_colors_base.dart'; // 假设你之前定义了 AppColorsBase
|
||||
|
||||
TextTheme buildTextTheme(AppColorsBase colors) {
|
||||
return TextTheme(
|
||||
// 标题层级
|
||||
titleLarge: TextStyle(
|
||||
fontSize: 22, // 比正文大6
|
||||
fontWeight: FontWeight.w700,
|
||||
color: colors.textPrimary,
|
||||
),
|
||||
titleMedium: TextStyle(
|
||||
fontSize: 20, // 比正文大4
|
||||
fontWeight: FontWeight.w700,
|
||||
color: colors.textPrimary,
|
||||
),
|
||||
titleSmall: TextStyle(
|
||||
fontSize: 18, // 比正文大2
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colors.textPrimary,
|
||||
),
|
||||
|
||||
// 正文字体
|
||||
bodyLarge: TextStyle(
|
||||
fontSize: 18, // 稍大正文
|
||||
color: colors.textPrimary,
|
||||
),
|
||||
bodyMedium: TextStyle(
|
||||
fontSize: 16, // 正文标准
|
||||
color: colors.textPrimary,
|
||||
),
|
||||
bodySmall: TextStyle(
|
||||
fontSize: 14, // 辅助正文
|
||||
color: colors.textSecondary,
|
||||
),
|
||||
|
||||
// 标签/提示文字
|
||||
labelLarge: TextStyle(
|
||||
fontSize: 14, // 比正文小一点
|
||||
fontWeight: FontWeight.w500,
|
||||
color: colors.textSecondary,
|
||||
),
|
||||
labelMedium: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: colors.textSecondary,
|
||||
),
|
||||
labelSmall: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: colors.textSecondary,
|
||||
),
|
||||
);
|
||||
}
|
||||
66
lib/config/theme/base/app_theme_ext.dart
Normal file
66
lib/config/theme/base/app_theme_ext.dart
Normal file
@@ -0,0 +1,66 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'app_colors_base.dart';
|
||||
|
||||
@immutable
|
||||
class AppThemeExtension extends ThemeExtension<AppThemeExtension> {
|
||||
final Color success;
|
||||
final Color warning;
|
||||
final Color danger;
|
||||
final Color textSecondary;
|
||||
|
||||
const AppThemeExtension({
|
||||
required this.success,
|
||||
required this.warning,
|
||||
required this.danger,
|
||||
required this.textSecondary,
|
||||
});
|
||||
|
||||
// 工厂方法,根据 AppColorsBase 创建
|
||||
factory AppThemeExtension.fromColors(AppColorsBase colors) {
|
||||
return AppThemeExtension(
|
||||
success: colors.success,
|
||||
warning: colors.warning,
|
||||
danger: colors.danger,
|
||||
textSecondary: colors.textSecondary,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
ThemeExtension<AppThemeExtension> copyWith({
|
||||
Color? success,
|
||||
Color? warning,
|
||||
Color? danger,
|
||||
Color? textSecondary,
|
||||
}) {
|
||||
return AppThemeExtension(
|
||||
success: success ?? this.success,
|
||||
warning: warning ?? this.warning,
|
||||
danger: danger ?? this.danger,
|
||||
textSecondary: textSecondary ?? this.textSecondary,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AppThemeExtension lerp(AppThemeExtension? other, double t) {
|
||||
if (other == null) return this;
|
||||
return AppThemeExtension(
|
||||
success: Color.lerp(success, other.success, t)!,
|
||||
warning: Color.lerp(warning, other.warning, t)!,
|
||||
danger: Color.lerp(danger, other.danger, t)!,
|
||||
textSecondary: Color.lerp(textSecondary, other.textSecondary, t)!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension AppThemeExt on BuildContext {
|
||||
AppThemeExtension get themeEx => Theme.of(this).extension<AppThemeExtension>()!;
|
||||
|
||||
Color get success => themeEx.success;
|
||||
|
||||
Color get warning => themeEx.warning;
|
||||
|
||||
Color get danger => themeEx.danger;
|
||||
|
||||
Color get textSecondary => themeEx.textSecondary;
|
||||
}
|
||||
@@ -12,3 +12,4 @@ extension CustomColors on ColorScheme {
|
||||
|
||||
Color get primaryEnd => const Color(0xff06b6d4);
|
||||
}
|
||||
|
||||
@@ -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); // 次要强调
|
||||
|
||||
43
lib/config/theme/themes/light_theme.dart
Normal file
43
lib/config/theme/themes/light_theme.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../base/app_colors_base.dart';
|
||||
|
||||
class LightTheme extends AppColorsBase {
|
||||
@override
|
||||
Color get primary => const Color(0xff22BB9B);
|
||||
|
||||
@override
|
||||
Color get secondary => const Color(0xffE8F5E9);
|
||||
|
||||
@override
|
||||
Color get textPrimary => const Color(0xFF212121);
|
||||
|
||||
@override
|
||||
Color get textSecondary => const Color(0xffa8aca4);
|
||||
|
||||
@override
|
||||
Color get success => const Color(0xff57be80);
|
||||
|
||||
@override
|
||||
Color get warning => const Color(0xffff9800);
|
||||
|
||||
@override
|
||||
Color get info => const Color(0xff909399);
|
||||
|
||||
@override
|
||||
Color get danger => const Color(0xfff44545);
|
||||
|
||||
@override
|
||||
Color get surfaceContainerLowest => Color(0xffE0E0E0);
|
||||
|
||||
@override
|
||||
Color get surfaceContainerLow => Color(0xffF0F0F0);
|
||||
|
||||
@override
|
||||
Color get surfaceContainer => Color(0xffF5F5F5);
|
||||
|
||||
@override
|
||||
Color get surfaceContainerHigh => Color(0xffFFFFFF);
|
||||
}
|
||||
239
lib/l10n/app_localizations.dart
Normal file
239
lib/l10n/app_localizations.dart
Normal file
@@ -0,0 +1,239 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
import 'app_localizations_en.dart';
|
||||
import 'app_localizations_zh.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// Callers can lookup localized strings with an instance of AppLocalizations
|
||||
/// returned by `AppLocalizations.of(context)`.
|
||||
///
|
||||
/// Applications need to include `AppLocalizations.delegate()` in their app's
|
||||
/// `localizationDelegates` list, and the locales they support in the app's
|
||||
/// `supportedLocales` list. For example:
|
||||
///
|
||||
/// ```dart
|
||||
/// import 'l10n/app_localizations.dart';
|
||||
///
|
||||
/// return MaterialApp(
|
||||
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
/// supportedLocales: AppLocalizations.supportedLocales,
|
||||
/// home: MyApplicationHome(),
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// ## Update pubspec.yaml
|
||||
///
|
||||
/// Please make sure to update your pubspec.yaml to include the following
|
||||
/// packages:
|
||||
///
|
||||
/// ```yaml
|
||||
/// dependencies:
|
||||
/// # Internationalization support.
|
||||
/// flutter_localizations:
|
||||
/// sdk: flutter
|
||||
/// intl: any # Use the pinned version from flutter_localizations
|
||||
///
|
||||
/// # Rest of dependencies
|
||||
/// ```
|
||||
///
|
||||
/// ## iOS Applications
|
||||
///
|
||||
/// iOS applications define key application metadata, including supported
|
||||
/// locales, in an Info.plist file that is built into the application bundle.
|
||||
/// To configure the locales supported by your app, 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<AppLocalizations>(context, AppLocalizations);
|
||||
}
|
||||
|
||||
static const LocalizationsDelegate<AppLocalizations> delegate = _AppLocalizationsDelegate();
|
||||
|
||||
/// A list of this localizations delegate along with the default localizations
|
||||
/// delegates.
|
||||
///
|
||||
/// Returns a list of localizations delegates containing this delegate along with
|
||||
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
|
||||
/// and GlobalWidgetsLocalizations.delegate.
|
||||
///
|
||||
/// Additional delegates can be added by appending to this list in
|
||||
/// MaterialApp. This list does not have to be used at all if a custom list
|
||||
/// of delegates is preferred or required.
|
||||
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
|
||||
<LocalizationsDelegate<dynamic>>[
|
||||
delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
];
|
||||
|
||||
/// A list of this localizations delegate's supported locales.
|
||||
static const List<Locale> supportedLocales = <Locale>[
|
||||
Locale('en'),
|
||||
Locale('zh'),
|
||||
];
|
||||
|
||||
/// No description provided for @welcome_title.
|
||||
///
|
||||
/// In zh, this message translates to:
|
||||
/// **'成分一目了然'**
|
||||
String get welcome_title;
|
||||
|
||||
/// No description provided for @welcome_desc.
|
||||
///
|
||||
/// In zh, this message translates to:
|
||||
/// **'自动识别食物成分,营养与风险一手掌握'**
|
||||
String get welcome_desc;
|
||||
|
||||
/// No description provided for @welcome_button_text.
|
||||
///
|
||||
/// In zh, this message translates to:
|
||||
/// **'开始使用'**
|
||||
String get welcome_button_text;
|
||||
|
||||
/// No description provided for @login_title.
|
||||
///
|
||||
/// In zh, this message translates to:
|
||||
/// **'欢迎来到FoodCura'**
|
||||
String get login_title;
|
||||
|
||||
/// No description provided for @login_email_hint.
|
||||
///
|
||||
/// In zh, this message translates to:
|
||||
/// **'请输入邮箱'**
|
||||
String get login_email_hint;
|
||||
|
||||
/// No description provided for @login_password_hint.
|
||||
///
|
||||
/// In zh, this message translates to:
|
||||
/// **'请输入密码'**
|
||||
String get login_password_hint;
|
||||
|
||||
/// No description provided for @login_button.
|
||||
///
|
||||
/// In zh, this message translates to:
|
||||
/// **'登录'**
|
||||
String get login_button;
|
||||
|
||||
/// No description provided for @login_tip_start.
|
||||
///
|
||||
/// In zh, this message translates to:
|
||||
/// **'注册或登录即表示您了解并同意'**
|
||||
String get login_tip_start;
|
||||
|
||||
/// No description provided for @login_privacy.
|
||||
///
|
||||
/// In zh, this message translates to:
|
||||
/// **'隐私协议'**
|
||||
String get login_privacy;
|
||||
|
||||
/// No description provided for @login_terms.
|
||||
///
|
||||
/// In zh, this message translates to:
|
||||
/// **'服务条款'**
|
||||
String get login_terms;
|
||||
|
||||
/// No description provided for @login_and.
|
||||
///
|
||||
/// In zh, this message translates to:
|
||||
/// **'和'**
|
||||
String get login_and;
|
||||
|
||||
/// No description provided for @login_other_login.
|
||||
///
|
||||
/// In zh, this message translates to:
|
||||
/// **'其他登录方式'**
|
||||
String get login_other_login;
|
||||
|
||||
/// No description provided for @login_error_text.
|
||||
///
|
||||
/// In zh, this message translates to:
|
||||
/// **'登录失败'**
|
||||
String get login_error_text;
|
||||
|
||||
/// No description provided for @code_title.
|
||||
///
|
||||
/// In zh, this message translates to:
|
||||
/// **'输入验证码'**
|
||||
String get code_title;
|
||||
|
||||
/// No description provided for @code_tip.
|
||||
///
|
||||
/// In zh, this message translates to:
|
||||
/// **'验证码已发送至'**
|
||||
String get code_tip;
|
||||
|
||||
/// No description provided for @code_send_code.
|
||||
///
|
||||
/// In zh, this message translates to:
|
||||
/// **'重新发送'**
|
||||
String get code_send_code;
|
||||
|
||||
/// No description provided for @code_success.
|
||||
///
|
||||
/// In zh, this message translates to:
|
||||
/// **'发送成功'**
|
||||
String get code_success;
|
||||
|
||||
/// No description provided for @code_error.
|
||||
///
|
||||
/// In zh, this message translates to:
|
||||
/// **'验证失败'**
|
||||
String get code_error;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
|
||||
const _AppLocalizationsDelegate();
|
||||
|
||||
@override
|
||||
Future<AppLocalizations> load(Locale locale) {
|
||||
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
|
||||
}
|
||||
|
||||
@override
|
||||
bool isSupported(Locale locale) => <String>['en', 'zh'].contains(locale.languageCode);
|
||||
|
||||
@override
|
||||
bool shouldReload(_AppLocalizationsDelegate old) => false;
|
||||
}
|
||||
|
||||
AppLocalizations lookupAppLocalizations(Locale locale) {
|
||||
// Lookup logic when only language code is specified.
|
||||
switch (locale.languageCode) {
|
||||
case 'en':
|
||||
return AppLocalizationsEn();
|
||||
case 'zh':
|
||||
return AppLocalizationsZh();
|
||||
}
|
||||
|
||||
throw FlutterError(
|
||||
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
|
||||
'an issue with the localizations generation tool. Please file an issue '
|
||||
'on GitHub with a reproducible sample app and the gen-l10n configuration '
|
||||
'that was used.',
|
||||
);
|
||||
}
|
||||
65
lib/l10n/app_localizations_en.dart
Normal file
65
lib/l10n/app_localizations_en.dart
Normal file
@@ -0,0 +1,65 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// The translations for English (`en`).
|
||||
class AppLocalizationsEn extends AppLocalizations {
|
||||
AppLocalizationsEn([String locale = 'en']) : super(locale);
|
||||
|
||||
@override
|
||||
String get welcome_title => 'Clear Ingredients at a Glance';
|
||||
|
||||
@override
|
||||
String get welcome_desc =>
|
||||
'Automatically identify food ingredients and get instant insights on nutrition and potential risks';
|
||||
|
||||
@override
|
||||
String get welcome_button_text => 'Get Started';
|
||||
|
||||
@override
|
||||
String get login_title => '欢迎来到FoodCura';
|
||||
|
||||
@override
|
||||
String get login_email_hint => 'Please enter your email';
|
||||
|
||||
@override
|
||||
String get login_password_hint => 'Please enter your password';
|
||||
|
||||
@override
|
||||
String get login_button => 'Continue';
|
||||
|
||||
@override
|
||||
String get login_tip_start => 'I agree to the';
|
||||
|
||||
@override
|
||||
String get login_privacy => 'Privacy Policy';
|
||||
|
||||
@override
|
||||
String get login_terms => 'Terms';
|
||||
|
||||
@override
|
||||
String get login_and => '&';
|
||||
|
||||
@override
|
||||
String get login_other_login => 'Or';
|
||||
|
||||
@override
|
||||
String get login_error_text => 'Login Failed';
|
||||
|
||||
@override
|
||||
String get code_title => 'Enter Verification Code';
|
||||
|
||||
@override
|
||||
String get code_tip => 'Verification code has been sent to';
|
||||
|
||||
@override
|
||||
String get code_send_code => 'Resend';
|
||||
|
||||
@override
|
||||
String get code_success => 'Sent';
|
||||
|
||||
@override
|
||||
String get code_error => 'Invalid Code';
|
||||
}
|
||||
64
lib/l10n/app_localizations_zh.dart
Normal file
64
lib/l10n/app_localizations_zh.dart
Normal file
@@ -0,0 +1,64 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// The translations for Chinese (`zh`).
|
||||
class AppLocalizationsZh extends AppLocalizations {
|
||||
AppLocalizationsZh([String locale = 'zh']) : super(locale);
|
||||
|
||||
@override
|
||||
String get welcome_title => '成分一目了然';
|
||||
|
||||
@override
|
||||
String get welcome_desc => '自动识别食物成分,营养与风险一手掌握';
|
||||
|
||||
@override
|
||||
String get welcome_button_text => '开始使用';
|
||||
|
||||
@override
|
||||
String get login_title => '欢迎来到FoodCura';
|
||||
|
||||
@override
|
||||
String get login_email_hint => '请输入邮箱';
|
||||
|
||||
@override
|
||||
String get login_password_hint => '请输入密码';
|
||||
|
||||
@override
|
||||
String get login_button => '登录';
|
||||
|
||||
@override
|
||||
String get login_tip_start => '注册或登录即表示您了解并同意';
|
||||
|
||||
@override
|
||||
String get login_privacy => '隐私协议';
|
||||
|
||||
@override
|
||||
String get login_terms => '服务条款';
|
||||
|
||||
@override
|
||||
String get login_and => '和';
|
||||
|
||||
@override
|
||||
String get login_other_login => '其他登录方式';
|
||||
|
||||
@override
|
||||
String get login_error_text => '登录失败';
|
||||
|
||||
@override
|
||||
String get code_title => '输入验证码';
|
||||
|
||||
@override
|
||||
String get code_tip => '验证码已发送至';
|
||||
|
||||
@override
|
||||
String get code_send_code => '重新发送';
|
||||
|
||||
@override
|
||||
String get code_success => '发送成功';
|
||||
|
||||
@override
|
||||
String get code_error => '验证失败';
|
||||
}
|
||||
19
lib/l10n/arb/app_en.arb
Normal file
19
lib/l10n/arb/app_en.arb
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"welcome_title": "Clear Ingredients at a Glance",
|
||||
"welcome_desc": "Automatically identify food ingredients and get instant insights on nutrition and potential risks",
|
||||
"welcome_button_text": "Get Started",
|
||||
"login_email_hint": "Please enter your email",
|
||||
"login_password_hint": "Please enter your password",
|
||||
"login_button": "Continue",
|
||||
"login_tip_start": "I agree to the",
|
||||
"login_privacy": "Privacy Policy",
|
||||
"login_terms": "Terms",
|
||||
"login_and": "&",
|
||||
"login_other_login": "Or",
|
||||
"login_error_text": "Login Failed",
|
||||
"code_title": "Enter Verification Code",
|
||||
"code_tip": "Verification code has been sent to",
|
||||
"code_send_code": "Resend",
|
||||
"code_success": "Sent",
|
||||
"code_error": "Invalid Code"
|
||||
}
|
||||
20
lib/l10n/arb/app_zh.arb
Normal file
20
lib/l10n/arb/app_zh.arb
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"welcome_title": "成分一目了然",
|
||||
"welcome_desc": "自动识别食物成分,营养与风险一手掌握",
|
||||
"welcome_button_text": "开始使用",
|
||||
"login_title": "欢迎来到FoodCura",
|
||||
"login_email_hint": "请输入邮箱",
|
||||
"login_password_hint": "请输入密码",
|
||||
"login_button": "登录",
|
||||
"login_tip_start": "注册或登录即表示您了解并同意",
|
||||
"login_privacy": "隐私协议",
|
||||
"login_terms": "服务条款",
|
||||
"login_and": "和",
|
||||
"login_other_login": "其他登录方式",
|
||||
"login_error_text": "登录失败",
|
||||
"code_title": "输入验证码",
|
||||
"code_tip": "验证码已发送至",
|
||||
"code_send_code": "重新发送",
|
||||
"code_success": "发送成功",
|
||||
"code_error": "验证失败"
|
||||
}
|
||||
56
lib/l10n/arb/i18n.yaml
Normal file
56
lib/l10n/arb/i18n.yaml
Normal file
@@ -0,0 +1,56 @@
|
||||
welcome:
|
||||
title:
|
||||
zh: 成分一目了然
|
||||
en: Clear Ingredients at a Glance
|
||||
desc:
|
||||
zh: 自动识别食物成分,营养与风险一手掌握
|
||||
en: Automatically identify food ingredients and get instant insights on nutrition and potential risks
|
||||
button_text:
|
||||
zh: 开始使用
|
||||
en: Get Started
|
||||
login:
|
||||
title:
|
||||
zh: 欢迎来到FoodCura
|
||||
email_hint:
|
||||
zh: 请输入邮箱
|
||||
en: Please enter your email
|
||||
password_hint:
|
||||
zh: 请输入密码
|
||||
en: Please enter your password
|
||||
button:
|
||||
zh: 登录
|
||||
en: Continue
|
||||
tip_start:
|
||||
zh: 注册或登录即表示您了解并同意
|
||||
en: I agree to the
|
||||
privacy:
|
||||
zh: 隐私协议
|
||||
en: Privacy Policy
|
||||
terms:
|
||||
zh: 服务条款
|
||||
en: Terms
|
||||
and:
|
||||
zh: 和
|
||||
en: "&"
|
||||
other_login:
|
||||
zh: 其他登录方式
|
||||
en: Or
|
||||
error_text:
|
||||
zh: 登录失败
|
||||
en: Login Failed
|
||||
code:
|
||||
title:
|
||||
zh: 输入验证码
|
||||
en: Enter Verification Code
|
||||
tip:
|
||||
zh: 验证码已发送至
|
||||
en: Verification code has been sent to
|
||||
send_code:
|
||||
zh: 重新发送
|
||||
en: Resend
|
||||
success:
|
||||
zh: 发送成功
|
||||
en: Sent
|
||||
error:
|
||||
zh: 验证失败
|
||||
en: Invalid Code
|
||||
12
lib/l10n/l10n.dart
Normal file
12
lib/l10n/l10n.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:food_health/l10n/app_localizations.dart';
|
||||
|
||||
class L10n {
|
||||
static AppLocalizations? _instance;
|
||||
|
||||
static void init(BuildContext context) {
|
||||
_instance = AppLocalizations.of(context);
|
||||
}
|
||||
|
||||
static AppLocalizations get of => _instance!;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import 'package:food_health/providers/user_store.dart';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:food_health/stores/app_store.dart';
|
||||
import 'package:food_health/stores/setting_store.dart';
|
||||
import 'package:food_health/stores/user_store.dart';
|
||||
import 'package:food_health/router/routes.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
@@ -6,7 +11,9 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'config/theme/theme.dart';
|
||||
import 'providers/app_store.dart';
|
||||
import 'config/theme/themes/light_theme.dart';
|
||||
import 'l10n/app_localizations.dart';
|
||||
import 'l10n/l10n.dart';
|
||||
|
||||
void main() {
|
||||
runApp(
|
||||
@@ -14,6 +21,7 @@ void main() {
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (context) => AppStore()),
|
||||
ChangeNotifierProvider(create: (context) => UserStore()),
|
||||
ChangeNotifierProvider(create: (_) => SettingStore()),
|
||||
],
|
||||
child: MyApp(),
|
||||
),
|
||||
@@ -25,32 +33,28 @@ class MyApp extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//语言化
|
||||
L10n.init(context);
|
||||
|
||||
final settingStore = context.watch<SettingStore>();
|
||||
return ScreenUtilInit(
|
||||
designSize: const Size(375, 694),
|
||||
useInheritedMediaQuery: true,
|
||||
child: MaterialApp.router(
|
||||
debugShowCheckedModeBanner: false,
|
||||
routerConfig: goRouter,
|
||||
localizationsDelegates: [],
|
||||
themeMode: ThemeMode.light,
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: scheme,
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: scheme.primary,
|
||||
foregroundColor: scheme.onPrimary,
|
||||
),
|
||||
),
|
||||
textTheme: textTheme,
|
||||
scaffoldBackgroundColor: Color(0xffFAFAFE),
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: scheme.surface,
|
||||
scrolledUnderElevation: 0,
|
||||
titleTextStyle: textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
builder: EasyLoading.init(),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: [
|
||||
Locale('en'),
|
||||
Locale('zh', 'CN'),
|
||||
],
|
||||
theme: AppTheme.createTheme(LightTheme()),
|
||||
locale: settingStore.locale,
|
||||
builder: (context, child) {
|
||||
// ⚡️ 在这里就能拿到 AppLocalizations
|
||||
L10n.init(context);
|
||||
return EasyLoading.init()(context, child);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
import 'package:food_health/api/endpoints/user_api.dart';
|
||||
import 'package:food_health/api/network/safe.dart';
|
||||
import 'package:food_health/page/system/login/widget/widget.dart';
|
||||
import 'package:food_health/router/config/route_paths.dart';
|
||||
import 'package:food_health/widgets/ui_kit/button/custom_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../../providers/app_store.dart';
|
||||
|
||||
class LoginCodePage extends StatefulWidget {
|
||||
final String email;
|
||||
final String password;
|
||||
|
||||
const LoginCodePage({super.key, required this.email, required this.password});
|
||||
|
||||
@override
|
||||
State<LoginCodePage> createState() => _LoginCodePageState();
|
||||
}
|
||||
|
||||
class _LoginCodePageState extends State<LoginCodePage> {
|
||||
final _codeController = TextEditingController();
|
||||
var _subLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_handSendCode();
|
||||
}
|
||||
|
||||
///发送验证码
|
||||
void _handSendCode() {
|
||||
sendEmailCodeApi(widget.email);
|
||||
EasyLoading.showSuccess("Send success");
|
||||
}
|
||||
|
||||
///提交
|
||||
void _handSubmit() async {
|
||||
if (_codeController.text.isNotEmpty) {
|
||||
setState(() {
|
||||
_subLoading = true;
|
||||
});
|
||||
var res = await safeRequest(
|
||||
registerApi(
|
||||
widget.email,
|
||||
widget.password,
|
||||
_codeController.text,
|
||||
),
|
||||
onError: (error) {
|
||||
setState(() {
|
||||
_subLoading = false;
|
||||
});
|
||||
},
|
||||
);
|
||||
var appStore = context.read<AppStore>();
|
||||
await appStore.setInfo(res);
|
||||
context.go(RoutePaths.layout);
|
||||
setState(() {
|
||||
_subLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.only(left: 20, right: 20, top: 40),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"Check your inbox",
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 20, bottom: 40),
|
||||
child: Text(
|
||||
"Enter the verification code we just sent to ${widget.email}.",
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.labelMedium,
|
||||
),
|
||||
),
|
||||
InputBox(hintText: "Code", controller: _codeController),
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 20),
|
||||
child: CustomButton(
|
||||
loading: _subLoading,
|
||||
onPressed: _handSubmit,
|
||||
child: Text("Continue"),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 20),
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
_handSendCode();
|
||||
},
|
||||
child: Text(
|
||||
"Resend code",
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,290 +0,0 @@
|
||||
import 'package:food_health/api/endpoints/user_api.dart';
|
||||
import 'package:food_health/data/models/other_login_type.dart';
|
||||
import 'package:food_health/router/config/route_paths.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_sign_in/google_sign_in.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:remixicon/remixicon.dart';
|
||||
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
|
||||
import '../../../providers/app_store.dart';
|
||||
import '../../../utils/common.dart';
|
||||
import '../../../widgets/common/app_backend.dart';
|
||||
import '../../../widgets/ui_kit/button/custom_button.dart';
|
||||
import 'widget/agreement_box.dart';
|
||||
import 'widget/widget.dart';
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
|
||||
@override
|
||||
State<LoginPage> createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends State<LoginPage> {
|
||||
var _subLoading = false;
|
||||
|
||||
///协议
|
||||
bool _agree = false;
|
||||
|
||||
///谷歌登陆
|
||||
final GoogleSignIn _googleSignIn = GoogleSignIn.instance;
|
||||
|
||||
///邮箱输入框
|
||||
final TextEditingController _emailController = TextEditingController(text: "");
|
||||
final TextEditingController _passwordController = TextEditingController(text: "");
|
||||
|
||||
//显示密码
|
||||
var _hidePassword = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_ensureNetworkPermission();
|
||||
_initGoogleSign();
|
||||
}
|
||||
|
||||
/// 预触发 iOS 网络权限弹窗
|
||||
Future<bool> _ensureNetworkPermission() async {
|
||||
try {
|
||||
await Dio().get(
|
||||
'https://captive.apple.com/hotspot-detect.html',
|
||||
options: Options(
|
||||
sendTimeout: const Duration(seconds: 3),
|
||||
receiveTimeout: const Duration(seconds: 3),
|
||||
headers: {'Cache-Control': 'no-cache'},
|
||||
),
|
||||
);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void _initGoogleSign() {
|
||||
if (isAndroid()) {
|
||||
_googleSignIn.initialize(
|
||||
clientId: null,
|
||||
serverClientId: "512878764950-0bsl98c4q4p695mlmfn35qhmr2ld5n0o.apps.googleusercontent.com",
|
||||
);
|
||||
} else {
|
||||
_googleSignIn.initialize(
|
||||
clientId: "512878764950-1ke7slf0c6dlmchnuk0fqh3fe954gcf2.apps.googleusercontent.com",
|
||||
serverClientId: "512878764950-0bsl98c4q4p695mlmfn35qhmr2ld5n0o.apps.googleusercontent.com",
|
||||
);
|
||||
}
|
||||
|
||||
_googleSignIn.authenticationEvents
|
||||
.listen((_) {
|
||||
print("登陆成功");
|
||||
})
|
||||
.onError((error) {
|
||||
print('登录错误: $error');
|
||||
});
|
||||
}
|
||||
|
||||
///谷歌登录
|
||||
void _handleGoogleSignIn() async {
|
||||
if (!_agree) {
|
||||
EasyLoading.showToast('Please read and agree to the terms first.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 如果用户未登录,则启动标准的 Google 登录
|
||||
if (_googleSignIn.supportsAuthenticate()) {
|
||||
// 使用 authenticate() 进行认证
|
||||
GoogleSignInAccount? user = await _googleSignIn.authenticate();
|
||||
var auth = user.authentication;
|
||||
|
||||
// var res = await Dio().get("https://oauth2.googleapis.com/tokeninfo?id_token=${auth.idToken}");
|
||||
//登陆
|
||||
EasyLoading.show(status: "Logging in...");
|
||||
var res = await thirdLoginApi(auth.idToken!, OtherLoginType.google);
|
||||
EasyLoading.dismiss();
|
||||
_onLogin(res);
|
||||
}
|
||||
// } catch (e) {
|
||||
// if (e is GoogleSignInException) {
|
||||
// if (e.code == GoogleSignInExceptionCode.canceled) {
|
||||
// // 用户取消登录
|
||||
// print("User canceled login.");
|
||||
// } else {
|
||||
// // 其他错误
|
||||
// print("Google Sign-In error: $e");
|
||||
// }
|
||||
// } else {
|
||||
// print("Unknown error: $e");
|
||||
// }
|
||||
// }
|
||||
} catch (e) {
|
||||
EasyLoading.showError("Login failed");
|
||||
print("登录错误: $e");
|
||||
}
|
||||
}
|
||||
|
||||
///apple登录
|
||||
void _handAppleSignIn() async {
|
||||
if (!_agree) {
|
||||
EasyLoading.showToast('Please read and agree to the terms first.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final credential = await SignInWithApple.getAppleIDCredential(
|
||||
scopes: [
|
||||
AppleIDAuthorizationScopes.email,
|
||||
AppleIDAuthorizationScopes.fullName,
|
||||
],
|
||||
);
|
||||
EasyLoading.show(status: "Logging in...");
|
||||
var res = await thirdLoginApi(credential.identityToken!, OtherLoginType.apple);
|
||||
EasyLoading.dismiss();
|
||||
_onLogin(res);
|
||||
print('Apple Credential: ${credential.identityToken}');
|
||||
print('Apple Email: ${credential.email}');
|
||||
} catch (e) {
|
||||
print('Error during Apple sign-in: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _handSubmit() async {
|
||||
if (!_agree) {
|
||||
EasyLoading.showToast('Please read and agree to the terms first.');
|
||||
return;
|
||||
}
|
||||
if (_emailController.text.isEmpty) {
|
||||
//请输入邮箱
|
||||
EasyLoading.showError("Please enter your email");
|
||||
return;
|
||||
} else if (_passwordController.text.isEmpty) {
|
||||
EasyLoading.showError("Please enter your Password");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setState(() {
|
||||
_subLoading = true;
|
||||
});
|
||||
var isRegister = await checkRegisterApi(_emailController.text);
|
||||
if (!isRegister) {
|
||||
context.push(
|
||||
RoutePaths.loginCode,
|
||||
extra: {
|
||||
"email": _emailController.text,
|
||||
"password": _passwordController.text,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
var res = await loginApi(_emailController.text, _passwordController.text);
|
||||
_onLogin(res);
|
||||
}
|
||||
setState(() {
|
||||
_subLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_subLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
///登陆的操作
|
||||
void _onLogin(dynamic res) {
|
||||
var appStore = context.read<AppStore>();
|
||||
appStore.setInfo(res);
|
||||
context.go(RoutePaths.layout);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: AppBackend(
|
||||
child: Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: SafeArea(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.only(
|
||||
top: 0.07.sh,
|
||||
left: 20,
|
||||
right: 20,
|
||||
),
|
||||
child: ListView(
|
||||
children: [
|
||||
LogoBox(),
|
||||
PageHeader(),
|
||||
InputBox(
|
||||
hintText: "Email",
|
||||
controller: _emailController,
|
||||
),
|
||||
SizedBox(height: 15),
|
||||
InputBox(
|
||||
obscureText: _hidePassword,
|
||||
hintText: "Password",
|
||||
controller: _passwordController,
|
||||
suffix: InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_hidePassword = !_hidePassword;
|
||||
});
|
||||
},
|
||||
child: Icon(
|
||||
_hidePassword ? RemixIcons.eye_off_fill : RemixIcons.eye_fill,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 20),
|
||||
height: 45,
|
||||
child: CustomButton(
|
||||
loading: _subLoading,
|
||||
round: false,
|
||||
onPressed: _handSubmit,
|
||||
child: Text("Continue"),
|
||||
),
|
||||
),
|
||||
LoginDivider(),
|
||||
OtherButton(
|
||||
title: "Continue with Google",
|
||||
icon: "assets/image/google.png",
|
||||
onTap: () {
|
||||
_handleGoogleSignIn();
|
||||
},
|
||||
),
|
||||
SizedBox(height: 15),
|
||||
OtherButton(
|
||||
title: "Continue with Apple",
|
||||
icon: "assets/image/apple.png",
|
||||
onTap: () {
|
||||
_handAppleSignIn();
|
||||
},
|
||||
),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
margin: EdgeInsets.only(top: 40),
|
||||
alignment: Alignment.center,
|
||||
child: AgreementBox(
|
||||
checked: _agree,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_agree = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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"},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
@@ -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];
|
||||
223
lib/pages/system/code/login_code_page.dart
Normal file
223
lib/pages/system/code/login_code_page.dart
Normal file
@@ -0,0 +1,223 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:food_health/api/endpoints/user_api.dart';
|
||||
import 'package:food_health/api/network/safe.dart';
|
||||
import 'package:food_health/l10n/l10n.dart';
|
||||
import 'package:food_health/router/config/route_paths.dart';
|
||||
import 'package:food_health/stores/app_store.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class LoginCodePage extends StatefulWidget {
|
||||
final String email;
|
||||
final String password;
|
||||
|
||||
const LoginCodePage({super.key, required this.email, required this.password});
|
||||
|
||||
@override
|
||||
State<LoginCodePage> createState() => _LoginCodePageState();
|
||||
}
|
||||
|
||||
class _LoginCodePageState extends State<LoginCodePage> {
|
||||
final List<FocusNode> _focusNodes = List.generate(4, (_) => FocusNode());
|
||||
final List<TextEditingController> _controllers = List.generate(
|
||||
4,
|
||||
(_) => TextEditingController(),
|
||||
);
|
||||
|
||||
//倒计时
|
||||
int _count = 60;
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_handSendCode();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
_handClear();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
///小输入框改变时
|
||||
void _onChanged(String value, int index) async {
|
||||
//一键复制
|
||||
if (value.length == 4) {
|
||||
_handlePaste(value);
|
||||
}
|
||||
//提交
|
||||
if (value.isNotEmpty && index == 3) {
|
||||
_handSubmit();
|
||||
return;
|
||||
}
|
||||
// 自动跳到下一格
|
||||
if (value.length == 1 && index < 3) {
|
||||
_focusNodes[index + 1].requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
void _handlePaste(String pastedText) {
|
||||
// 只取前4位数字
|
||||
final digits = pastedText.replaceAll(RegExp(r'[^0-9]'), '');
|
||||
for (int i = 0; i < 4; i++) {
|
||||
_controllers[i].text = i < digits.length ? digits[i] : '';
|
||||
}
|
||||
if (digits.length >= 4) {
|
||||
_focusNodes[3].requestFocus();
|
||||
_handSubmit();
|
||||
} else if (digits.isNotEmpty) {
|
||||
_focusNodes[digits.length].requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
///删除键
|
||||
void _onDelete(KeyEvent event, int index) {
|
||||
if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.backspace) {
|
||||
final currentController = _controllers[index];
|
||||
if (currentController.text.isEmpty && index > 0) {
|
||||
_focusNodes[index - 1].requestFocus();
|
||||
_controllers[index - 1].clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///发送验证码
|
||||
void _handSendCode() {
|
||||
if (_count != 60) {
|
||||
return;
|
||||
}
|
||||
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
|
||||
setState(() {
|
||||
_count--;
|
||||
});
|
||||
if (_count == 0) {
|
||||
setState(() {
|
||||
_count = 60;
|
||||
});
|
||||
timer.cancel();
|
||||
}
|
||||
});
|
||||
sendEmailCodeApi(widget.email);
|
||||
EasyLoading.showToast(L10n.of.code_success);
|
||||
}
|
||||
|
||||
///提交
|
||||
void _handSubmit() async {
|
||||
String code = _controllers.map((controller) => controller.text).join();
|
||||
if (code.length == 4) {
|
||||
EasyLoading.show();
|
||||
var res = await safeRequest(
|
||||
registerApi(
|
||||
widget.email,
|
||||
widget.password,
|
||||
code,
|
||||
),
|
||||
onError: (error) {
|
||||
_handClear();
|
||||
EasyLoading.showToast(L10n.of.code_error);
|
||||
},
|
||||
);
|
||||
var appStore = context.read<AppStore>();
|
||||
await appStore.setInfo(res);
|
||||
context.go(RoutePaths.layout);
|
||||
}
|
||||
}
|
||||
|
||||
///清空
|
||||
void _handClear() {
|
||||
for (var controller in _controllers) {
|
||||
controller.clear();
|
||||
}
|
||||
_focusNodes.first.requestFocus();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: ListView(
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.all(20),
|
||||
children: [
|
||||
Container(
|
||||
margin: EdgeInsets.only(bottom: 20),
|
||||
child: Text(L10n.of.code_title, style: Theme.of(context).textTheme.titleLarge),
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.only(bottom: 60),
|
||||
child: Text(
|
||||
"${L10n.of.code_tip} ${widget.email}",
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
spacing: 20,
|
||||
children: List.generate(4, (index) {
|
||||
return Expanded(
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: KeyboardListener(
|
||||
focusNode: FocusNode(),
|
||||
onKeyEvent: (event) {
|
||||
_onDelete(event, index);
|
||||
},
|
||||
child: TextField(
|
||||
controller: _controllers[index],
|
||||
focusNode: _focusNodes[index],
|
||||
textAlign: TextAlign.center,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 4,
|
||||
style: TextStyle(fontSize: 32),
|
||||
decoration: InputDecoration(
|
||||
counterText: "",
|
||||
border: InputBorder.none,
|
||||
isCollapsed: true,
|
||||
),
|
||||
onChanged: (value) {
|
||||
_onChanged(value, index);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 30),
|
||||
child: Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: _handSendCode,
|
||||
child: Visibility(
|
||||
visible: _count != 60,
|
||||
replacement: Text(
|
||||
L10n.of.code_send_code,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
child: Text(
|
||||
"${L10n.of.code_send_code}(${_count}s)",
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
69
lib/pages/system/intro/intro_page.dart
Normal file
69
lib/pages/system/intro/intro_page.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:food_health/l10n/l10n.dart';
|
||||
import 'package:food_health/router/config/route_paths.dart';
|
||||
import 'package:food_health/widgets/ui_kit/button/app_button.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class IntroPage extends StatefulWidget {
|
||||
const IntroPage({super.key});
|
||||
|
||||
@override
|
||||
State<IntroPage> createState() => _IntroPageState();
|
||||
}
|
||||
|
||||
class _IntroPageState extends State<IntroPage> {
|
||||
void _onTap() {
|
||||
context.go(RoutePaths.login);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(50),
|
||||
child: Image.asset("assets/image/bg/intro_bg.png"),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
margin: EdgeInsets.only(bottom: 20),
|
||||
child: Text(
|
||||
L10n.of.welcome_title,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
L10n.of.welcome_desc,
|
||||
style: Theme.of(context).textTheme.labelMedium,
|
||||
),
|
||||
Container(
|
||||
height: 50,
|
||||
margin: EdgeInsets.only(top: 20),
|
||||
child: AppButton(
|
||||
onPressed: _onTap,
|
||||
child: Text(L10n.of.welcome_button_text),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
289
lib/pages/system/login/login_page.dart
Normal file
289
lib/pages/system/login/login_page.dart
Normal file
@@ -0,0 +1,289 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:food_health/api/endpoints/user_api.dart';
|
||||
import 'package:food_health/data/models/other_login_type.dart';
|
||||
import 'package:food_health/l10n/l10n.dart';
|
||||
import 'package:food_health/pages/system/login/widgets/login_input.dart';
|
||||
import 'package:food_health/router/config/route_paths.dart';
|
||||
import 'package:food_health/stores/app_store.dart';
|
||||
import 'package:food_health/widgets/ui_kit/button/app_button.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_sign_in/google_sign_in.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:remixicon/remixicon.dart';
|
||||
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
|
||||
|
||||
import '../../../utils/common.dart';
|
||||
import 'widgets/login_agree.dart';
|
||||
import 'widgets/login_other.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
|
||||
@override
|
||||
State<LoginPage> createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends State<LoginPage> {
|
||||
Logger logger = Logger();
|
||||
|
||||
var _subLoading = false;
|
||||
|
||||
///邮箱输入框
|
||||
final TextEditingController _emailController = TextEditingController();
|
||||
final TextEditingController _passwordController = TextEditingController();
|
||||
|
||||
//显示密码
|
||||
bool _hidePassword = true;
|
||||
|
||||
///谷歌登陆
|
||||
final GoogleSignIn _googleSignIn = GoogleSignIn.instance;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_emailController.addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
_passwordController.addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
_ensureNetworkPermission();
|
||||
_initGoogleSign();
|
||||
}
|
||||
|
||||
/// 预触发 iOS 网络权限弹窗
|
||||
Future<bool> _ensureNetworkPermission() async {
|
||||
try {
|
||||
await Dio().get(
|
||||
'https://captive.apple.com/hotspot-detect.html',
|
||||
options: Options(
|
||||
sendTimeout: const Duration(seconds: 3),
|
||||
receiveTimeout: const Duration(seconds: 3),
|
||||
headers: {'Cache-Control': 'no-cache'},
|
||||
),
|
||||
);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void _initGoogleSign() {
|
||||
if (isAndroid()) {
|
||||
_googleSignIn.initialize(
|
||||
clientId: null,
|
||||
serverClientId:
|
||||
"512878764950-0bsl98c4q4p695mlmfn35qhmr2ld5n0o.apps.googleusercontent.com",
|
||||
);
|
||||
} else {
|
||||
_googleSignIn.initialize(
|
||||
clientId: "512878764950-1ke7slf0c6dlmchnuk0fqh3fe954gcf2.apps.googleusercontent.com",
|
||||
serverClientId:
|
||||
"512878764950-0bsl98c4q4p695mlmfn35qhmr2ld5n0o.apps.googleusercontent.com",
|
||||
);
|
||||
}
|
||||
|
||||
_googleSignIn.authenticationEvents
|
||||
.listen((_) {
|
||||
logger.d("登陆成功");
|
||||
})
|
||||
.onError((error) {
|
||||
logger.e("登陆错误: $error");
|
||||
});
|
||||
}
|
||||
|
||||
///谷歌登录
|
||||
void _handGoogleSignIn() async {
|
||||
try {
|
||||
// 如果用户未登录,则启动标准的 Google 登录
|
||||
if (_googleSignIn.supportsAuthenticate()) {
|
||||
// 使用 authenticate() 进行认证
|
||||
GoogleSignInAccount? user = await _googleSignIn.authenticate();
|
||||
var auth = user.authentication;
|
||||
|
||||
//登陆
|
||||
EasyLoading.show();
|
||||
var res = await thirdLoginApi(auth.idToken!, OtherLoginType.google);
|
||||
EasyLoading.dismiss();
|
||||
_onLogin(res);
|
||||
}
|
||||
} catch (e) {
|
||||
EasyLoading.showError(L10n.of.login_error_text);
|
||||
logger.e("登录错误: $e");
|
||||
}
|
||||
}
|
||||
|
||||
///apple登录
|
||||
void _handAppleSignIn() async {
|
||||
try {
|
||||
final credential = await SignInWithApple.getAppleIDCredential(
|
||||
scopes: [
|
||||
AppleIDAuthorizationScopes.email,
|
||||
AppleIDAuthorizationScopes.fullName,
|
||||
],
|
||||
);
|
||||
EasyLoading.show();
|
||||
var res = await thirdLoginApi(credential.identityToken!, OtherLoginType.apple);
|
||||
EasyLoading.dismiss();
|
||||
_onLogin(res);
|
||||
logger.d('Apple Credential: ${credential.identityToken}');
|
||||
logger.d('Apple Email: ${credential.email}');
|
||||
} catch (e) {
|
||||
logger.e("登录错误: $e");
|
||||
}
|
||||
}
|
||||
|
||||
void _handSubmit() async {
|
||||
if (_emailController.text.isEmpty) {
|
||||
//请输入邮箱
|
||||
EasyLoading.showToast(L10n.of.login_email_hint);
|
||||
return;
|
||||
} else if (_passwordController.text.isEmpty) {
|
||||
EasyLoading.showToast(L10n.of.login_password_hint);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setState(() {
|
||||
_subLoading = true;
|
||||
});
|
||||
var isRegister = await checkRegisterApi(_emailController.text);
|
||||
if (!isRegister && mounted) {
|
||||
context.push(
|
||||
RoutePaths.loginCode,
|
||||
extra: {
|
||||
"email": _emailController.text,
|
||||
"password": _passwordController.text,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
var res = await loginApi(_emailController.text, _passwordController.text);
|
||||
_onLogin(res);
|
||||
}
|
||||
setState(() {
|
||||
_subLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_subLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
///登陆的操作
|
||||
void _onLogin(dynamic res) {
|
||||
var appStore = context.read<AppStore>();
|
||||
appStore.setInfo(res);
|
||||
context.go(RoutePaths.layout);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: SafeArea(
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(
|
||||
left: 20,
|
||||
right: 20,
|
||||
top: 0.08.sh,
|
||||
bottom: 40,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
margin: EdgeInsets.only(bottom: 40),
|
||||
child: Text(
|
||||
L10n.of.login_title,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
LoginInput(
|
||||
hintText: L10n.of.login_email_hint,
|
||||
controller: _emailController,
|
||||
suffix: Visibility(
|
||||
visible: _emailController.text.isNotEmpty,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
_emailController.clear();
|
||||
},
|
||||
child: Icon(
|
||||
RemixIcons.close_circle_fill,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
LoginInput(
|
||||
hintText: L10n.of.login_password_hint,
|
||||
controller: _passwordController,
|
||||
obscureText: _hidePassword,
|
||||
suffix: InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_hidePassword = !_hidePassword;
|
||||
});
|
||||
},
|
||||
child: Icon(
|
||||
_hidePassword ? RemixIcons.eye_off_fill : RemixIcons.eye_fill,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 40),
|
||||
height: 50,
|
||||
child: AppButton(
|
||||
disabled: _emailController.text.isEmpty || _passwordController.text.isEmpty,
|
||||
loading: _subLoading,
|
||||
onPressed: _handSubmit,
|
||||
child: Text(L10n.of.login_button),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
margin: EdgeInsets.only(top: 20),
|
||||
alignment: Alignment.center,
|
||||
child: LoginAgree(),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
LoginDivider(),
|
||||
Row(
|
||||
spacing: 20,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
OtherButton(
|
||||
onTap: () {
|
||||
_handGoogleSignIn();
|
||||
},
|
||||
icon: "assets/image/google.png",
|
||||
),
|
||||
OtherButton(
|
||||
onTap: () {
|
||||
_handAppleSignIn();
|
||||
},
|
||||
icon: "assets/image/apple.png",
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
50
lib/pages/system/login/widgets/login_agree.dart
Normal file
50
lib/pages/system/login/widgets/login_agree.dart
Normal file
@@ -0,0 +1,50 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:food_health/l10n/l10n.dart';
|
||||
import 'package:food_health/router/config/route_paths.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class LoginAgree extends StatelessWidget {
|
||||
const LoginAgree({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
style: Theme.of(context).textTheme.labelMedium,
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "${L10n.of.login_tip_start} ",
|
||||
),
|
||||
TextSpan(
|
||||
text: L10n.of.login_terms,
|
||||
style: TextStyle(color: Theme.of(context).primaryColor),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => context.push(
|
||||
RoutePaths.agreement,
|
||||
extra: {
|
||||
"title": "Terms of Service",
|
||||
"url": "https://support.curain.ai/privacy/foodcura/terms_service.html",
|
||||
},
|
||||
),
|
||||
),
|
||||
TextSpan(text: " ${L10n.of.login_and} "),
|
||||
TextSpan(
|
||||
text: L10n.of.login_privacy,
|
||||
style: TextStyle(color: Theme.of(context).primaryColor),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => context.push(
|
||||
RoutePaths.agreement,
|
||||
extra: {
|
||||
"title": "Privacy",
|
||||
"url": "https://support.curain.ai/privacy/foodcura/privacy_policy.html",
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
46
lib/pages/system/login/widgets/login_input.dart
Normal file
46
lib/pages/system/login/widgets/login_input.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class LoginInput extends StatelessWidget {
|
||||
final bool obscureText;
|
||||
final String hintText;
|
||||
final TextEditingController controller;
|
||||
final Widget? suffix;
|
||||
|
||||
const LoginInput({
|
||||
super.key,
|
||||
this.obscureText = false,
|
||||
required this.hintText,
|
||||
required this.controller,
|
||||
this.suffix,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextField(
|
||||
controller: controller,
|
||||
obscureText: obscureText,
|
||||
maxLength: 100,
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
hintStyle: Theme.of(context).textTheme.labelLarge?.copyWith(fontSize: 16),
|
||||
counterText: '',
|
||||
filled: true,
|
||||
fillColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(60),
|
||||
borderSide: BorderSide.none, // 去掉边框
|
||||
),
|
||||
isCollapsed: true,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 14, horizontal: 20),
|
||||
suffixIcon: Container(
|
||||
padding: EdgeInsets.only(right: 20),
|
||||
child: suffix,
|
||||
),
|
||||
suffixIconConstraints: BoxConstraints(
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
67
lib/pages/system/login/widgets/login_other.dart
Normal file
67
lib/pages/system/login/widgets/login_other.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:food_health/l10n/l10n.dart';
|
||||
|
||||
///分割线
|
||||
class LoginDivider extends StatelessWidget {
|
||||
const LoginDivider({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: EdgeInsets.only(top: 20, bottom: 20),
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
L10n.of.login_other_login,
|
||||
style: Theme.of(context).textTheme.labelMedium,
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class OtherButton extends StatelessWidget {
|
||||
final Function() onTap;
|
||||
final String icon;
|
||||
|
||||
const OtherButton({
|
||||
super.key,
|
||||
required this.onTap,
|
||||
required this.icon,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: 45,
|
||||
padding: EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: Image.asset(icon),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:food_health/providers/app_store.dart';
|
||||
import 'package:food_health/stores/app_store.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:food_health/providers/user_store.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -51,13 +50,6 @@ class _SplashPageState extends State<SplashPage> {
|
||||
"assets/image/logo.png",
|
||||
width: 68.w,
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 16),
|
||||
child: Text(
|
||||
"Demacare",
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
26
lib/pages/system/test_page.dart
Normal file
26
lib/pages/system/test_page.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:food_health/l10n/app_localizations.dart';
|
||||
|
||||
class TestPage extends StatefulWidget {
|
||||
const TestPage({super.key});
|
||||
|
||||
@override
|
||||
State<TestPage> createState() => _TestPageState();
|
||||
}
|
||||
|
||||
class _TestPageState extends State<TestPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: Text(
|
||||
AppLocalizations.of(context)?.login_title ?? "",
|
||||
style: TextStyle(
|
||||
fontSize: 50,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,9 @@ class RoutePaths {
|
||||
///闪烁页
|
||||
static const splash = "/";
|
||||
|
||||
///引导页
|
||||
static const intro = "/intro";
|
||||
|
||||
///协议页
|
||||
static const agreement = "/agreement";
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'package:food_health/layout/layout_page.dart';
|
||||
import 'package:food_health/page/system/login/login_code_page.dart';
|
||||
import 'package:food_health/page/system/splash/splash_page.dart';
|
||||
import 'package:food_health/pages/system/code/login_code_page.dart';
|
||||
import 'package:food_health/pages/system/intro/intro_page.dart';
|
||||
import 'package:food_health/pages/system/splash/splash_page.dart';
|
||||
import 'package:food_health/pages/system/test_page.dart';
|
||||
import 'package:food_health/router/config/route_paths.dart';
|
||||
|
||||
import '../../page/system/agree/agree_page.dart';
|
||||
import '../../page/system/login/login_page.dart';
|
||||
import '../../pages/system/agree/agree_page.dart';
|
||||
import '../../pages/system/login/login_page.dart';
|
||||
import '../config/route_type.dart';
|
||||
|
||||
List<RouteType> baseRoutes = [
|
||||
@@ -30,6 +32,12 @@ List<RouteType> baseRoutes = [
|
||||
return LoginPage();
|
||||
},
|
||||
),
|
||||
RouteType(
|
||||
path: RoutePaths.intro,
|
||||
child: (state) {
|
||||
return IntroPage();
|
||||
},
|
||||
),
|
||||
RouteType(
|
||||
path: RoutePaths.loginCode,
|
||||
child: (state) {
|
||||
@@ -46,4 +54,10 @@ List<RouteType> baseRoutes = [
|
||||
return LayoutPage();
|
||||
},
|
||||
),
|
||||
RouteType(
|
||||
path: "/test",
|
||||
child: (state) {
|
||||
return TestPage();
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ List<RouteBase> routes = routeConfigs.map((item) {
|
||||
|
||||
//变量命名
|
||||
GoRouter goRouter = GoRouter(
|
||||
initialLocation: RoutePaths.splash,
|
||||
initialLocation: RoutePaths.intro,
|
||||
routes: routes,
|
||||
navigatorKey: navigatorKey,
|
||||
);
|
||||
|
||||
20
lib/stores/setting_store.dart
Normal file
20
lib/stores/setting_store.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
class SettingStore extends ChangeNotifier {
|
||||
late Locale _locale;
|
||||
|
||||
Locale get locale => _locale;
|
||||
|
||||
SettingStore() {
|
||||
final systemLocale = PlatformDispatcher.instance.locale;
|
||||
_locale = systemLocale;
|
||||
}
|
||||
|
||||
/// 设置语言
|
||||
void setLocale(Locale locale) {
|
||||
_locale = locale;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
43
lib/widgets/shared/async_image.dart
Normal file
43
lib/widgets/shared/async_image.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AsyncImage extends StatelessWidget {
|
||||
final String url;
|
||||
final double? width;
|
||||
|
||||
const AsyncImage({
|
||||
super.key,
|
||||
required this.url,
|
||||
this.width,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: width,
|
||||
child: CachedNetworkImage(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
imageUrl: url ?? "",
|
||||
imageBuilder: (context, imageProvider) => Container(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: imageProvider,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
errorWidget: (context, url, error) => Icon(Icons.error),
|
||||
placeholder: (context, url) => Container(
|
||||
alignment: Alignment.center,
|
||||
child: FractionallySizedBox(
|
||||
widthFactor: 0.3,
|
||||
heightFactor: 0.3,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
75
lib/widgets/ui_kit/button/app_button.dart
Normal file
75
lib/widgets/ui_kit/button/app_button.dart
Normal file
@@ -0,0 +1,75 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../utils/enums/ui_theme.dart';
|
||||
import '../utils/enums/ui_variant.dart';
|
||||
import '../utils/theme/ui_color.dart';
|
||||
|
||||
class AppButton extends StatelessWidget {
|
||||
final Widget child;
|
||||
final VoidCallback? onPressed;
|
||||
final UiThemeType type;
|
||||
final UiVariant variant;
|
||||
final bool loading;
|
||||
final bool round;
|
||||
final bool disabled;
|
||||
|
||||
const AppButton({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.onPressed,
|
||||
this.type = UiThemeType.primary,
|
||||
this.variant = UiVariant.solid,
|
||||
this.loading = false,
|
||||
this.round = true,
|
||||
this.disabled = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//设置颜色
|
||||
final scheme = UiColor.getColorScheme(
|
||||
context,
|
||||
type: type,
|
||||
variant: variant,
|
||||
);
|
||||
|
||||
return Opacity(
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
if (!loading && !disabled) {
|
||||
onPressed?.call();
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: scheme.background,
|
||||
// 应用自定义颜色
|
||||
shadowColor: Colors.transparent,
|
||||
foregroundColor: scheme.foreground,
|
||||
shape: round
|
||||
? const StadiumBorder()
|
||||
: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Visibility(
|
||||
visible: loading,
|
||||
child: SizedBox.square(
|
||||
dimension: 15,
|
||||
child: CircularProgressIndicator(
|
||||
color: scheme.foreground,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
child,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
7
lib/widgets/ui_kit/utils/enums/ui_theme.dart
Normal file
7
lib/widgets/ui_kit/utils/enums/ui_theme.dart
Normal file
@@ -0,0 +1,7 @@
|
||||
// 主题风格
|
||||
enum UiThemeType {
|
||||
primary,
|
||||
success,
|
||||
warning,
|
||||
danger,
|
||||
}
|
||||
5
lib/widgets/ui_kit/utils/enums/ui_variant.dart
Normal file
5
lib/widgets/ui_kit/utils/enums/ui_variant.dart
Normal file
@@ -0,0 +1,5 @@
|
||||
//定义Variant
|
||||
enum UiVariant {
|
||||
solid,
|
||||
plain,
|
||||
}
|
||||
46
lib/widgets/ui_kit/utils/theme/ui_color.dart
Normal file
46
lib/widgets/ui_kit/utils/theme/ui_color.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:food_health/config/theme/base/app_theme_ext.dart';
|
||||
|
||||
import '../enums/ui_theme.dart';
|
||||
import '../enums/ui_variant.dart';
|
||||
|
||||
class UiColorScheme {
|
||||
final Color background;
|
||||
final Color foreground;
|
||||
|
||||
UiColorScheme({
|
||||
required this.background,
|
||||
required this.foreground,
|
||||
});
|
||||
}
|
||||
|
||||
class UiColor {
|
||||
static UiColorScheme getColorScheme(
|
||||
BuildContext context, {
|
||||
UiThemeType type = UiThemeType.primary,
|
||||
UiVariant variant = UiVariant.solid,
|
||||
}) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final themeExt = Theme.of(context).extension<AppThemeExtension>()!;
|
||||
|
||||
//主题色映射
|
||||
final baseColor = switch (type) {
|
||||
UiThemeType.primary => colorScheme.primary,
|
||||
UiThemeType.success => themeExt.success,
|
||||
UiThemeType.warning => themeExt.warning,
|
||||
UiThemeType.danger => themeExt.danger,
|
||||
};
|
||||
|
||||
// 风格区分
|
||||
return switch (variant) {
|
||||
UiVariant.solid => UiColorScheme(
|
||||
background: baseColor,
|
||||
foreground: Colors.white,
|
||||
),
|
||||
UiVariant.plain => UiColorScheme(
|
||||
background: baseColor.withValues(alpha: 0.3),
|
||||
foreground: baseColor,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
173
pubspec.lock
173
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"
|
||||
|
||||
11
pubspec.yaml
11
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/
|
||||
- assets/image/
|
||||
- assets/image/bg/
|
||||
67
scripts/gen_arb.dart
Normal file
67
scripts/gen_arb.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
void main() {
|
||||
// 输入文件和输出目录
|
||||
var inputFile = File("./lib/l10n/arb/i18n.yaml");
|
||||
var outDir = Directory("./lib/l10n/arb");
|
||||
|
||||
if (!inputFile.existsSync()) {
|
||||
print('❌ i18n.yaml 文件不存在:$inputFile');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// 读取并解析 yaml
|
||||
final yamlMap = loadYaml(inputFile.readAsStringSync());
|
||||
|
||||
Map<String, Map<String, dynamic>> arbMaps = {};
|
||||
|
||||
// 递归处理嵌套的 key
|
||||
void processYaml(Map yaml, {String? prefix}) {
|
||||
yaml.forEach((key, value) {
|
||||
if (value is Map && value.values.any((v) => v is Map)) {
|
||||
// 说明还有子层级,递归处理
|
||||
processYaml(value, prefix: prefix == null ? key : "${prefix}_$key");
|
||||
} else if (value is Map) {
|
||||
// 叶子节点:包含 zh/en/description
|
||||
final description = value['description'] ?? '';
|
||||
|
||||
value.forEach((lang, text) {
|
||||
if (lang == 'description') return;
|
||||
|
||||
final arbKey = prefix == null ? key : "${prefix}_$key";
|
||||
|
||||
arbMaps.putIfAbsent(lang, () => {});
|
||||
arbMaps[lang]![arbKey] = text;
|
||||
|
||||
if (description.toString().isNotEmpty) {
|
||||
arbMaps[lang]!['@$arbKey'] = {'description': description};
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
processYaml(yamlMap);
|
||||
|
||||
// 创建 arb 文件
|
||||
arbMaps.forEach((lang, json) {
|
||||
final file = File("${outDir.path}/app_$lang.arb");
|
||||
final encoder = JsonEncoder.withIndent(' ');
|
||||
file.writeAsStringSync(encoder.convert(json));
|
||||
print('✅ 生成 ${file.path}');
|
||||
});
|
||||
|
||||
// 执行 flutter gen-l10n
|
||||
final result = Process.runSync(
|
||||
"flutter",
|
||||
["gen-l10n"],
|
||||
runInShell: true, // 确保跨平台可用
|
||||
);
|
||||
if (result.exitCode == 0) {
|
||||
print('✅ flutter gen-l10n 成功');
|
||||
} else {
|
||||
print('❌ flutter gen-l10n 失败:${result.stderr}');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user