多语言切换+首次设置语言
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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": "提交",
|
||||
|
||||
93
lib/components/language_selection_dialog.dart
Normal file
93
lib/components/language_selection_dialog.dart
Normal file
@@ -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<LanguageItem> 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<void> _onSelect(BuildContext context, Locale locale) async {
|
||||
await context.read<LocaleCubit>().setLocale(locale);
|
||||
if (context.mounted) Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final current = context.select<LocaleCubit, Locale>(
|
||||
(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<String>(
|
||||
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('取消'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
2
lib/core/env/env_config.dart
vendored
2
lib/core/env/env_config.dart
vendored
@@ -1,7 +1,7 @@
|
||||
class EnvConfig {
|
||||
static const String environment = String.fromEnvironment(
|
||||
'ENV',
|
||||
defaultValue: 'prod',
|
||||
defaultValue: 'test',
|
||||
);
|
||||
|
||||
static String get sentryDsn {
|
||||
|
||||
@@ -4,33 +4,45 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class LocaleCubit extends Cubit<Locale> {
|
||||
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<void> _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<void> 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';
|
||||
}
|
||||
|
||||
@@ -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<AuthState> {
|
||||
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<AuthState> {
|
||||
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<AuthState> {
|
||||
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<AuthState> {
|
||||
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<AuthState> {
|
||||
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<AuthState> {
|
||||
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<AuthState> {
|
||||
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<AuthState> {
|
||||
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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<LoginPage> 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<LocaleCubit>();
|
||||
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<LoginPage> 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<LoginPage> 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<LoginPage> 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<LoginPage> 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<LoginPage> 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<LoginPage> 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<LoginPage> 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<LoginPage> 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<LoginPage> 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<LoginPage> 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<LoginPage> with SingleTickerProviderStateMix
|
||||
width: double.infinity,
|
||||
child: BlocBuilder<LoginCubit, LoginState>(
|
||||
builder: (context, state) {
|
||||
return CCPrimaryButton(onPressed: () => Navigator.pop(context), text: "我已了解");
|
||||
return CCPrimaryButton(onPressed: () => Navigator.pop(context), text: AppLocalizations.of(context).translate('login.understood'));
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -287,7 +287,16 @@ class _MainWrapperState extends State<MainWrapper> {
|
||||
? 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -117,7 +117,9 @@ class _CustomBottomNavBarState extends State<CustomBottomNavBar>
|
||||
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<TabConfigCubit>().selectTab(index),
|
||||
|
||||
@@ -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<LocaleCubit, Locale>(
|
||||
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<LocaleCubit>().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<LocaleCubit>().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),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1456,22 +1456,22 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
||||
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')),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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<RobotControlPage> {
|
||||
Map<String, dynamic> 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<RemoteControlCubit>()
|
||||
.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<RobotControlPage> {
|
||||
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<RobotControlPage> {
|
||||
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<RobotControlPage> {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'查看和编辑机器人巡检路径',
|
||||
AppLocalizations.of(context).translate('device_list_v2.path_planning_desc'),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: context.appColors.textTertiary,
|
||||
|
||||
@@ -886,22 +886,22 @@ class _RobotListViewState extends State<RobotListView> {
|
||||
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')),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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, () {})),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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<RobotHeaderCard> {
|
||||
|
||||
// 视角配置
|
||||
final List<Map<String, dynamic>> _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<RobotHeaderCard> {
|
||||
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<RobotHeaderCard> {
|
||||
),
|
||||
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<RobotHeaderCard> {
|
||||
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<RobotHeaderCard> {
|
||||
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<RobotHeaderCard> {
|
||||
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<RobotHeaderCard> {
|
||||
),
|
||||
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,
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -21,76 +21,46 @@ class _HelpFeedbackPageState extends State<HelpFeedbackPage>
|
||||
List<String> _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<HelpFeedbackPage>
|
||||
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<HelpFeedbackPage>
|
||||
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<HelpFeedbackPage>
|
||||
final item = entry.value as Map<String, dynamic>;
|
||||
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<HelpFeedbackPage>
|
||||
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<HelpFeedbackPage>
|
||||
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<HelpFeedbackPage>
|
||||
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<HelpFeedbackPage>
|
||||
),
|
||||
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<HelpFeedbackPage>
|
||||
|
||||
// 问题描述
|
||||
_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<HelpFeedbackPage>
|
||||
|
||||
// 添加截图
|
||||
_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<HelpFeedbackPage>
|
||||
|
||||
// 联系方式
|
||||
_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<HelpFeedbackPage>
|
||||
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<HelpFeedbackPage>
|
||||
),
|
||||
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<HelpFeedbackPage>
|
||||
// ==================== 交互方法 ====================
|
||||
|
||||
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<HelpFeedbackPage>
|
||||
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<HelpFeedbackPage>
|
||||
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<HelpFeedbackPage>
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('反馈已提交,感谢您的支持!'),
|
||||
content: Text(AppLocalizations.of(context).translate('help_feedback.submit_success')),
|
||||
backgroundColor: context.appColors.success,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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<SystemSettingsPage> {
|
||||
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<SystemSettingsPage> {
|
||||
color: context.appColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 12),
|
||||
// 统一复用语言选择弹窗,只需选一个入口维护语言列表
|
||||
BlocBuilder<LocaleCubit, Locale>(
|
||||
builder: (context, locale) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildLanguageOption(
|
||||
AppLocalizations.of(context).translate('my.chinese'),
|
||||
'zh',
|
||||
locale.languageCode == 'zh',
|
||||
() => context.read<LocaleCubit>().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<LocaleCubit>().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<SystemSettingsPage> {
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user