登录流程已全部重构
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:dio/dio.dart';
|
||||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||||
|
|
||||||
import '../../providers/app_store.dart';
|
import '../../stores/app_store.dart';
|
||||||
import '../dto/base_dto.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);
|
Color get primaryEnd => const Color(0xff06b6d4);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,30 +1,43 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
///颜色
|
import 'base/app_colors_base.dart';
|
||||||
final scheme = ColorScheme.fromSeed(
|
import 'base/app_text_style.dart';
|
||||||
primary: Color(0xff3784f1),
|
import 'base/app_theme_ext.dart';
|
||||||
seedColor: Color(0xff3885f2),
|
|
||||||
brightness: Brightness.light,
|
|
||||||
//卡片色
|
|
||||||
surface: Colors.white,
|
|
||||||
surfaceContainerLow: Color(0xFFF4F8FB),
|
|
||||||
surfaceContainer: Color(0xFFE9ECF3),
|
|
||||||
surfaceContainerHigh: Color(0xFFDDE2EA),
|
|
||||||
//颜色
|
|
||||||
onSurfaceVariant: Color(0xFF828282),
|
|
||||||
|
|
||||||
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,
|
||||||
|
|
||||||
///字体
|
onSurfaceVariant: themeBase.textSecondary,
|
||||||
final textTheme = TextTheme(
|
//背景色
|
||||||
titleLarge: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, color: scheme.onSurface),
|
surfaceContainerHigh: themeBase.surfaceContainerHigh,
|
||||||
titleMedium: TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: scheme.onSurface),
|
surfaceContainer: themeBase.surfaceContainer,
|
||||||
titleSmall: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: scheme.onSurface),
|
surfaceContainerLow: themeBase.surfaceContainerLow,
|
||||||
bodyLarge: TextStyle(fontSize: 18),
|
surfaceContainerLowest: themeBase.surfaceContainerLowest,
|
||||||
bodyMedium: TextStyle(fontSize: 16),
|
),
|
||||||
bodySmall: TextStyle(fontSize: 14),
|
textTheme: textTheme,
|
||||||
labelLarge: TextStyle(fontSize: 16, color: scheme.onSurfaceVariant),
|
extensions: [AppThemeExtension.fromColors(themeBase)],
|
||||||
labelMedium: TextStyle(fontSize: 14, color: scheme.onSurfaceVariant),
|
appBarTheme: AppBarTheme(
|
||||||
labelSmall: TextStyle(fontSize: 12, color: scheme.onSurfaceVariant),
|
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:provider/provider.dart';
|
||||||
import 'package:remixicon/remixicon.dart';
|
import 'package:remixicon/remixicon.dart';
|
||||||
|
|
||||||
import '../page/home/home_page.dart';
|
import '../pages/home/home_page.dart';
|
||||||
import '../page/profile/my/my_page.dart';
|
import '../pages/profile/my/my_page.dart';
|
||||||
import '../page/record/list/record_list_page.dart';
|
import '../pages/record/list/record_list_page.dart';
|
||||||
import '../providers/user_store.dart';
|
import '../stores/user_store.dart';
|
||||||
import '../router/config/route_paths.dart';
|
import '../router/config/route_paths.dart';
|
||||||
import 'tabbar.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:food_health/router/routes.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_easyloading/flutter_easyloading.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 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'config/theme/theme.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() {
|
void main() {
|
||||||
runApp(
|
runApp(
|
||||||
@@ -14,6 +21,7 @@ void main() {
|
|||||||
providers: [
|
providers: [
|
||||||
ChangeNotifierProvider(create: (context) => AppStore()),
|
ChangeNotifierProvider(create: (context) => AppStore()),
|
||||||
ChangeNotifierProvider(create: (context) => UserStore()),
|
ChangeNotifierProvider(create: (context) => UserStore()),
|
||||||
|
ChangeNotifierProvider(create: (_) => SettingStore()),
|
||||||
],
|
],
|
||||||
child: MyApp(),
|
child: MyApp(),
|
||||||
),
|
),
|
||||||
@@ -25,32 +33,28 @@ class MyApp extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
//语言化
|
||||||
|
L10n.init(context);
|
||||||
|
|
||||||
|
final settingStore = context.watch<SettingStore>();
|
||||||
return ScreenUtilInit(
|
return ScreenUtilInit(
|
||||||
designSize: const Size(375, 694),
|
designSize: const Size(375, 694),
|
||||||
useInheritedMediaQuery: true,
|
useInheritedMediaQuery: true,
|
||||||
child: MaterialApp.router(
|
child: MaterialApp.router(
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
routerConfig: goRouter,
|
routerConfig: goRouter,
|
||||||
localizationsDelegates: [],
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
themeMode: ThemeMode.light,
|
supportedLocales: [
|
||||||
theme: ThemeData(
|
Locale('en'),
|
||||||
useMaterial3: true,
|
Locale('zh', 'CN'),
|
||||||
colorScheme: scheme,
|
],
|
||||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
theme: AppTheme.createTheme(LightTheme()),
|
||||||
style: ElevatedButton.styleFrom(
|
locale: settingStore.locale,
|
||||||
backgroundColor: scheme.primary,
|
builder: (context, child) {
|
||||||
foregroundColor: scheme.onPrimary,
|
// ⚡️ 在这里就能拿到 AppLocalizations
|
||||||
),
|
L10n.init(context);
|
||||||
),
|
return EasyLoading.init()(context, child);
|
||||||
textTheme: textTheme,
|
},
|
||||||
scaffoldBackgroundColor: Color(0xffFAFAFE),
|
|
||||||
appBarTheme: AppBarTheme(
|
|
||||||
backgroundColor: scheme.surface,
|
|
||||||
scrolledUnderElevation: 0,
|
|
||||||
titleTextStyle: textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
builder: EasyLoading.init(),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 'package:flutter/material.dart';
|
||||||
import '../../widgets/common/app_backend.dart';
|
import '../../widgets/shared/app_backend.dart';
|
||||||
import '../../widgets/common/app_header.dart';
|
import '../../widgets/shared/app_header.dart';
|
||||||
import 'widget/upload_panel.dart';
|
import 'widget/upload_panel.dart';
|
||||||
|
|
||||||
class HomePage extends StatefulWidget {
|
class HomePage extends StatefulWidget {
|
||||||
@@ -6,7 +6,7 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||||
import 'package:food_health/api/endpoints/food_api.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:food_health/router/config/route_paths.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:image_picker/image_picker.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:flutter_easyloading/flutter_easyloading.dart';
|
||||||
import 'package:food_health/api/dto/profile_options_dto.dart';
|
import 'package:food_health/api/dto/profile_options_dto.dart';
|
||||||
import 'package:food_health/api/endpoints/profile_api.dart';
|
import 'package:food_health/api/endpoints/profile_api.dart';
|
||||||
import 'package:food_health/config/theme/custom_colors.dart';
|
import 'package:food_health/config/theme/color_ext.dart';
|
||||||
import 'package:food_health/page/profile/edit/widget/food_allergies.dart';
|
import 'package:food_health/pages/profile/edit/widget/food_allergies.dart';
|
||||||
import 'package:food_health/providers/user_store.dart';
|
import 'package:food_health/stores/user_store.dart';
|
||||||
import 'package:food_health/widgets/common/app_backend.dart';
|
import 'package:food_health/widgets/shared/app_backend.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:remixicon/remixicon.dart';
|
import 'package:remixicon/remixicon.dart';
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:food_health/api/dto/profile_options_dto.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 'package:remixicon/remixicon.dart';
|
||||||
|
|
||||||
import '../data/state.dart';
|
import '../data/state.dart';
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:food_health/api/dto/profile_options_dto.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 'package:remixicon/remixicon.dart';
|
||||||
|
|
||||||
import '../data/state.dart';
|
import '../data/state.dart';
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
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:food_health/providers/user_store.dart';
|
import 'package:food_health/stores/user_store.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:remixicon/remixicon.dart';
|
import 'package:remixicon/remixicon.dart';
|
||||||
|
|
||||||
@@ -3,9 +3,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||||
import 'package:food_health/api/dto/user_profile_dto.dart';
|
import 'package:food_health/api/dto/user_profile_dto.dart';
|
||||||
import 'package:food_health/api/endpoints/user_api.dart';
|
import 'package:food_health/api/endpoints/user_api.dart';
|
||||||
import 'package:food_health/config/theme/custom_colors.dart';
|
import 'package:food_health/config/theme/color_ext.dart';
|
||||||
import 'package:food_health/providers/app_store.dart';
|
import 'package:food_health/stores/app_store.dart';
|
||||||
import 'package:food_health/providers/user_store.dart';
|
import 'package:food_health/stores/user_store.dart';
|
||||||
import 'package:food_health/router/config/route_paths.dart';
|
import 'package:food_health/router/config/route_paths.dart';
|
||||||
import 'package:food_health/utils/common.dart';
|
import 'package:food_health/utils/common.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
@@ -45,29 +45,39 @@ class DetailedAnalysis extends StatelessWidget {
|
|||||||
physics: NeverScrollableScrollPhysics(),
|
physics: NeverScrollableScrollPhysics(),
|
||||||
data: detail.explanation ?? "",
|
data: detail.explanation ?? "",
|
||||||
),
|
),
|
||||||
Row(
|
Visibility(
|
||||||
spacing: 10,
|
visible: detail.ingredientsList!.isNotEmpty,
|
||||||
children: [
|
child: Column(
|
||||||
Icon(RemixIcons.menu_2_line),
|
children: [
|
||||||
Text(
|
Row(
|
||||||
"Detected Ingredients",
|
spacing: 10,
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
children: [
|
||||||
),
|
Icon(RemixIcons.menu_2_line),
|
||||||
],
|
Text(
|
||||||
),
|
"Detected Ingredients",
|
||||||
Wrap(
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
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),
|
Container(
|
||||||
);
|
margin: EdgeInsets.only(top: 10),
|
||||||
}).toList(),
|
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: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:markdown_widget/widget/markdown.dart';
|
||||||
import 'package:remixicon/remixicon.dart';
|
import 'package:remixicon/remixicon.dart';
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:food_health/api/dto/food_scan_dto.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';
|
import 'package:remixicon/remixicon.dart';
|
||||||
|
|
||||||
class ResultChip extends StatelessWidget {
|
class ResultChip extends StatelessWidget {
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:food_health/api/dto/food_scan_dto.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/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:food_health/widgets/ui_kit/empty/index.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:remixicon/remixicon.dart';
|
import 'package:remixicon/remixicon.dart';
|
||||||
@@ -35,6 +35,7 @@ class RecordListCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
|
cacheExtent: 2000,
|
||||||
padding: EdgeInsets.symmetric(horizontal: 15, vertical: 15),
|
padding: EdgeInsets.symmetric(horizontal: 15, vertical: 15),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
var item = records[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/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.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:go_router/go_router.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@@ -51,13 +50,6 @@ class _SplashPageState extends State<SplashPage> {
|
|||||||
"assets/image/logo.png",
|
"assets/image/logo.png",
|
||||||
width: 68.w,
|
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 splash = "/";
|
||||||
|
|
||||||
|
///引导页
|
||||||
|
static const intro = "/intro";
|
||||||
|
|
||||||
///协议页
|
///协议页
|
||||||
static const agreement = "/agreement";
|
static const agreement = "/agreement";
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import 'package:food_health/layout/layout_page.dart';
|
import 'package:food_health/layout/layout_page.dart';
|
||||||
import 'package:food_health/page/system/login/login_code_page.dart';
|
import 'package:food_health/pages/system/code/login_code_page.dart';
|
||||||
import 'package:food_health/page/system/splash/splash_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 'package:food_health/router/config/route_paths.dart';
|
||||||
|
|
||||||
import '../../page/system/agree/agree_page.dart';
|
import '../../pages/system/agree/agree_page.dart';
|
||||||
import '../../page/system/login/login_page.dart';
|
import '../../pages/system/login/login_page.dart';
|
||||||
import '../config/route_type.dart';
|
import '../config/route_type.dart';
|
||||||
|
|
||||||
List<RouteType> baseRoutes = [
|
List<RouteType> baseRoutes = [
|
||||||
@@ -30,6 +32,12 @@ List<RouteType> baseRoutes = [
|
|||||||
return LoginPage();
|
return LoginPage();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
RouteType(
|
||||||
|
path: RoutePaths.intro,
|
||||||
|
child: (state) {
|
||||||
|
return IntroPage();
|
||||||
|
},
|
||||||
|
),
|
||||||
RouteType(
|
RouteType(
|
||||||
path: RoutePaths.loginCode,
|
path: RoutePaths.loginCode,
|
||||||
child: (state) {
|
child: (state) {
|
||||||
@@ -46,4 +54,10 @@ List<RouteType> baseRoutes = [
|
|||||||
return LayoutPage();
|
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_paths.dart';
|
||||||
import '../config/route_type.dart';
|
import '../config/route_type.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ List<RouteBase> routes = routeConfigs.map((item) {
|
|||||||
|
|
||||||
//变量命名
|
//变量命名
|
||||||
GoRouter goRouter = GoRouter(
|
GoRouter goRouter = GoRouter(
|
||||||
initialLocation: RoutePaths.splash,
|
initialLocation: RoutePaths.intro,
|
||||||
routes: routes,
|
routes: routes,
|
||||||
navigatorKey: navigatorKey,
|
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"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
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:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -57,6 +81,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.4+2"
|
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:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -153,11 +185,27 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.3+4"
|
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:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
flutter_easyloading:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -230,6 +278,11 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.0"
|
version: "5.0.0"
|
||||||
|
flutter_localizations:
|
||||||
|
dependency: "direct main"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
flutter_plugin_android_lifecycle:
|
flutter_plugin_android_lifecycle:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -408,6 +461,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.2"
|
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:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -512,6 +573,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
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:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -520,6 +589,30 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
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:
|
path_provider_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -584,6 +677,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
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:
|
scroll_to_index:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -693,6 +794,54 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.1"
|
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:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -717,6 +866,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
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:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -805,6 +962,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.4"
|
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:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -893,6 +1058,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.6.1"
|
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:
|
sdks:
|
||||||
dart: ">=3.8.1 <4.0.0"
|
dart: ">=3.8.1 <4.0.0"
|
||||||
flutter: ">=3.31.0-0.0.pre"
|
flutter: ">=3.31.0-0.0.pre"
|
||||||
|
|||||||
11
pubspec.yaml
11
pubspec.yaml
@@ -10,7 +10,8 @@ environment:
|
|||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
flutter_localizations:
|
||||||
|
sdk: flutter
|
||||||
cupertino_icons: ^1.0.8
|
cupertino_icons: ^1.0.8
|
||||||
shared_preferences: ^2.5.3
|
shared_preferences: ^2.5.3
|
||||||
dio: ^5.8.0+1
|
dio: ^5.8.0+1
|
||||||
@@ -28,7 +29,9 @@ dependencies:
|
|||||||
markdown_widget: ^2.3.2+8
|
markdown_widget: ^2.3.2+8
|
||||||
sign_in_with_apple: ^7.0.1
|
sign_in_with_apple: ^7.0.1
|
||||||
flutter_image_compress: ^2.4.0
|
flutter_image_compress: ^2.4.0
|
||||||
|
cached_network_image: ^3.4.1
|
||||||
|
yaml: ^3.1.3
|
||||||
|
intl: any
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -39,5 +42,7 @@ dev_dependencies:
|
|||||||
flutter:
|
flutter:
|
||||||
|
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
generate: true
|
||||||
assets:
|
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