多语言切换+首次设置语言

This commit is contained in:
2026-10-12 14:21:09 +08:00
parent dc006c2935
commit 9671798f29
22 changed files with 572 additions and 274 deletions

View File

@@ -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",

View File

@@ -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": "提交",

View 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('取消'),
),
],
),
);
}
}

View File

@@ -1,7 +1,7 @@
class EnvConfig {
static const String environment = String.fromEnvironment(
'ENV',
defaultValue: 'prod',
defaultValue: 'test',
);
static String get sentryDsn {

View File

@@ -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';
}

View File

@@ -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),
),
);
}

View File

@@ -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'));
},
),
),

View File

@@ -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,
),
),
],
),

View File

@@ -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),

View File

@@ -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),
),
);
},

View File

@@ -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')),
),
],
);

View File

@@ -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,

View File

@@ -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')),
),
],
);

View File

@@ -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,

View File

@@ -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,

View File

@@ -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, () {})),
],
),
],

View File

@@ -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,

View File

@@ -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':

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,
),
);

View File

@@ -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(