From 9671798f2909f3db17e011929cb7ac3d99e38dd1 Mon Sep 17 00:00:00 2001 From: Songzex <2402265378@qq.com> Date: Mon, 12 Oct 2026 14:21:09 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=9A=E8=AF=AD=E8=A8=80=E5=88=87=E6=8D=A2+?= =?UTF-8?q?=E9=A6=96=E6=AC=A1=E8=AE=BE=E7=BD=AE=E8=AF=AD=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/languages/en-US.json | 106 ++++++++++++++- assets/languages/zh-CN.json | 106 ++++++++++++++- lib/components/language_selection_dialog.dart | 93 +++++++++++++ lib/core/env/env_config.dart | 2 +- lib/core/localization/locale_cubit.dart | 30 ++-- .../auth/presentation/bloc/auth_cubit.dart | 46 ++++--- .../auth/presentation/pages/login_page.dart | 61 ++++++--- .../presentation/main_wrapper.dart | 11 +- .../widgets/custom_bottom_nav_bar.dart | 4 +- .../my/presentation/pages/my_page.dart | 35 ++--- .../pages/device_status_page.dart | 8 +- .../pages/robot_control_page.dart | 21 ++- .../presentation/pages/robot_list_page.dart | 8 +- .../widgets/robot_action_buttons.dart | 7 +- .../presentation/widgets/robot_checklist.dart | 3 +- .../widgets/robot_control_panel.dart | 11 +- .../widgets/robot_header_card.dart | 31 +++-- .../presentation/widgets/robot_item_card.dart | 18 ++- .../widgets/robot_status_bar.dart | 9 +- .../presentation/widgets/robot_task_info.dart | 5 +- .../pages/help_feedback_page.dart | 128 +++++++----------- .../pages/system_settings_page.dart | 103 +++++--------- 22 files changed, 572 insertions(+), 274 deletions(-) create mode 100644 lib/components/language_selection_dialog.dart diff --git a/assets/languages/en-US.json b/assets/languages/en-US.json index 0ed19775..626b3bfb 100644 --- a/assets/languages/en-US.json +++ b/assets/languages/en-US.json @@ -23,7 +23,29 @@ "username_hint": "Enter username", "password_hint": "Enter password", "login_success": "Login Success", - "login_failed": "Login Failed" + "login_failed": "Login Failed", + "token_expired_title": "Login Expired", + "token_expired_content": "Your login session has expired, please log in again", + "kicked_out_title": "Logged In Elsewhere", + "kicked_out_content": "Your account has logged in on another device, signing out...", + "reconnect_failed_title": "Connection Error", + "reconnect_failed_content": "TCP reconnection authentication failed, please log in again", + "reconnecting": "TCP reconnecting...", + "reconnect_success": "Connection successful", + "account_login_title": "Account Login", + "subtitle": "Please fill in the following to verify your identity", + "cert_text": "Certified by Huawei Cloud ecosystem, safe to use. Certificate No. C202605301", + "login_submit": "Log In", + "logging_in": "Logging in...", + "no_account": "No account?", + "register_now": "Sign up", + "agree_first": "Please read and agree to the user agreement first", + "agree_prefix": "I have read and agree to the ", + "agree_and": " and ", + "user_agreement_link": "User Agreement", + "privacy_policy_link": "Privacy Policy", + "understood": "Got it", + "protocol_detail_text": "Your %s details will appear here..." }, "home": { @@ -376,7 +398,37 @@ "data": "Data", "inspection_robot": "Inspection Robot", "cleaning_robot": "Cleaning Robot", - "weeding_robot": "Weeding Robot" + "weeding_robot": "Weeding Robot", + "device_not_found_title": "Device Not Found", + "device_not_found_content": "Device not matched yet, please retry later", + "no_alias": "No alias", + "task_standby": "Standby", + "task_charging": "Charging", + "control_panel": "Control Panel", + "speed_gear": "Speed", + "speed_low": "Low", + "speed_mid": "Medium", + "speed_high": "High", + "status_speed": "Speed", + "status_mode": "Mode", + "status_signal": "Signal", + "action_start": "Start", + "action_pause": "Pause", + "action_return": "Return", + "task_in_progress": "In Progress", + "checklist_title": "Checklist", + "signal_strong": "Strong", + "view_front": "Front", + "view_back": "Back", + "view_left": "Left", + "view_right": "Right", + "view_panorama": "Panorama", + "no_video": "No Video", + "play": "Play", + "path_planning": "Path Planning", + "path_planning_desc": "View and edit the robot patrol path", + "release_permission_success": "Remote control permission released", + "release_permission_failed": "Failed to release control permission" }, "alarm_center": { @@ -448,6 +500,56 @@ "tab_settings": "Tab Settings" }, + "help_feedback": { + "tab_faq": "FAQ", + "tab_feedback": "Feedback", + "type_feature": "Feature Suggestion", + "type_bug": "Bug Report", + "type_experience": "Experience Issue", + "type_other": "Other", + "cat_bind": "Device Binding", + "cat_path": "Path Planning", + "cat_remote": "Remote Control", + "cat_order": "Work Order System", + "cat_account": "Account Issues", + "q_bind_fail": "What should I do if QR-code binding fails?", + "a_bind_fail": "Check that the device is powered on and in pending-binding state, and make sure Bluetooth and the network are working on your phone. If it still fails, restart the device and scan again.", + "q_bt_notfound": "Can't find the device's Bluetooth signal?", + "a_bt_notfound": "Make sure the device is on and its Bluetooth indicator is blinking. Move closer to the device (within 1 meter) and search again. When binding multiple devices, do them one by one.", + "q_path_none": "Unable to generate a path?", + "a_path_none": "Ensure a complete work area is drawn (at least 3 points) and the GPS signal is good. If using auto-planning, make sure the area boundary does not self-intersect.", + "q_map_issue": "Map not displaying or location drifting?", + "a_map_issue": "Check that phone GPS is enabled and location permission is granted. Drifting may occur indoors or in weak-signal areas; use it in open areas.", + "q_video_stutter": "Remote video lagging or connection timeout?", + "a_video_stutter": "Check that the network connection on both phone and device is stable. Use remote control under good 4G/Wi-Fi. If lag persists, try lowering the video resolution.", + "q_estop": "How do I use the emergency stop?", + "a_estop": "On the remote control page, tap the red emergency-stop button at the center to stop the device immediately. You must manually release it to resume.", + "q_order_submit": "How do I submit a work order?", + "a_order_submit": "On the Work Order page, tap \"New Order\" at the bottom right, choose a type, fill in the description, upload images, then submit.", + "q_order_status": "How do I check work order status?", + "a_order_status": "On the Work Order page you can view all submitted orders and their status (Pending/Processing/Completed). Tap an order for details and replies.", + "q_pwd_forget": "What if I forgot my password?", + "a_pwd_forget": "Tap \"Forgot Password\" on the login page and verify with your registered phone number to reset it.", + "q_rebind_device": "Do I need to rebind after switching devices?", + "a_rebind_device": "Device bindings are not lost when logging into the same account on a new phone, but you need to search and connect the devices again on the new phone.", + "contact_title": "Contact Support", + "hotline": "Service Hotline", + "wechat_work": "WeCom", + "wechat_hint": "Scan to add support on WeChat", + "feedback_type": "Feedback Type", + "problem_desc": "Description", + "problem_hint": "Please describe your problem or suggestion in detail...", + "add_screenshot": "Add Screenshot", + "contact_label": "Contact (optional)", + "contact_hint": "Phone/WeChat number for follow-up", + "submit_feedback": "Submit Feedback", + "close": "Close", + "copy_number": "Copy Number", + "number_copied": "Number copied", + "please_input_desc": "Please enter a description", + "submit_success": "Feedback submitted. Thank you for your support!" + }, + "report": { "title": "Field Report", "submit": "Submit", diff --git a/assets/languages/zh-CN.json b/assets/languages/zh-CN.json index e847d07c..7ef16bb1 100644 --- a/assets/languages/zh-CN.json +++ b/assets/languages/zh-CN.json @@ -23,7 +23,29 @@ "username_hint": "请输入用户名", "password_hint": "请输入密码", "login_success": "登录成功", - "login_failed": "登录失败" + "login_failed": "登录失败", + "token_expired_title": "登录已过期", + "token_expired_content": "账号登录状态已过期,请重新登录", + "kicked_out_title": "账号被顶下线", + "kicked_out_content": "您的账号在其他设备登录,即将退出当前设备...", + "reconnect_failed_title": "连接异常", + "reconnect_failed_content": "TCP重连认证无效请重新登陆", + "reconnecting": "TCP重连中...", + "reconnect_success": "连接成功", + "account_login_title": "账号登录", + "subtitle": "请填写以下信息以验证身份", + "cert_text": "通过华为云生态技术认证,请放心使用,证书编号C202605301", + "login_submit": "登 录", + "logging_in": "登录中...", + "no_account": "没有账号吗?", + "register_now": "立即注册", + "agree_first": "请先阅读并同意用户协议", + "agree_prefix": "我已阅读并同意 ", + "agree_and": " 和 ", + "user_agreement_link": "《用户协议》", + "privacy_policy_link": "《隐私政策》", + "understood": "我已了解", + "protocol_detail_text": "此处放置您的%s详细内容..." }, "home": { @@ -389,7 +411,37 @@ "data": "数据", "inspection_robot": "巡检机器人", "cleaning_robot": "清洗机器人", - "weeding_robot": "除草机器人" + "weeding_robot": "除草机器人", + "device_not_found_title": "未找到设备", + "device_not_found_content": "暂未匹配到该设备,稍后重试", + "no_alias": "暂无别名", + "task_standby": "待机中", + "task_charging": "充电中", + "control_panel": "控制面板", + "speed_gear": "速度档位", + "speed_low": "低速", + "speed_mid": "中速", + "speed_high": "高速", + "status_speed": "转速", + "status_mode": "模式", + "status_signal": "信号", + "action_start": "开始", + "action_pause": "暂停", + "action_return": "返航", + "task_in_progress": "进行中", + "checklist_title": "检查清单", + "signal_strong": "强", + "view_front": "前视", + "view_back": "后视", + "view_left": "左视", + "view_right": "右视", + "view_panorama": "全景", + "no_video": "暂无视频", + "play": "播放", + "path_planning": "路径规划", + "path_planning_desc": "查看和编辑机器人巡检路径", + "release_permission_success": "已释放远程控制权限", + "release_permission_failed": "控制权限释放失败" }, "alarm_center": { @@ -462,6 +514,56 @@ "tab_settings": "Tab 设置" }, + "help_feedback": { + "tab_faq": "常见问题", + "tab_feedback": "意见反馈", + "type_feature": "功能建议", + "type_bug": "Bug报告", + "type_experience": "体验问题", + "type_other": "其他", + "cat_bind": "设备绑定", + "cat_path": "路径规划", + "cat_remote": "远程控制", + "cat_order": "工单系统", + "cat_account": "账号问题", + "q_bind_fail": "扫码绑定失败怎么办?", + "a_bind_fail": "请检查设备是否已开机并处于待绑定状态,确保手机蓝牙已开启且网络正常。如仍无法绑定,请尝试重启设备后重新扫码。", + "q_bt_notfound": "搜不到设备蓝牙信号?", + "a_bt_notfound": "请确认设备已开机且蓝牙指示灯正常闪烁。尝试靠近设备(1米以内)后重新搜索。如多个设备同时绑定,请逐一操作。", + "q_path_none": "无法生成路径?", + "a_path_none": "请确保已绘制完整的作业区域(至少3个打点),并检查GPS信号是否良好。如果使用自动规划模式,请确认区域边界没有自相交。", + "q_map_issue": "地图不显示或定位偏移?", + "a_map_issue": "请检查手机GPS是否已开启,并授权App位置权限。在室内或信号较弱的区域可能出现定位偏移,建议到开阔区域使用。", + "q_video_stutter": "远程视频卡顿/连接超时?", + "a_video_stutter": "请检查手机和设备的网络连接是否稳定。建议在4G/Wi-Fi信号良好的环境下使用远程控制功能。如持续卡顿,可尝试降低视频分辨率。", + "q_estop": "急停功能怎么使用?", + "a_estop": "在远程控制页面,点击屏幕中央红色急停按钮即可立即停止设备运行。急停后需手动解除才能恢复操作。", + "q_order_submit": "如何提交工单?", + "a_order_submit": "在「工单」页面点击右下角「新建工单」按钮,选择工单类型,填写问题描述并上传相关图片后提交即可。", + "q_order_status": "工单状态怎么看?", + "a_order_status": "在「工单」页面可查看所有已提交的工单及其当前状态(待处理/处理中/已完成)。点击具体工单可查看详情和回复。", + "q_pwd_forget": "忘记密码怎么办?", + "a_pwd_forget": "在登录页面点击「忘记密码」,通过注册手机号验证后即可重置密码。", + "q_rebind_device": "换设备登录需要重新绑定?", + "a_rebind_device": "更换手机登录同一账号后,设备绑定关系不会丢失。但需要在新设备上重新搜索和连接设备。", + "contact_title": "联系客服", + "hotline": "客服热线", + "wechat_work": "企业微信", + "wechat_hint": "扫码添加客服微信", + "feedback_type": "反馈类型", + "problem_desc": "问题描述", + "problem_hint": "请详细描述您遇到的问题或建议...", + "add_screenshot": "添加截图", + "contact_label": "联系方式(选填)", + "contact_hint": "手机号/微信号,方便我们回访", + "submit_feedback": "提交反馈", + "close": "关闭", + "copy_number": "复制号码", + "number_copied": "号码已复制", + "please_input_desc": "请输入问题描述", + "submit_success": "反馈已提交,感谢您的支持!" + }, + "report": { "title": "现场上报", "submit": "提交", diff --git a/lib/components/language_selection_dialog.dart b/lib/components/language_selection_dialog.dart new file mode 100644 index 00000000..e38767b5 --- /dev/null +++ b/lib/components/language_selection_dialog.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:maibu_satabot_v2/core/localization/locale_cubit.dart'; + +/// 可选语言项:tag 对应 assets/languages/{tag}.json,nativeName 用该语言本地写法展示。 +class LanguageItem { + final Locale locale; + final String nativeName; + const LanguageItem(this.locale, this.nativeName); + + String get tag => locale.countryCode == null || locale.countryCode!.isEmpty + ? locale.languageCode + : '${locale.languageCode}-${locale.countryCode}'; +} + +/// 全局唯一的“可选语言列表”来源。 +/// 后续接入“接口下载语言包”时,只需在这里扩展(例如把已下载的语言追加进来), +/// 登录强制弹窗与设置页入口都会自动同步,无需改多处。 +class AppLanguages { + AppLanguages._(); + static final AppLanguages instance = AppLanguages._(); + + final List items = const [ + LanguageItem(Locale('zh', 'CN'), '简体中文'), + LanguageItem(Locale('en', 'US'), 'English'), + ]; + + String nameOf(Locale locale) { + final tag = locale.countryCode == null || locale.countryCode!.isEmpty + ? locale.languageCode + : '${locale.languageCode}-${locale.countryCode}'; + for (final item in items) { + if (item.tag == tag) return item.nativeName; + } + return tag; + } +} + +/// 语言选择弹窗。 +/// [mandatory] = true:登录时强制选择,不可返回/点外部关闭; +/// [mandatory] = false:设置页中可随时关闭。 +class LanguageSelectionDialog extends StatelessWidget { + final bool mandatory; + const LanguageSelectionDialog({super.key, this.mandatory = true}); + + Future _onSelect(BuildContext context, Locale locale) async { + await context.read().setLocale(locale); + if (context.mounted) Navigator.of(context).pop(); + } + + @override + Widget build(BuildContext context) { + final current = context.select( + (c) => c.state, + ); + + return PopScope( + // 强制弹窗时拦截返回键,必须选一个才能继续使用 + canPop: !mandatory, + child: AlertDialog( + title: const Text('选择语言 / Select Language'), + content: SizedBox( + width: 300, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (final item in AppLanguages.instance.items) + RadioListTile( + value: item.tag, + // 高亮当前语言:按 languageCode 匹配,兼容国家码为空的历史值 + groupValue: current.countryCode == null || + current.countryCode!.isEmpty + ? current.languageCode + : '${current.languageCode}-${current.countryCode}', + title: Text(item.nativeName), + onChanged: (_) => _onSelect(context, item.locale), + ), + ], + ), + ), + actions: mandatory + ? null + : [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('取消'), + ), + ], + ), + ); + } +} diff --git a/lib/core/env/env_config.dart b/lib/core/env/env_config.dart index 0d1204e8..a0147362 100644 --- a/lib/core/env/env_config.dart +++ b/lib/core/env/env_config.dart @@ -1,7 +1,7 @@ class EnvConfig { static const String environment = String.fromEnvironment( 'ENV', - defaultValue: 'prod', + defaultValue: 'test', ); static String get sentryDsn { diff --git a/lib/core/localization/locale_cubit.dart b/lib/core/localization/locale_cubit.dart index 454cd098..4f635865 100644 --- a/lib/core/localization/locale_cubit.dart +++ b/lib/core/localization/locale_cubit.dart @@ -4,33 +4,45 @@ import 'package:shared_preferences/shared_preferences.dart'; class LocaleCubit extends Cubit { static const String _localeKey = 'app_locale'; - - LocaleCubit() : super(const Locale('zh', 'CN')) { + static const String _chosenKey = 'app_locale_chosen'; + + /// 用户是否已明确选择过语言(决定登录页是否弹强制语言选择框) + bool hasChosenLanguage = false; + + LocaleCubit() : super(const Locale('en', 'US')) { _loadSavedLocale(); } - + Future _loadSavedLocale() async { try { final prefs = await SharedPreferences.getInstance(); - final languageCode = prefs.getString('${_localeKey}_lang') ?? 'zh'; - final countryCode = prefs.getString('${_localeKey}_country') ?? 'CN'; - emit(Locale(languageCode, countryCode)); + hasChosenLanguage = prefs.getBool(_chosenKey) ?? false; + if (hasChosenLanguage) { + final languageCode = prefs.getString('${_localeKey}_lang') ?? 'en'; + final countryCode = prefs.getString('${_localeKey}_country') ?? 'US'; + emit(Locale(languageCode, countryCode)); + } else { + // 未选择过:默认英文(登录页仍会弹出强制选择框) + emit(const Locale('en', 'US')); + } } catch (e) { - emit(const Locale('zh', 'CN')); + emit(const Locale('en', 'US')); } } - + Future setLocale(Locale locale) async { try { final prefs = await SharedPreferences.getInstance(); await prefs.setString('${_localeKey}_lang', locale.languageCode); await prefs.setString('${_localeKey}_country', locale.countryCode ?? ''); + await prefs.setBool(_chosenKey, true); + hasChosenLanguage = true; emit(locale); } catch (e) { debugPrint('❌ [LocaleCubit] 保存语言设置失败: $e'); } } - + bool get isChinese => state.languageCode == 'zh'; bool get isEnglish => state.languageCode == 'en'; } diff --git a/lib/features/auth/presentation/bloc/auth_cubit.dart b/lib/features/auth/presentation/bloc/auth_cubit.dart index 56128510..568ace65 100644 --- a/lib/features/auth/presentation/bloc/auth_cubit.dart +++ b/lib/features/auth/presentation/bloc/auth_cubit.dart @@ -13,6 +13,7 @@ import 'package:maibu_satabot_v2/core/domain/entities/user_entity.dart'; import 'package:maibu_satabot_v2/core/network/net_message_dispatcher.dart'; import '../../../../core/app/app_user_cubit.dart'; +import '../../../../core/localization/app_localizations.dart'; import '../../../../core/infrastructure/logging/sentry_logger_impl.dart'; import '../../../../core/logging/i_logger_service.dart'; import '../../../../core/network/tcp/tcp_client.dart'; @@ -265,15 +266,16 @@ class AuthCubit extends Cubit { context: context, barrierDismissible: false, builder: (BuildContext dialogContext) { + final loc = AppLocalizations.of(dialogContext); return AlertDialog( - title: const Text('登录已过期'), - content: const Text('账号登录状态已过期,请重新登录'), + title: Text(loc.translate('login.token_expired_title')), + content: Text(loc.translate('login.token_expired_content')), actions: [ ElevatedButton( onPressed: () { Navigator.of(dialogContext).pop(); }, - child: const Text('确定'), + child: Text(loc.translate('common.ok')), ), ], ); @@ -317,9 +319,10 @@ class AuthCubit extends Cubit { context: context, barrierDismissible: false, // 不允许点击背景关闭 builder: (BuildContext dialogContext) { + final loc = AppLocalizations.of(dialogContext); return AlertDialog( - title: const Text('账号被顶下线'), - content: const Text('您的账号在其他设备登录,即将退出当前设备...'), + title: Text(loc.translate('login.kicked_out_title')), + content: Text(loc.translate('login.kicked_out_content')), actions: [], ); }, @@ -358,6 +361,7 @@ class AuthCubit extends Cubit { context: context, barrierDismissible: false, builder: (BuildContext dialogContext) { + final loc = AppLocalizations.of(dialogContext); return AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), content: Column( @@ -365,18 +369,18 @@ class AuthCubit extends Cubit { children: [ const Icon(Icons.wifi_off_rounded, size: 48, color: Color(0xFFF53F3F)), const SizedBox(height: 12), - const Text( - '连接异常', - style: TextStyle( + Text( + loc.translate('login.reconnect_failed_title'), + style: const TextStyle( fontSize: 17, fontWeight: FontWeight.w600, color: Color(0xFF1D2129), ), ), const SizedBox(height: 8), - const Text( - 'TCP重连认证无效请重新登陆', - style: TextStyle(fontSize: 14, color: Color(0xFF86909C)), + Text( + loc.translate('login.reconnect_failed_content'), + style: const TextStyle(fontSize: 14, color: Color(0xFF86909C)), textAlign: TextAlign.center, ), ], @@ -395,7 +399,7 @@ class AuthCubit extends Cubit { borderRadius: BorderRadius.circular(8), ), ), - child: const Text('取消', style: TextStyle(fontSize: 15)), + child: Text(loc.translate('common.cancel'), style: const TextStyle(fontSize: 15)), ), ), const SizedBox(width: 12), @@ -414,7 +418,7 @@ class AuthCubit extends Cubit { borderRadius: BorderRadius.circular(8), ), ), - child: const Text('退出登录', style: TextStyle(fontSize: 15)), + child: Text(loc.translate('login.logout'), style: const TextStyle(fontSize: 15)), ), ), ], @@ -439,10 +443,10 @@ class AuthCubit extends Cubit { final context = navigatorKey.currentContext; if (context != null) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('TCP重连中...'), - backgroundColor: Color(0xFFFF7D00), - duration: Duration(seconds: 4), + SnackBar( + content: Text(AppLocalizations.of(context).translate('login.reconnecting')), + backgroundColor: const Color(0xFFFF7D00), + duration: const Duration(seconds: 4), ), ); } @@ -456,10 +460,10 @@ class AuthCubit extends Cubit { if (context != null) { ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('连接成功'), - backgroundColor: Color(0xFF00B42A), - duration: Duration(seconds: 2), + SnackBar( + content: Text(AppLocalizations.of(context).translate('login.reconnect_success')), + backgroundColor: const Color(0xFF00B42A), + duration: const Duration(seconds: 2), ), ); } diff --git a/lib/features/auth/presentation/pages/login_page.dart b/lib/features/auth/presentation/pages/login_page.dart index d2a241b5..6901f5bb 100644 --- a/lib/features/auth/presentation/pages/login_page.dart +++ b/lib/features/auth/presentation/pages/login_page.dart @@ -4,6 +4,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart'; import 'package:maibu_satabot_v2/core/di/injection.dart'; +import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; +import 'package:maibu_satabot_v2/core/localization/locale_cubit.dart'; +import 'package:maibu_satabot_v2/components/language_selection_dialog.dart'; import 'package:maibu_satabot_v2/core/theme/AppTheme.dart'; import 'package:maibu_satabot_v2/features/devices/presentation/bloc/devices_cubit.dart'; @@ -37,12 +40,25 @@ class _LoginPageState extends State with SingleTickerProviderStateMix // 等首帧渲染 + 图片解码完成后再播放,避开启动初始化争抢导致的掉帧 WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; + _maybeShowLanguageDialog(); Future.delayed(const Duration(milliseconds: 120), () { if (mounted) _entranceController.forward(); }); }); } + /// 进入登录页:若用户从未选择过语言,弹出强制语言选择框。 + /// 选择后 LocaleCubit.setLocale 触发 MaterialApp 重建,登录页立即显示对应语言。 + void _maybeShowLanguageDialog() { + final localeCubit = context.read(); + if (localeCubit.hasChosenLanguage) return; + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => const LanguageSelectionDialog(mandatory: true), + ); + } + @override void dispose() { _entranceController.dispose(); @@ -74,7 +90,7 @@ class _LoginPageState extends State with SingleTickerProviderStateMix debugPrint('✅ [LOGIN] 已调用 context.go(RoutePaths.home)'); } else if (state is LoginFailure) { // 可以在这里弹出简洁的黑白提示框 - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('登录失败,${state.message}'))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('${AppLocalizations.of(context).translate('login.login_failed')},${state.message}'))); } }, child: LayoutBuilder( @@ -109,7 +125,7 @@ class _LoginPageState extends State with SingleTickerProviderStateMix Padding( padding: const EdgeInsets.symmetric(horizontal: 20.0), child: Text( - "通过华为云生态技术认证,请放心使用,证书编号C202605301", + AppLocalizations.of(context).translate('login.cert_text'), textAlign: TextAlign.center, style: TextStyle( fontSize: 10, @@ -132,11 +148,11 @@ class _LoginPageState extends State with SingleTickerProviderStateMix crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "账号登录", + AppLocalizations.of(context).translate('login.account_login_title'), style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: context.appColors.textPrimary), ), const SizedBox(height: 8), - Text("请填写以下信息以验证身份", style: TextStyle(fontSize: 15, color: context.appColors.textTertiary)), + Text(AppLocalizations.of(context).translate('login.subtitle'), style: TextStyle(fontSize: 15, color: context.appColors.textTertiary)), ], ), ), @@ -150,12 +166,12 @@ class _LoginPageState extends State with SingleTickerProviderStateMix child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildInputLabel("账号"), + _buildInputLabel(AppLocalizations.of(context).translate('login.username')), TextField( controller: _userCtrl, style: TextStyle(fontSize: 14, color: context.appColors.textPrimary), cursorColor: context.appColors.textPrimary, - decoration: _inputDecoration(hint: "请输入用户名"), + decoration: _inputDecoration(hint: AppLocalizations.of(context).translate('login.username_hint')), ), ], ), @@ -169,14 +185,14 @@ class _LoginPageState extends State with SingleTickerProviderStateMix child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildInputLabel("密码"), + _buildInputLabel(AppLocalizations.of(context).translate('login.password')), TextField( controller: _pwdCtrl, obscureText: _obscurePwd, style: TextStyle(fontSize: 14, color: context.appColors.textPrimary), cursorColor: context.appColors.textPrimary, decoration: _inputDecoration( - hint: "请输入密码", + hint: AppLocalizations.of(context).translate('login.password_hint'), suffixIcon: IconButton( icon: Icon(_obscurePwd ? Icons.visibility_off_outlined : Icons.visibility_outlined, color: context.appColors.textTertiary, size: 20), onPressed: () => setState(() => _obscurePwd = !_obscurePwd), @@ -212,16 +228,18 @@ class _LoginPageState extends State with SingleTickerProviderStateMix : () { if (!_isAgreed) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('请先阅读并同意用户协议'), - duration: Duration(seconds: 1), + SnackBar( + content: Text(AppLocalizations.of(context).translate('login.agree_first')), + duration: const Duration(seconds: 1), ), ); return; } _handleLogin(); }, - text: isLoading ? '登录中...' : '登 录', + text: isLoading + ? AppLocalizations.of(context).translate('login.logging_in') + : AppLocalizations.of(context).translate('login.login_submit'), ); }, ), @@ -237,7 +255,7 @@ class _LoginPageState extends State with SingleTickerProviderStateMix mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - "没有账号吗?", + AppLocalizations.of(context).translate('login.no_account'), style: TextStyle(fontSize: 14, color: context.appColors.textTertiary), ), InkWell( @@ -250,7 +268,7 @@ class _LoginPageState extends State with SingleTickerProviderStateMix child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: Text( - "立即注册", + AppLocalizations.of(context).translate('login.register_now'), style: TextStyle( fontSize: 14, color: context.appColors.textPrimary, @@ -335,10 +353,10 @@ class _LoginPageState extends State with SingleTickerProviderStateMix Expanded( child: Wrap( children: [ - Text("我已阅读并同意 ", style: TextStyle(fontSize: 13, color: context.appColors.textTertiary)), - _protocolText("《用户协议》", () => _showProtocolDetail("用户协议")), - Text(" 和 ", style: TextStyle(fontSize: 13, color: context.appColors.textTertiary)), - _protocolText("《隐私政策》", () => _showProtocolDetail("隐私政策")), + Text(AppLocalizations.of(context).translate('login.agree_prefix'), style: TextStyle(fontSize: 13, color: context.appColors.textTertiary)), + _protocolText(AppLocalizations.of(context).translate('login.user_agreement_link'), () => _showProtocolDetail(AppLocalizations.of(context).translate('login.user_agreement_link'))), + Text(AppLocalizations.of(context).translate('login.agree_and'), style: TextStyle(fontSize: 13, color: context.appColors.textTertiary)), + _protocolText(AppLocalizations.of(context).translate('login.privacy_policy_link'), () => _showProtocolDetail(AppLocalizations.of(context).translate('login.privacy_policy_link'))), ], ), ), @@ -371,7 +389,10 @@ class _LoginPageState extends State with SingleTickerProviderStateMix Divider(height: 32, color: context.appColors.divider), Expanded( child: SingleChildScrollView( - child: Text("此处放置您的${title}详细内容...\n" * 20, style: TextStyle(color: context.appColors.textSecondary, height: 1.5)), + child: Text( + (AppLocalizations.of(context).translate('login.protocol_detail_text').replaceAll('%s', title) + '\n') * 20, + style: TextStyle(color: context.appColors.textSecondary, height: 1.5), + ), ), ), const SizedBox(height: 16), @@ -379,7 +400,7 @@ class _LoginPageState extends State with SingleTickerProviderStateMix width: double.infinity, child: BlocBuilder( builder: (context, state) { - return CCPrimaryButton(onPressed: () => Navigator.pop(context), text: "我已了解"); + return CCPrimaryButton(onPressed: () => Navigator.pop(context), text: AppLocalizations.of(context).translate('login.understood')); }, ), ), diff --git a/lib/features/main_container/presentation/main_wrapper.dart b/lib/features/main_container/presentation/main_wrapper.dart index e1866ef0..16311664 100644 --- a/lib/features/main_container/presentation/main_wrapper.dart +++ b/lib/features/main_container/presentation/main_wrapper.dart @@ -287,7 +287,16 @@ class _MainWrapperState extends State { ? FontWeight.w600 : FontWeight.w400, ), - child: Text(item.name), + child: Text( + // 按当前语言取 Tab 名:en 用 nameEn,其余用 name + // 通过 Localizations.localeOf 注册依赖,语言切换时底部导航随之重建 + Localizations.localeOf( + context, + ).languageCode == + 'en' + ? item.nameEn + : item.name, + ), ), ], ), diff --git a/lib/features/main_container/presentation/widgets/custom_bottom_nav_bar.dart b/lib/features/main_container/presentation/widgets/custom_bottom_nav_bar.dart index b87e75fc..1a6ddf87 100644 --- a/lib/features/main_container/presentation/widgets/custom_bottom_nav_bar.dart +++ b/lib/features/main_container/presentation/widgets/custom_bottom_nav_bar.dart @@ -117,7 +117,9 @@ class _CustomBottomNavBarState extends State child: _buildNavItem( context, icon: _getIconData(tab.icon), - label: tab.name, + label: Localizations.localeOf(context).languageCode == 'en' + ? tab.nameEn + : tab.name, isSelected: isSelected, onTap: () => context.read().selectTab(index), diff --git a/lib/features/my/presentation/pages/my_page.dart b/lib/features/my/presentation/pages/my_page.dart index 5dcd7e1c..7920a6cc 100644 --- a/lib/features/my/presentation/pages/my_page.dart +++ b/lib/features/my/presentation/pages/my_page.dart @@ -7,6 +7,7 @@ import 'package:maibu_satabot_v2/components/BannerCarousel.dart'; import 'package:maibu_satabot_v2/core/di/injection.dart'; import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; import 'package:maibu_satabot_v2/core/localization/locale_cubit.dart'; +import 'package:maibu_satabot_v2/components/language_selection_dialog.dart'; import 'package:maibu_satabot_v2/features/my/presentation/bloc/my_cubit.dart'; import 'package:maibu_satabot_v2/features/my/repository/my_repository.dart'; import 'package:maibu_satabot_v2/features/my/usecases/updatename_usecase.dart'; @@ -129,37 +130,17 @@ class MyPage extends StatelessWidget { // 分割线 Divider(height: 1, color: Colors.grey.withOpacity(0.3)), - // 🔥 语言切换 + // 🔥 语言切换(统一复用语言选择弹窗) BlocBuilder( builder: (context, locale) { return ListTile( leading: const Icon(Icons.language, color: Colors.black), - title: Text(AppLocalizations.of(context).translate('my.language'), style: TextStyle(color: Colors.black)), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - TextButton( - onPressed: () => context.read().setLocale(const Locale('zh', 'CN')), - child: Text( - AppLocalizations.of(context).translate('my.chinese'), - style: TextStyle( - color: locale.languageCode == 'zh' ? Colors.blue : Colors.grey, - fontWeight: locale.languageCode == 'zh' ? FontWeight.bold : FontWeight.normal, - ), - ), - ), - const SizedBox(width: 8), - TextButton( - onPressed: () => context.read().setLocale(const Locale('en', 'US')), - child: Text( - AppLocalizations.of(context).translate('my.english'), - style: TextStyle( - color: locale.languageCode == 'en' ? Colors.blue : Colors.grey, - fontWeight: locale.languageCode == 'en' ? FontWeight.bold : FontWeight.normal, - ), - ), - ), - ], + title: Text(AppLocalizations.of(context).translate('my.language'), style: const TextStyle(color: Colors.black)), + subtitle: Text(AppLanguages.instance.nameOf(locale)), + trailing: const Icon(Icons.arrow_forward_ios, size: 16, color: Colors.grey), + onTap: () => showDialog( + context: context, + builder: (_) => const LanguageSelectionDialog(mandatory: false), ), ); }, diff --git a/lib/features/v2/device_list/presentation/pages/device_status_page.dart b/lib/features/v2/device_list/presentation/pages/device_status_page.dart index ad56e4cd..4a08fb01 100644 --- a/lib/features/v2/device_list/presentation/pages/device_status_page.dart +++ b/lib/features/v2/device_list/presentation/pages/device_status_page.dart @@ -1456,22 +1456,22 @@ class _DeviceStatusViewState extends State { builder: (dialogContext) { dialogShown = true; return AlertDialog( - title: const Text('未找到设备'), - content: const Text('暂未匹配到该设备,稍后重试'), + title: Text(AppLocalizations.of(dialogContext).translate('device_list_v2.device_not_found_title')), + content: Text(AppLocalizations.of(dialogContext).translate('device_list_v2.device_not_found_content')), actions: [ TextButton( onPressed: () { dialogShown = false; Navigator.pop(dialogContext); }, - child: const Text('取消'), + child: Text(AppLocalizations.of(dialogContext).translate('common.cancel')), ), TextButton( onPressed: () { dialogShown = false; Navigator.pop(dialogContext); }, - child: const Text('确认'), + child: Text(AppLocalizations.of(dialogContext).translate('common.confirm')), ), ], ); diff --git a/lib/features/v2/device_list/presentation/pages/robot_control_page.dart b/lib/features/v2/device_list/presentation/pages/robot_control_page.dart index b888b17a..6c623dcd 100644 --- a/lib/features/v2/device_list/presentation/pages/robot_control_page.dart +++ b/lib/features/v2/device_list/presentation/pages/robot_control_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; import 'package:maibu_satabot_v2/components/capsule_toast.dart'; +import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; import 'package:maibu_satabot_v2/core/theme/AppTheme.dart'; import 'package:maibu_satabot_v2/features/remote_control/presentation/bloc/remote_control_cubit.dart'; import '../../../../../core/router/route_paths.dart'; @@ -25,16 +26,28 @@ class RobotControlPage extends StatefulWidget { class _RobotControlPageState extends State { Map get robot => widget.robot; + /// 缓存本地化实例:dispose 后的异步回调中 context 已失效,需提前捕获 + AppLocalizations? _loc; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _loc = AppLocalizations.of(context); + } + @override void dispose() { // 🔥 退出详情页返回设备列表时释放远程控制权限(type=1) final deviceId = robot['name']?.toString(); // 长序列号 deviceName if (deviceId != null && deviceId.isNotEmpty) { + final loc = _loc; GetIt.I() .releasePermission(deviceId: deviceId, type: 1) .then((success) { CapsuleToast.show( - success ? '已释放远程控制权限' : '控制权限释放失败', + loc!.translate(success + ? 'device_list_v2.release_permission_success' + : 'device_list_v2.release_permission_failed'), showCheck: success, ); }); @@ -59,7 +72,7 @@ class _RobotControlPageState extends State { Text( robot['alias'] != null && (robot['alias'] as String).isNotEmpty ? robot['alias'] - : '暂无别名', + : AppLocalizations.of(context).translate('device_list_v2.no_alias'), style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, @@ -166,7 +179,7 @@ class _RobotControlPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '路径规划', + AppLocalizations.of(context).translate('device_list_v2.path_planning'), style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, @@ -175,7 +188,7 @@ class _RobotControlPageState extends State { ), const SizedBox(height: 4), Text( - '查看和编辑机器人巡检路径', + AppLocalizations.of(context).translate('device_list_v2.path_planning_desc'), style: TextStyle( fontSize: 12, color: context.appColors.textTertiary, diff --git a/lib/features/v2/device_list/presentation/pages/robot_list_page.dart b/lib/features/v2/device_list/presentation/pages/robot_list_page.dart index 34475753..bac5f8ff 100644 --- a/lib/features/v2/device_list/presentation/pages/robot_list_page.dart +++ b/lib/features/v2/device_list/presentation/pages/robot_list_page.dart @@ -886,22 +886,22 @@ class _RobotListViewState extends State { builder: (dialogContext) { dialogShown = true; return AlertDialog( - title: const Text('未找到设备'), - content: const Text('暂未匹配到该设备,稍后重试'), + title: Text(AppLocalizations.of(dialogContext).translate('device_list_v2.device_not_found_title')), + content: Text(AppLocalizations.of(dialogContext).translate('device_list_v2.device_not_found_content')), actions: [ TextButton( onPressed: () { dialogShown = false; Navigator.pop(dialogContext); }, - child: const Text('取消'), + child: Text(AppLocalizations.of(dialogContext).translate('common.cancel')), ), TextButton( onPressed: () { dialogShown = false; Navigator.pop(dialogContext); }, - child: const Text('确认'), + child: Text(AppLocalizations.of(dialogContext).translate('common.confirm')), ), ], ); diff --git a/lib/features/v2/device_list/presentation/widgets/robot_action_buttons.dart b/lib/features/v2/device_list/presentation/widgets/robot_action_buttons.dart index ad854b04..2e8b3fed 100644 --- a/lib/features/v2/device_list/presentation/widgets/robot_action_buttons.dart +++ b/lib/features/v2/device_list/presentation/widgets/robot_action_buttons.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; import 'package:maibu_satabot_v2/core/theme/AppTheme.dart'; /// 底部操作按钮 @@ -13,7 +14,7 @@ class RobotActionButtons extends StatelessWidget { children: [ Expanded( child: _buildActionButton( - label: '开始', + label: AppLocalizations.of(context).translate('device_list_v2.action_start'), color: context.appColors.primary, onTap: () {}, ), @@ -21,7 +22,7 @@ class RobotActionButtons extends StatelessWidget { const SizedBox(width: 12), Expanded( child: _buildActionButton( - label: '暂停', + label: AppLocalizations.of(context).translate('device_list_v2.action_pause'), color: context.appColors.warning, onTap: () {}, ), @@ -29,7 +30,7 @@ class RobotActionButtons extends StatelessWidget { const SizedBox(width: 12), Expanded( child: _buildActionButton( - label: '返航', + label: AppLocalizations.of(context).translate('device_list_v2.action_return'), color: context.appColors.cardBackground, borderColor: context.appColors.textTertiary, textColor: context.appColors.textSecondary, diff --git a/lib/features/v2/device_list/presentation/widgets/robot_checklist.dart b/lib/features/v2/device_list/presentation/widgets/robot_checklist.dart index dafb3a4e..35e7fd6c 100644 --- a/lib/features/v2/device_list/presentation/widgets/robot_checklist.dart +++ b/lib/features/v2/device_list/presentation/widgets/robot_checklist.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; import 'package:maibu_satabot_v2/core/theme/AppTheme.dart'; /// 检查清单 @@ -26,7 +27,7 @@ class RobotChecklist extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '检查清单', + AppLocalizations.of(context).translate('device_list_v2.checklist_title'), style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold, diff --git a/lib/features/v2/device_list/presentation/widgets/robot_control_panel.dart b/lib/features/v2/device_list/presentation/widgets/robot_control_panel.dart index 5e9262f4..393a42f6 100644 --- a/lib/features/v2/device_list/presentation/widgets/robot_control_panel.dart +++ b/lib/features/v2/device_list/presentation/widgets/robot_control_panel.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; import 'package:maibu_satabot_v2/core/theme/AppTheme.dart'; import '../../../../../../core/di/injection.dart'; import '../../../../devices/presentation/bloc/devices_cubit.dart'; @@ -76,7 +77,7 @@ class RobotControlPanel extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '控制面板', + AppLocalizations.of(context).translate('device_list_v2.control_panel'), style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold, @@ -148,17 +149,17 @@ class RobotControlPanel extends StatelessWidget { const SizedBox(height: 12), // 速度档位 Text( - '速度档位', + AppLocalizations.of(context).translate('device_list_v2.speed_gear'), style: TextStyle(fontSize: 13, color: context.appColors.textTertiary), ), const SizedBox(height: 12), Row( children: [ - Expanded(child: _buildSpeedButton(context, '低速', false, () {})), + Expanded(child: _buildSpeedButton(context, AppLocalizations.of(context).translate('device_list_v2.speed_low'), false, () {})), const SizedBox(width: 12), - Expanded(child: _buildSpeedButton(context, '中速', true, () {})), + Expanded(child: _buildSpeedButton(context, AppLocalizations.of(context).translate('device_list_v2.speed_mid'), true, () {})), const SizedBox(width: 12), - Expanded(child: _buildSpeedButton(context, '高速', false, () {})), + Expanded(child: _buildSpeedButton(context, AppLocalizations.of(context).translate('device_list_v2.speed_high'), false, () {})), ], ), ], diff --git a/lib/features/v2/device_list/presentation/widgets/robot_header_card.dart b/lib/features/v2/device_list/presentation/widgets/robot_header_card.dart index 225c06a6..3a67d132 100644 --- a/lib/features/v2/device_list/presentation/widgets/robot_header_card.dart +++ b/lib/features/v2/device_list/presentation/widgets/robot_header_card.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart'; import 'package:maibu_satabot_v2/core/app/app_user_state.dart'; // 🔥 导入 AppUserState import 'package:maibu_satabot_v2/core/consts/tcp_consts.dart'; +import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; import 'package:maibu_satabot_v2/core/theme/AppTheme.dart'; import 'package:maibu_satabot_v2/features/remote_control/presentation/widgets/webrtc/webrtc_local_player.dart'; import '../../../device_run_param/presentation/pages/robot_param_settings_page.dart'; @@ -30,19 +31,19 @@ class _RobotHeaderCardState extends State { // 视角配置 final List> _viewConfigs = [ - {'name': '前视', 'alignment': Alignment.topLeft, 'icon': Icons.arrow_upward}, + {'name': 'view_front', 'alignment': Alignment.topLeft, 'icon': Icons.arrow_upward}, { - 'name': '后视', + 'name': 'view_back', 'alignment': Alignment.topRight, 'icon': Icons.arrow_downward, }, - {'name': '左视', 'alignment': Alignment.bottomLeft, 'icon': Icons.arrow_back}, + {'name': 'view_left', 'alignment': Alignment.bottomLeft, 'icon': Icons.arrow_back}, { - 'name': '右视', + 'name': 'view_right', 'alignment': Alignment.bottomRight, 'icon': Icons.arrow_forward, }, - {'name': '全景', 'alignment': Alignment.center, 'icon': Icons.view_agenda}, + {'name': 'view_panorama', 'alignment': Alignment.center, 'icon': Icons.view_agenda}, ]; @override @@ -114,7 +115,7 @@ class _RobotHeaderCardState extends State { Text( (widget.robot['alias'] as String?)?.isNotEmpty == true ? widget.robot['alias'] as String - : '暂无别名', + : AppLocalizations.of(context).translate('device_list_v2.no_alias'), style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, @@ -147,7 +148,7 @@ class _RobotHeaderCardState extends State { ), const SizedBox(width: 4), Text( - '强', + AppLocalizations.of(context).translate('device_list_v2.signal_strong'), style: TextStyle( fontSize: 12, color: context.appColors.success, @@ -165,7 +166,7 @@ class _RobotHeaderCardState extends State { borderRadius: BorderRadius.circular(4), ), child: Text( - '在线', + AppLocalizations.of(context).translate('device_list_v2.online'), style: TextStyle( fontSize: 12, color: context.appColors.success, @@ -286,7 +287,7 @@ class _RobotHeaderCardState extends State { Icon(config['icon'] as IconData, size: 16), const SizedBox(height: 2), Text( - config['name'] as String, + AppLocalizations.of(context).translate('device_list_v2.${config['name']}'), style: const TextStyle(fontSize: 10), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -323,9 +324,9 @@ class _RobotHeaderCardState extends State { if (showNoStreamHint) ...[ const Icon(Icons.videocam_off, size: 36, color: Colors.white38), const SizedBox(height: 8), - const Text( - '暂无视频', - style: TextStyle( + Text( + AppLocalizations.of(context).translate('device_list_v2.no_video'), + style: const TextStyle( color: Colors.white54, fontSize: 13, fontWeight: FontWeight.w500, @@ -363,9 +364,9 @@ class _RobotHeaderCardState extends State { ), const SizedBox(height: 10), // “播放”文字 - const Text( - '播放', - style: TextStyle( + Text( + AppLocalizations.of(context).translate('device_list_v2.play'), + style: const TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500, diff --git a/lib/features/v2/device_list/presentation/widgets/robot_item_card.dart b/lib/features/v2/device_list/presentation/widgets/robot_item_card.dart index 3fbf7d49..c0e0511d 100644 --- a/lib/features/v2/device_list/presentation/widgets/robot_item_card.dart +++ b/lib/features/v2/device_list/presentation/widgets/robot_item_card.dart @@ -74,7 +74,7 @@ class RobotItemCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - alias != null && alias!.isNotEmpty ? alias! : '暂无别名', + alias != null && alias!.isNotEmpty ? alias! : AppLocalizations.of(context).translate('device_list_v2.no_alias'), style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, @@ -158,7 +158,7 @@ class RobotItemCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - task, + _displayTask(context, task), style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, @@ -212,6 +212,20 @@ class RobotItemCard extends StatelessWidget { ); } + /// 🔥 仅做显示层映射:把内部语义状态值转成当前语言文案,未命中则原样显示 + /// 注意:model 默认值与列表过滤逻辑仍使用 '待机中'/'充电中',此处不改数据 + String _displayTask(BuildContext context, String task) { + final loc = AppLocalizations.of(context); + switch (task) { + case '待机中': + return loc.translate('device_list_v2.task_standby'); + case '充电中': + return loc.translate('device_list_v2.task_charging'); + default: + return task; + } + } + String _getRobotImage() { switch (type) { case 'inspection': diff --git a/lib/features/v2/device_list/presentation/widgets/robot_status_bar.dart b/lib/features/v2/device_list/presentation/widgets/robot_status_bar.dart index 8ceca2ee..55e6af9c 100644 --- a/lib/features/v2/device_list/presentation/widgets/robot_status_bar.dart +++ b/lib/features/v2/device_list/presentation/widgets/robot_status_bar.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:get_it/get_it.dart'; +import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; import 'package:maibu_satabot_v2/core/theme/AppTheme.dart'; import '../../../../../components/device_status_modal.dart'; import '../../../../devices/presentation/bloc/device_status_bloc.dart'; @@ -111,21 +112,21 @@ class RobotStatusBar extends StatelessWidget { children: [ _buildStatusItem( context, - label: '转速', + label: AppLocalizations.of(context).translate('device_list_v2.status_speed'), value: speed, unit: 'rpm', ), Container(width: 1, height: 40, color: context.appColors.divider), _buildStatusItem( context, - label: '模式', + label: AppLocalizations.of(context).translate('device_list_v2.status_mode'), value: mode, unit: '', ), Container(width: 1, height: 40, color: context.appColors.divider), _buildStatusItem( context, - label: '电量', + label: AppLocalizations.of(context).translate('device_list_v2.battery_level'), value: battery, unit: '%', valueColor: batteryColor, @@ -133,7 +134,7 @@ class RobotStatusBar extends StatelessWidget { Container(width: 1, height: 40, color: context.appColors.divider), _buildStatusItem( context, - label: '信号', + label: AppLocalizations.of(context).translate('device_list_v2.status_signal'), value: signal, unit: '', valueColor: signalColor, diff --git a/lib/features/v2/device_list/presentation/widgets/robot_task_info.dart b/lib/features/v2/device_list/presentation/widgets/robot_task_info.dart index 343240a8..9b02343b 100644 --- a/lib/features/v2/device_list/presentation/widgets/robot_task_info.dart +++ b/lib/features/v2/device_list/presentation/widgets/robot_task_info.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; import 'package:maibu_satabot_v2/core/theme/AppTheme.dart'; /// 当前任务信息 @@ -26,7 +27,7 @@ class RobotTaskInfo extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '当前任务', + AppLocalizations.of(context).translate('device_list_v2.current_task'), style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold, @@ -53,7 +54,7 @@ class RobotTaskInfo extends StatelessWidget { borderRadius: BorderRadius.circular(4), ), child: Text( - '进行中', + AppLocalizations.of(context).translate('device_list_v2.task_in_progress'), style: TextStyle( fontSize: 11, color: context.appColors.primary, diff --git a/lib/features/v2/my/presentation/pages/help_feedback_page.dart b/lib/features/v2/my/presentation/pages/help_feedback_page.dart index bb4a48be..7f76c8ce 100644 --- a/lib/features/v2/my/presentation/pages/help_feedback_page.dart +++ b/lib/features/v2/my/presentation/pages/help_feedback_page.dart @@ -21,76 +21,46 @@ class _HelpFeedbackPageState extends State List _selectedImages = []; static const _feedbackTypes = [ - {'icon': Icons.lightbulb_outline, 'label': '功能建议'}, - {'icon': Icons.bug_report_outlined, 'label': 'Bug报告'}, - {'icon': Icons.sentiment_dissatisfied_outlined, 'label': '体验问题'}, - {'icon': Icons.more_horiz, 'label': '其他'}, + {'icon': Icons.lightbulb_outline, 'label': 'type_feature'}, + {'icon': Icons.bug_report_outlined, 'label': 'type_bug'}, + {'icon': Icons.sentiment_dissatisfied_outlined, 'label': 'type_experience'}, + {'icon': Icons.more_horiz, 'label': 'type_other'}, ]; static const _faqList = [ { - 'category': '设备绑定', + 'category': 'cat_bind', 'questions': [ - { - 'q': '扫码绑定失败怎么办?', - 'a': '请检查设备是否已开机并处于待绑定状态,确保手机蓝牙已开启且网络正常。如仍无法绑定,请尝试重启设备后重新扫码。' - }, - { - 'q': '搜不到设备蓝牙信号?', - 'a': '请确认设备已开机且蓝牙指示灯正常闪烁。尝试靠近设备(1米以内)后重新搜索。如多个设备同时绑定,请逐一操作。' - }, + {'q': 'q_bind_fail', 'a': 'a_bind_fail'}, + {'q': 'q_bt_notfound', 'a': 'a_bt_notfound'}, ] }, { - 'category': '路径规划', + 'category': 'cat_path', 'questions': [ - { - 'q': '无法生成路径?', - 'a': '请确保已绘制完整的作业区域(至少3个打点),并检查GPS信号是否良好。如果使用自动规划模式,请确认区域边界没有自相交。' - }, - { - 'q': '地图不显示或定位偏移?', - 'a': '请检查手机GPS是否已开启,并授权App位置权限。在室内或信号较弱的区域可能出现定位偏移,建议到开阔区域使用。' - }, + {'q': 'q_path_none', 'a': 'a_path_none'}, + {'q': 'q_map_issue', 'a': 'a_map_issue'}, ] }, { - 'category': '远程控制', + 'category': 'cat_remote', 'questions': [ - { - 'q': '远程视频卡顿/连接超时?', - 'a': '请检查手机和设备的网络连接是否稳定。建议在4G/Wi-Fi信号良好的环境下使用远程控制功能。如持续卡顿,可尝试降低视频分辨率。' - }, - { - 'q': '急停功能怎么使用?', - 'a': '在远程控制页面,点击屏幕中央红色急停按钮即可立即停止设备运行。急停后需手动解除才能恢复操作。' - }, + {'q': 'q_video_stutter', 'a': 'a_video_stutter'}, + {'q': 'q_estop', 'a': 'a_estop'}, ] }, { - 'category': '工单系统', + 'category': 'cat_order', 'questions': [ - { - 'q': '如何提交工单?', - 'a': '在「工单」页面点击右下角「新建工单」按钮,选择工单类型,填写问题描述并上传相关图片后提交即可。' - }, - { - 'q': '工单状态怎么看?', - 'a': '在「工单」页面可查看所有已提交的工单及其当前状态(待处理/处理中/已完成)。点击具体工单可查看详情和回复。' - }, + {'q': 'q_order_submit', 'a': 'a_order_submit'}, + {'q': 'q_order_status', 'a': 'a_order_status'}, ] }, { - 'category': '账号问题', + 'category': 'cat_account', 'questions': [ - { - 'q': '忘记密码怎么办?', - 'a': '在登录页面点击「忘记密码」,通过注册手机号验证后即可重置密码。' - }, - { - 'q': '换设备登录需要重新绑定?', - 'a': '更换手机登录同一账号后,设备绑定关系不会丢失。但需要在新设备上重新搜索和连接设备。' - }, + {'q': 'q_pwd_forget', 'a': 'a_pwd_forget'}, + {'q': 'q_rebind_device', 'a': 'a_rebind_device'}, ] }, ]; @@ -139,9 +109,9 @@ class _HelpFeedbackPageState extends State unselectedLabelColor: context.appColors.textTertiary, indicatorColor: const Color(0xFF165DFF), indicatorSize: TabBarIndicatorSize.label, - tabs: const [ - Tab(text: '常见问题'), - Tab(text: '意见反馈'), + tabs: [ + Tab(text: AppLocalizations.of(context).translate('help_feedback.tab_faq')), + Tab(text: AppLocalizations.of(context).translate('help_feedback.tab_feedback')), ], ), ), @@ -177,7 +147,7 @@ class _HelpFeedbackPageState extends State Padding( padding: const EdgeInsets.only(top: 8, bottom: 8), child: Text( - categoryData['category'] as String, + AppLocalizations.of(context).translate("help_feedback.${categoryData['category']}"), style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, @@ -203,8 +173,8 @@ class _HelpFeedbackPageState extends State final item = entry.value as Map; final isLast = index == questions.length - 1; return _buildFAQItem( - item['q'] as String, - item['a'] as String, + AppLocalizations.of(context).translate("help_feedback.${item['q']}"), + AppLocalizations.of(context).translate("help_feedback.${item['a']}"), isLast, ); }).toList(), @@ -275,7 +245,7 @@ class _HelpFeedbackPageState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '联系客服', + AppLocalizations.of(context).translate('help_feedback.contact_title'), style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, @@ -285,15 +255,15 @@ class _HelpFeedbackPageState extends State const SizedBox(height: 16), _buildContactItem( Icons.phone_outlined, - '客服热线', + AppLocalizations.of(context).translate('help_feedback.hotline'), '400-000-0000', onTap: () => _showPhoneDialog('400-000-0000'), ), const Divider(height: 24), _buildContactItem( Icons.qr_code_2, - '企业微信', - '扫码添加客服微信', + AppLocalizations.of(context).translate('help_feedback.wechat_work'), + AppLocalizations.of(context).translate('help_feedback.wechat_hint'), onTap: () => debugPrint('显示企业微信二维码'), ), ], @@ -350,7 +320,7 @@ class _HelpFeedbackPageState extends State children: [ // 反馈类型 _buildSectionCard( - title: '反馈类型', + title: AppLocalizations.of(context).translate('help_feedback.feedback_type'), child: Wrap( spacing: 8, runSpacing: 8, @@ -389,7 +359,7 @@ class _HelpFeedbackPageState extends State ), const SizedBox(width: 6), Text( - type['label'] as String, + AppLocalizations.of(context).translate("help_feedback.${type['label']}"), style: TextStyle( fontSize: 13, color: isSelected @@ -410,13 +380,13 @@ class _HelpFeedbackPageState extends State // 问题描述 _buildSectionCard( - title: '问题描述', + title: AppLocalizations.of(context).translate('help_feedback.problem_desc'), child: TextField( controller: _feedbackController, maxLines: 5, maxLength: 500, decoration: InputDecoration( - hintText: '请详细描述您遇到的问题或建议...', + hintText: AppLocalizations.of(context).translate('help_feedback.problem_hint'), hintStyle: TextStyle( fontSize: 14, color: context.appColors.textTertiary, @@ -446,7 +416,8 @@ class _HelpFeedbackPageState extends State // 添加截图 _buildSectionCard( - title: '添加截图 (${_selectedImages.length}/6)', + title: + '${AppLocalizations.of(context).translate('help_feedback.add_screenshot')} (${_selectedImages.length}/6)', child: Wrap( spacing: 8, runSpacing: 8, @@ -462,11 +433,11 @@ class _HelpFeedbackPageState extends State // 联系方式 _buildSectionCard( - title: '联系方式(选填)', + title: AppLocalizations.of(context).translate('help_feedback.contact_label'), child: TextField( controller: _contactController, decoration: InputDecoration( - hintText: '手机号/微信号,方便我们回访', + hintText: AppLocalizations.of(context).translate('help_feedback.contact_hint'), hintStyle: TextStyle( fontSize: 14, color: context.appColors.textTertiary, @@ -516,9 +487,9 @@ class _HelpFeedbackPageState extends State borderRadius: BorderRadius.circular(12), ), ), - child: const Text( - '提交反馈', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + child: Text( + AppLocalizations.of(context).translate('help_feedback.submit_feedback'), + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), ), ), ), @@ -630,7 +601,7 @@ class _HelpFeedbackPageState extends State ), const SizedBox(height: 4), Text( - '添加截图', + AppLocalizations.of(context).translate('help_feedback.add_screenshot'), style: TextStyle( fontSize: 11, color: context.appColors.textTertiary, @@ -645,10 +616,11 @@ class _HelpFeedbackPageState extends State // ==================== 交互方法 ==================== void _showPhoneDialog(String phoneNumber) { + final loc = AppLocalizations.of(context); showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('客服热线'), + title: Text(loc.translate('help_feedback.hotline')), content: SelectableText( phoneNumber, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), @@ -656,17 +628,17 @@ class _HelpFeedbackPageState extends State actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('关闭'), + child: Text(loc.translate('help_feedback.close')), ), TextButton( onPressed: () { Clipboard.setData(ClipboardData(text: phoneNumber)); Navigator.pop(context); ScaffoldMessenger.of(this.context).showSnackBar( - const SnackBar(content: Text('号码已复制')), + SnackBar(content: Text(loc.translate('help_feedback.number_copied'))), ); }, - child: const Text('复制号码'), + child: Text(loc.translate('help_feedback.copy_number')), ), ], ), @@ -684,9 +656,9 @@ class _HelpFeedbackPageState extends State void _submitFeedback() { if (_feedbackController.text.trim().isEmpty) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('请输入问题描述'), - backgroundColor: Color(0xFFF53F3F), + SnackBar( + content: Text(AppLocalizations.of(context).translate('help_feedback.please_input_desc')), + backgroundColor: const Color(0xFFF53F3F), ), ); return; @@ -700,7 +672,7 @@ class _HelpFeedbackPageState extends State ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: const Text('反馈已提交,感谢您的支持!'), + content: Text(AppLocalizations.of(context).translate('help_feedback.submit_success')), backgroundColor: context.appColors.success, ), ); diff --git a/lib/features/v2/my/presentation/pages/system_settings_page.dart b/lib/features/v2/my/presentation/pages/system_settings_page.dart index 1ea7767a..5cac40b4 100644 --- a/lib/features/v2/my/presentation/pages/system_settings_page.dart +++ b/lib/features/v2/my/presentation/pages/system_settings_page.dart @@ -5,6 +5,7 @@ import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; import 'package:maibu_satabot_v2/core/localization/locale_cubit.dart'; +import 'package:maibu_satabot_v2/components/language_selection_dialog.dart'; import 'package:maibu_satabot_v2/core/router/route_paths.dart'; import 'package:maibu_satabot_v2/core/theme/AppTheme.dart'; import 'package:maibu_satabot_v2/core/storage/user_storage.dart'; @@ -196,7 +197,10 @@ class _SystemSettingsPageState extends State { children: [ Expanded( child: Text( - tab.name, + // 按当前语言显示 Tab 名 + Localizations.localeOf(context).languageCode == 'en' + ? tab.nameEn + : tab.name, style: TextStyle( fontSize: 14, color: context.appColors.textPrimary, @@ -251,29 +255,40 @@ class _SystemSettingsPageState extends State { color: context.appColors.textPrimary, ), ), - const SizedBox(height: 16), + const SizedBox(height: 12), + // 统一复用语言选择弹窗,只需选一个入口维护语言列表 BlocBuilder( builder: (context, locale) { - return Column( - children: [ - _buildLanguageOption( - AppLocalizations.of(context).translate('my.chinese'), - 'zh', - locale.languageCode == 'zh', - () => context.read().setLocale( - const Locale('zh', 'CN'), + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => showDialog( + context: context, + builder: (_) => const LanguageSelectionDialog(mandatory: false), + ), + child: Row( + children: [ + Icon( + Icons.language, + size: 20, + color: context.appColors.textSecondary, ), - ), - const SizedBox(height: 12), - _buildLanguageOption( - AppLocalizations.of(context).translate('my.english'), - 'en', - locale.languageCode == 'en', - () => context.read().setLocale( - const Locale('en', 'US'), + const SizedBox(width: 8), + Expanded( + child: Text( + AppLanguages.instance.nameOf(locale), + style: TextStyle( + fontSize: 14, + color: context.appColors.textPrimary, + ), + ), ), - ), - ], + Icon( + Icons.arrow_forward_ios, + size: 16, + color: context.appColors.textTertiary, + ), + ], + ), ); }, ), @@ -282,54 +297,6 @@ class _SystemSettingsPageState extends State { ); } - Widget _buildLanguageOption( - String label, - String code, - bool isSelected, - VoidCallback onTap, - ) { - return GestureDetector( - onTap: onTap, - child: Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: isSelected - ? const Color(0xFF165DFF).withOpacity(0.1) - : Colors.transparent, - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: isSelected - ? const Color(0xFF165DFF) - : context.appColors.divider, - width: 1, - ), - ), - child: Row( - children: [ - Expanded( - child: Text( - label, - style: TextStyle( - fontSize: 14, - color: isSelected - ? const Color(0xFF165DFF) - : context.appColors.textPrimary, - fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, - ), - ), - ), - if (isSelected) - const Icon( - Icons.check_circle, - color: Color(0xFF165DFF), - size: 20, - ), - ], - ), - ), - ); - } - /// 版本更新 Widget _buildUpdateSection() { return Container(