Browse Source

Merge branch 'new_dev' of https://github.com/deepcloud2048/deep_voice into new_dev

weicu
tanlongsheng 1 year ago
parent
commit
339bc7a41b
  1. 1
      .gitignore
  2. 2
      lib/core/themes/app_theme.dart
  3. 192
      lib/core/translations/language/en_us.dart
  4. 91
      lib/core/translations/language/zh_cn.dart
  5. 7
      lib/data/models/appconfig.dart
  6. 24
      lib/data/models/appconfig_model.dart
  7. 16
      lib/data/models/appconfig_model.g.dart
  8. 5
      lib/data/models/user_Info.dart
  9. 4
      lib/data/services/navigation_manager.dart
  10. 13
      lib/data/services/network/api.dart
  11. 4
      lib/data/services/network/nw_api.dart
  12. 27
      lib/modules/agent/views/music_card.dart
  13. 436
      lib/modules/agent/views/navigation_card.dart
  14. 110
      lib/modules/agent/views/travel_card.dart
  15. 213
      lib/modules/agent/views/weather_card.dart
  16. 14
      lib/modules/devices/controllers/devices_controller.dart
  17. 6
      lib/modules/devices/views/devices_view.dart
  18. 6
      lib/modules/home/controllers/home_controller.dart
  19. 6
      lib/modules/home/views/home_view.dart
  20. 5
      lib/modules/login/controllers/login_controller.dart
  21. 20
      lib/modules/login/views/login_view.dart
  22. 109
      lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart
  23. 310
      lib/modules/ota_upgrade/views/ota_upgrade_view.dart
  24. 27
      lib/modules/pairing/controllers/pairing_controller.dart
  25. 7
      lib/modules/settings/controllers/settings_controller.dart
  26. 49
      lib/modules/settings/views/settings_view.dart
  27. 175
      lib/modules/translation/controllers/translation_controller.dart
  28. 188
      lib/modules/translation/views/translation_detail_view.dart
  29. 91
      lib/modules/translation/views/translation_history_view.dart
  30. 344
      lib/modules/translation/views/translation_view.dart
  31. 14
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  32. 144
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt
  33. 102
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt

1
.gitignore

@ -220,3 +220,4 @@ fastlane/screenshots/**/*.png
fastlane/test_output
fastlane/readme.md
.trae/rules/project_rules.md
.augment/rules/imported/project_rules.md

2
lib/core/themes/app_theme.dart

@ -69,4 +69,4 @@ class AppTheme {
),
),
);
}
}

192
lib/core/translations/language/en_us.dart

@ -46,6 +46,26 @@ const Map<String, String> enUS = {
"malay": "Malay",
"turkish": "Turkish",
"polish": "Polish",
"filipino": "Filipino",
"dutch": "Dutch",
"swedish": "Swedish",
"danish": "Danish",
"finnish": "Finnish",
"czech": "Czech",
"hungarian": "Hungarian",
"norwegian": "Norwegian",
"romanian": "Romanian",
"ukrainian": "Ukrainian",
"greek": "Greek",
"bulgarian": "Bulgarian",
"croatian": "Croatian",
"slovak": "Slovak",
"slovenian": "Slovenian",
"lithuanian": "Lithuanian",
"latvian": "Latvian",
"estonian": "Estonian",
"serbian": "Serbian",
"icelandic": "Icelandic",
// 登录页
"welcome": "Welcome",
@ -68,10 +88,14 @@ const Map<String, String> enUS = {
"invalidPhoneFormat": "Invalid phone number format",
"tip": "Tip",
"pleaseEnterCompleteCode": "Please enter the complete verification code",
// 登录错误提示
"loginFailed": "Login failed, please try again",
'pleaseAcceptPrivacyPolicy':
'Please read and accept the Privacy Policy and User Agreement first',
"enterVerificationCode": "Enter verification code",
"verificationCodeSentTo": "Verification code sent to",
"resendVerificationCode": "Resend verification code",
"login": "Login",
"usePasswordLogin": "Use password login",
// 首页
"deviceStatus": "Device Status",
@ -116,6 +140,12 @@ const Map<String, String> enUS = {
"unknown": "Unknown",
'bluetoothTurnedOff': 'Bluetooth is turned off, please turn it on',
'bluetoothOff': 'bluetooth is off',
'leftEar': 'Left Ear',
'rightEar': 'Right Ear',
'chargingCase': 'Charging Case',
'left': 'Left',
'right': 'Right',
'checkConnectionStatusFailed': 'Failed to check connection status',
// 发现页
"searchAi": "Search AI features...",
@ -127,6 +157,9 @@ const Map<String, String> enUS = {
"allRoundAssistant": "All-round Assistant",
"allRoundAssistantInfo": "Helps solve various problems",
"simultaneousTranslation": "Real-time Translation",
"faceToFaceTranslation": "Face-to-face Translation",
'audioVideoTranslation': 'Audio/Video Translation',
'callTranslation': 'Call Translation',
"simultaneousTranslationInfo": "Instant translation for 80+ languages",
"aiGirlfriend": "Parent-child bondingn",
"aiGirlfriendInfo": "Intelligent company, chat at any time",
@ -165,6 +198,7 @@ const Map<String, String> enUS = {
'meetingTranscript': 'Meeting Transcript',
// 设置页
"systemSettings": "System Settings",
"premiumMember": "Premium Membership",
"joinDevice": "Connected device:",
"accountId": "Account ID:",
@ -177,6 +211,8 @@ const Map<String, String> enUS = {
"lastBackup": "Last backup:",
"privacySettings": "Privacy Settings",
"privacySettingsInfo": "Manage data collection and permissions",
"permissionManagement": "Permission Management", // 权限管理
"permissionManagementInfo": "Manage app permission settings", // 管理应用权限设置
"logout": "Log Out",
"logoutTips": "Are you sure you want to log out of the current account?",
"aboutApplication": "About",
@ -204,6 +240,16 @@ const Map<String, String> enUS = {
'deleteAccount': 'Delete Account',
'deleteAccountTips':
'Are you sure you want to permanently delete your account? This action cannot be undone.',
"firmwareUpgrade": "Firmware Upgrade", // 固件升级
"bleHeadsetFirmwareUpgrade": "BLE Headset Firmware Upgrade", // BLE耳机固件版本升级
"deviceManagement": "Device Management", // 设备管理
"manageConnectedDevices": "Manage Connected Bluetooth Devices", // 管理已绑定的蓝牙设备
// 设置页面 - 实时语音对话
'realtimeVoiceChat': 'Real-time Voice Chat',
'realtimeVoiceChatInfo':
'Experience intelligent voice interaction features (Preview)',
'loading': 'Loading...',
// 聊天页
"clearChatHistory": "Clear Chat History",
@ -212,16 +258,12 @@ const Map<String, String> enUS = {
"imListening": "Listening...",
"canStartTalking": "You may start speaking",
// 翻译页
"selectSourceLanguage": "Select Source Language",
"selectTargetLanguage": "Select Target Language",
//编辑个人信息页
// 编辑个人信息页
"editProfile": "Edit Profile",
"nickname": "Nickname",
"save": "Save",
//国家列表
// 国家列表
"邮箱": "Email",
"中国": "China",
"美国": "United States",
@ -329,51 +371,109 @@ const Map<String, String> enUS = {
"斐济": "Fiji",
"所罗门群岛": "Solomon Islands",
// Personal Assistant Emma
"personalAssistantName": "Emma",
"personalAssistantDescription":
"Hi, I'm Emma, a versatile AI assistant. Whether it's work planning, study advice, life arrangements, or any other issues, I'm happy to help. Let's improve efficiency and achieve goals together!",
"assistantTag": "Assistant",
"personalAssistantWelcome":
"Hello! I'm Emma, and I'm happy to be your personal assistant. I can help you with various tasks such as time management, goal planning, information organization, etc. What can I help you with?",
"personalAssistantVoiceWelcome": "I'm here!",
// 导航
"driving": "Driving",
"walking": "Walking",
"bicycling": "Cycling",
"tsransit": "Transit",
// 同声传译 Lucy
"interpreterName": "Simultaneous Interpreter",
"interpreterDescription":
"Hi! I'm Lucy, a professional English-Chinese interpreter specializing in real-time translation for meetings, lectures, and conversations, making communication barrier-free.",
"interpreterTag": "Translation",
"interpreterWelcome":
"Hello everyone! I'm Lucy, your simultaneous interpreter. I'll translate between Chinese and English for you. When you speak Chinese, I'll translate to English; when you speak English, I'll translate to Chinese. Let's start our translation session!",
// ... existing code ...
// 亲子陪伴 芯愿
"companionName": "XinYuan",
"companionDescription":
"I am XinYuan, your dedicated parent-child companion, gentle and understanding. Whether you want to chat, share concerns, or need companionship, I'll always be by your side.",
"companionTag": "Emotional",
"companionWelcome":
"Hello! Finally meeting you! I'm XinYuan, your dedicated parent-child companion. How was your day? Is there anything you'd like to share with me? I'm always here, ready to listen to your thoughts.",
"companionVoiceWelcome": "I'm here with you!",
// 多功能AI聊天 吴小江
"healthAdvisorName": "AI Chat Assistant Wu",
"healthAdvisorDescription":
"I'm Wu, ready to provide you with various AI chat features.",
"healthAdvisorTag": "Chat",
"healthAdvisorWelcome":
"Hello, I'm Wu. I'm happy to assist you with AI chat features. How may I help you today?",
"healthAdvisorVoiceWelcome": "I'm here!",
// 翻译模块
// 翻译主页
"translationHistory": "Translation History",
"enableRecording": "Enable Recording",
"viewAudioFiles": "View Audio Files",
"headsetUser": "Headset User",
"phoneUser": "Phone User",
"selectSourceLanguage": "Select Source Language",
"selectTargetLanguage": "Select Target Language",
// Translation History
// 翻译历史页
"noTranslationHistory": "No translation history",
"clearHistory": "Clear History",
"clearHistoryConfirm":
"Are you sure you want to clear all translation history? This action cannot be undone.",
"translationHistory": "Translation History",
"unknownLanguage": "Unknown Language",
"deleteSessionRecord": "Delete Session Record",
"deleteSessionConfirm":
"Are you sure you want to delete all translation records for this session? This action cannot be undone.",
// 翻译音频文件列表页
"translateAudioFiles": "Translate Audio Files",
"noAudioFiles": "No audio files",
"calculating": "Calculating...",
"playing": "Playing...",
"paused": "Paused",
"clickToPlay": "Click to play",
"rename": "Rename",
"smartTranscription": "Smart Transcription",
"renameAudioFile": "Rename Audio File",
"enterNewFileName": "Please enter new file name",
// 翻译详情页
"originalRecord": "Original Record",
"recordSummary": "Record Summary",
"clickToGenerateSummary":
"Click the button below to generate session summary",
"summaryContent": "Summary Content",
"copied": "Copied",
"summaryCopiledToClipboard": "Summary copied to clipboard",
"extractSummary": "Extract Summary",
"summaryGenerated": "Summary generated",
"generating": "Generating...",
"regenerate": "Regenerate",
"summaryTimeoutMessage":
"Summary generation request timed out, possibly due to network issues or server busy.",
"deleteTranslationRecord": "Delete Translation Record",
"deleteTranslationConfirm":
"Are you sure you want to delete this translation record? This action cannot be undone.",
"originalText": "Original Text",
"translatedText": "Translated Text",
"sessionSummary": "Session Summary",
"cannotShare": "Cannot Share",
"generateSummaryFirst": "Please generate summary first",
"translationSessionRecord": "Translation Session Record",
// 配对模块
// 配对页面
"devicePairing": "Device Pairing",
"connectYourDeepSoundHeadset": "Connect Your DeepSound Headset",
"bind": "Bind",
"authorizeBind": "Authorize Binding",
"enterCode": "Please enter code",
"autoInputCode": "Auto Input Code",
"skipBinding": "Skip Binding",
"rescan": "Rescan",
"skip": "Skip",
"verifyingDevice": "Verifying device...",
"confirmBinding": "Confirm Binding",
"associatingDevice": "Associating device...",
"goToBluetoothSettings": "Go to Bluetooth Settings",
// 配对状态文本
"readyToPair": "Ready to pair",
"preparingPairingEnvironment": "Preparing pairing environment...",
"connectDeepSoundViaSystemBluetooth":
"Please connect DeepSound device via system Bluetooth first",
"deviceBindingConfirm": "Device binding confirmation...",
"establishingDeviceAssociation": "Establishing device association...",
"waitingCompletePairing": "Please wait, completing pairing...",
"pairingSuccessRedirecting":
"Pairing successful! Redirecting to home page...",
// 配对错误状态文本
"ensureConnectedDeepSoundDevice":
"Please ensure connected device is DeepSound",
"deviceAlreadyBoundToOtherAccount":
"This device is already bound to another account",
"deviceAssociationFailed": "Device association failed, please retry",
"scanTimeoutEnsureDeviceNearby":
"Scan timeout, please ensure device is nearby",
"networkError": "Network error",
"authorizationFailedCheckCode":
"Authorization failed, please check authorization code",
"pairingFailedRetry": "Pairing failed, please retry",
// User Feedback Page
// 用户反馈页
"feedback": "Feedback",
'feedbackInfo': 'Provide feedback or suggestions',
'feedbackDescription': 'Please describe your issue or suggestion',
@ -385,7 +485,7 @@ const Map<String, String> enUS = {
'feedbackSubmitSuccess': 'Feedback submitted successfully, thank you!',
'feedbackSubmitFailed': 'Failed to submit feedback, please try again later',
// Language names
// 语言名称
"中文(简体)": "Chinese (Simplified)",
"中文(繁体)": "Chinese (Traditional)",
"中文(香港繁体)": "Chinese (Hong Kong)",

91
lib/core/translations/language/zh_cn.dart

@ -46,6 +46,26 @@ const Map<String, String> zhCN = {
"malay": "马来语",
"turkish": "土耳其语",
"polish": "波兰语",
"filipino": "菲律宾语",
"dutch": "荷兰语",
"swedish": "瑞典语",
"danish": "丹麦语",
"finnish": "芬兰语",
"czech": "捷克语",
"hungarian": "匈牙利语",
"norwegian": "挪威语",
"romanian": "罗马尼亚语",
"ukrainian": "乌克兰语",
"greek": "希腊语",
"bulgarian": "保加利亚语",
"croatian": "克罗地亚语",
"slovak": "斯洛伐克语",
"slovenian": "斯洛文尼亚语",
"lithuanian": "立陶宛语",
"latvian": "拉脱维亚语",
"estonian": "爱沙尼亚语",
"serbian": "塞尔维亚语",
"icelandic": "冰岛语",
// 登录页
"welcome": "欢迎使用",
@ -69,6 +89,11 @@ const Map<String, String> zhCN = {
"pleaseEnterCompleteCode": "请输入完整的验证码",
"loginFailed": "登录失败,请重试",
'pleaseAcceptPrivacyPolicy': '请先阅读并同意隐私政策和用户协议',
"enterVerificationCode": "输入验证码",
"verificationCodeSentTo": "验证码已发送至",
"resendVerificationCode": "重新发送验证码",
"login": "登录",
"usePasswordLogin": "使用密码登录",
// 首页
"deviceStatus": "设备状态",
@ -112,8 +137,14 @@ const Map<String, String> zhCN = {
"unknown": "未知",
'bluetoothTurnedOff': '蓝牙已关闭,请打开蓝牙',
'bluetoothOff': '蓝牙已关闭',
'leftEar': '左耳',
'rightEar': '右耳',
'chargingCase': '充电仓',
'left': '左',
'right': '右',
'checkConnectionStatusFailed': '检查连接状态失败',
// 发现页
// 发现页
"searchAi": "搜索AI功能...",
"exploreSlogan": "智能耳机,无限可能",
"exploreSlogan1": "智能陪伴,无限可能",
@ -162,6 +193,7 @@ const Map<String, String> zhCN = {
'meetingTranscript': '会议原文',
// 设置页
"systemSettings": "系统设置",
"premiumMember": "高级会员",
"joinDevice": "已连接设备:",
"accountId": "账号ID:",
@ -174,6 +206,8 @@ const Map<String, String> zhCN = {
"lastBackup": "上次备份:",
"privacySettings": "隐私设置",
"privacySettingsInfo": "管理数据收集和权限",
"permissionManagement": "权限管理", // 权限管理
"permissionManagementInfo": "管理应用权限设置", // 管理应用权限设置
"logout": "退出登录",
"logoutTips": "确定要退出当前账号吗?",
"aboutApplication": "关于应用",
@ -198,6 +232,15 @@ const Map<String, String> zhCN = {
'completeRecording': '完成录制',
'deleteAccount': '注销账号',
'deleteAccountTips': '确定要永久删除您的账号吗?此操作不可撤销',
"firmwareUpgrade": "固件升级",
"bleHeadsetFirmwareUpgrade": "BLE耳机固件版本升级",
"deviceManagement": "设备管理",
"manageConnectedDevices": "管理已绑定的蓝牙设备",
// 设置页面 - 实时语音对话
"realtimeVoiceChat": "实时语音对话",
"realtimeVoiceChatInfo": "体验智能语音交互功能(功能预览)",
"loading": "加载中...",
// 聊天页
"clearChatHistory": "清空聊天记录",
@ -206,10 +249,6 @@ const Map<String, String> zhCN = {
"imListening": "正在聆听...",
"canStartTalking": "你可以开始说话",
// 翻译页
"selectSourceLanguage": "选择源语言",
"selectTargetLanguage": "选择目标语言",
//编辑个人信息页
"editProfile": "编辑个人信息",
"nickname": "昵称",
@ -323,47 +362,12 @@ const Map<String, String> zhCN = {
"斐济": "斐济",
"所罗门群岛": "所罗门群岛",
// 个人助理 Emma
"personalAssistantName": "个人助理 Emma",
"personalAssistantDescription":
"你好,我是 Emma,一位全能型 AI 助理。无论是工作规划、学习建议、生活安排,还是其他任何问题,我都很乐意为你提供帮助。让我们一起提高效率,实现目标!",
"assistantTag": "助理",
"personalAssistantWelcome":
"你好!我是 Emma,很高兴成为你的私人助理。我可以帮你处理各种任务,比如时间管理、目标规划、信息整理等。有什么我可以帮你的吗?",
"personalAssistantVoiceWelcome": "我在呢!",
// 导航
"driving": "驾车",
"walking": "步行",
"bicycling": "骑行",
"tsransit": "公交",
// 同声传译 Lucy
"interpreterName": "同声传译",
"interpreterDescription":
"Hello!我是 Lucy,一名专业的中英同声翻译官,擅长在会议、讲座、对话场景中进行实时翻译,让沟通无障碍。",
"interpreterTag": "翻译",
"interpreterWelcome":
"大家好!我是同声翻译官 Lucy,为你在中英之间提供实时翻译。你说中文,我就给你翻译成英文;你说英文,我就给你翻译成中文。现在,开始我们的翻译吧!",
// ... existing code ...
// 亲子陪伴 芯愿
"companionName": "亲子陪伴 芯愿",
"companionDescription":
"我是你的专属亲子陪伴芯愿,温柔体贴,善解人意。无论你想聊天、倾诉烦恼,还是需要陪伴,我都会一直在你身边。",
"companionTag": "情感",
"companionWelcome":
"您好呀,终于等到你啦!我是芯愿,你的专属亲子陪伴。今天过得怎么样?有什么想和我分享的吗?我一直都在这里,随时倾听你的心声。",
"companionVoiceWelcome": "我在这里陪着你!",
// 多功能AI聊天 吴小江
"healthAdvisorName": "多功能AI聊天 吴小江",
"healthAdvisorDescription": "我是吴小江,我可以为你提供更多AI聊天功能。",
"healthAdvisorTag": "健康",
"healthAdvisorWelcome": "您好,我是吴小江,很高兴能为您提供多功能AI聊天。我将用专业知识帮助您。请问有什么问题想咨询吗?",
"healthAdvisorVoiceWelcome": "我在这里!",
// 翻译模块
// 翻译主页 (translation_view.dart)
"translationHistory": "翻译历史",
@ -371,6 +375,8 @@ const Map<String, String> zhCN = {
"viewAudioFiles": "查看录音文件",
"headsetUser": "耳机用户",
"phoneUser": "手机用户",
"selectSourceLanguage": "选择源语言",
"selectTargetLanguage": "选择目标语言",
// 翻译历史页 (translation_history_view.dart)
"noTranslationHistory": "暂无历史记录",
@ -424,12 +430,15 @@ const Map<String, String> zhCN = {
"skipBinding": "跳过绑定",
"rescan": "重新扫描",
"skip": "跳过",
"verifyingDevice": "正在验证设备...",
"confirmBinding": "确认绑定",
"associatingDevice": "正在关联设备...",
"goToBluetoothSettings": "前往蓝牙设置",
// 配对状态文本
"readyToPair": "准备开始配对",
"preparingPairingEnvironment": "正在准备配对环境...",
"connectDeepSoundViaSystemBluetooth": "请先通过系统蓝牙连接DeapSound设备",
"verifyingDevice": "正在验证设备...",
"deviceBindingConfirm": "设备绑定确认...",
"establishingDeviceAssociation": "正在建立设备关联...",
"waitingCompletePairing": "请稍候,正在完成配对...",

7
lib/data/models/appconfig.dart

@ -4,7 +4,6 @@ import 'package:deep_voice/data/models/appconfig_model.dart';
class AppConfig {
static late UserGetAppConfigResp config;
// 可选:提供初始化方法
static void initialize(UserGetAppConfigResp resp) {
config = resp;
@ -25,4 +24,10 @@ class AppConfig {
final foundAgent = config.agents.firstWhere((agent) => agent.id == agentId);
return foundAgent.systemPrompt;
}
static List<String> allproductNames() {
List<String> names =
config.products.map((user) => user.devicename).toList();
return names;
}
}

24
lib/data/models/appconfig_model.dart

@ -7,8 +7,13 @@ class UserGetAppConfigResp {
final Map<String, String> env;
final List<DBAgent> agents;
final Map<String, DBMCPServer> mcps;
final List<DBProduct> products;
UserGetAppConfigResp(
{required this.env, required this.agents, required this.mcps});
{required this.env,
required this.agents,
required this.mcps,
required this.products});
factory UserGetAppConfigResp.fromJson(Map<String, dynamic> json) =>
_$UserGetAppConfigRespFromJson(json);
@ -63,3 +68,20 @@ class DBMCPServer {
_$DBMCPServerFromJson(json);
Map<String, dynamic> toJson() => _$DBMCPServerToJson(this);
}
@JsonSerializable()
class DBProduct {
final String id;
final int factoryid;
final String devicename;
DBProduct({
required this.id,
required this.factoryid,
required this.devicename,
});
factory DBProduct.fromJson(Map<String, dynamic> json) =>
_$DBProductFromJson(json);
Map<String, dynamic> toJson() => _$DBProductToJson(this);
}

16
lib/data/models/appconfig_model.g.dart

@ -16,6 +16,9 @@ UserGetAppConfigResp _$UserGetAppConfigRespFromJson(
mcps: (json['mcps'] as Map<String, dynamic>).map(
(k, e) => MapEntry(k, DBMCPServer.fromJson(e as Map<String, dynamic>)),
),
products: (json['products'] as List<dynamic>)
.map((e) => DBProduct.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$UserGetAppConfigRespToJson(
@ -24,6 +27,7 @@ Map<String, dynamic> _$UserGetAppConfigRespToJson(
'env': instance.env,
'agents': instance.agents,
'mcps': instance.mcps,
'products': instance.products,
};
DBAgent _$DBAgentFromJson(Map<String, dynamic> json) => DBAgent(
@ -66,3 +70,15 @@ Map<String, dynamic> _$DBMCPServerToJson(DBMCPServer instance) =>
'url': instance.url,
'tools': instance.tools,
};
DBProduct _$DBProductFromJson(Map<String, dynamic> json) => DBProduct(
id: json['id'] as String,
factoryid: (json['factoryid'] as num).toInt(),
devicename: json['devicename'] as String,
);
Map<String, dynamic> _$DBProductToJson(DBProduct instance) => <String, dynamic>{
'id': instance.id,
'factoryid': instance.factoryid,
'devicename': instance.devicename,
};

5
lib/data/models/user_Info.dart

@ -23,6 +23,7 @@ class User {
int viplv = 0; //添加会员等级字段,0表示普通用户
String earphonename;
String earphonemac;
String earphonelicense;
String earphoneauthcode;
// final Int64 lastbettime;
// final Int64 createtime;
@ -44,6 +45,7 @@ class User {
required this.viplv, // 添加默认值
required this.earphonename,
required this.earphonemac,
required this.earphonelicense,
required this.earphoneauthcode,
// required this.lastbettime,
// required this.createtime,
@ -74,6 +76,7 @@ class User {
viplv: (userData?['viplv'] as int?) ?? 0, // 提供默认值
earphonename: (userData?['earphonename'] as String?) ?? '',
earphonemac: (userData?['earphonemac'] as String?) ?? '',
earphonelicense: (userData?['earphonelicense'] as String?) ?? '',
earphoneauthcode: (userData?['earphoneauthcode'] as String?) ?? '',
// lastbettime: _parseTimestamp(userData['lastbettime']),
// createtime: _parseTimestamp(userData['createtime']),
@ -102,6 +105,7 @@ class User {
viplv: (userData?['viplv'] as int?) ?? 0, // 提供默认值
earphonename: (userData?['earphonename'] as String?) ?? '',
earphonemac: (userData?['earphonemac'] as String?) ?? '',
earphonelicense: (userData?['earphonelicense'] as String?) ?? '',
earphoneauthcode: (userData?['earphoneauthcode'] as String?) ?? '',
// lastbettime: _parseTimestamp(userData['lastbettime']),
// createtime: _parseTimestamp(userData['createtime']),
@ -128,6 +132,7 @@ class User {
'earphonename': earphonename,
'earphonemac': earphonemac,
"earphonelicense": earphonelicense,
'earphoneauthcode': earphoneauthcode,
// 'lastbettime': lastbettime,

4
lib/data/services/navigation_manager.dart

@ -2,11 +2,11 @@ import 'package:amap_service/amap_service.dart';
import 'package:deep_voice/core/utils/logger.dart';
import 'package:get/get.dart';
//音乐播放管理
//导航播放管理
class NavigationManager extends GetxService {
static NavigationManager get to => Get.find<NavigationManager>();
// 音乐服务实例
// 导航服务实例
final AmapService _amapervice = AmapService();
/// 初始化

13
lib/data/services/network/api.dart

@ -74,10 +74,19 @@ class Api {
}
//app获取设备信息
static getequipmentinfo([params]) {
static getproduct([params]) {
return DioManager().request(
NWMethod.post,
'/api/home/user_getequipmentinfo',
'/api/home/user_getproduct',
params: params,
);
}
//app获取设备信息
static getproducts([params]) {
return DioManager().request(
NWMethod.post,
'/api/home/user_getproducts',
params: params,
);
}

4
lib/data/services/network/nw_api.dart

@ -1,5 +1,5 @@
class NWApi {
// static const baseApi = "https://www.deapsound.com";
//static const baseApi = "https://api.deapsound.com";
static const baseApi = "https://api.ideapsound.com";
static const baseApi = "https://api.deapsound.com";
// static const baseApi = "https://api.ideapsound.com";
}

27
lib/modules/agent/views/music_card.dart

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../../../routes/app_routes.dart';
import '../controllers/agent_controller.dart';
@ -20,10 +21,10 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url,
controller.musicStatus(); //刷新状态
return Container(
padding: const EdgeInsets.all(14),
padding: EdgeInsets.all(14.w),
decoration: BoxDecoration(
color: isDarkMode ? const Color(0xFF1E1E1E) : Colors.white,
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: isDarkMode ? 0.2 : 0.03),
@ -36,11 +37,11 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url,
child: Row(
children: [
Container(
width: 50,
height: 50,
width: 50.w,
height: 50.h,
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
color: isDarkMode ? Colors.grey[800] : Colors.grey[100],
borderRadius: BorderRadius.circular(8.r),
image: image.isNotEmpty
? DecorationImage(
image: NetworkImage(image),
@ -51,12 +52,12 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url,
child: image.isEmpty
? Icon(
Icons.music_note,
size: 18,
size: 18.sp,
color: Theme.of(context).primaryColor,
)
: null,
),
const SizedBox(width: 12),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -64,18 +65,18 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url,
Text(
name,
style: TextStyle(
fontSize: 13,
fontSize: 13.sp,
fontWeight: FontWeight.w500,
color: Colors.grey[800],
color: isDarkMode ? Colors.white.withValues(alpha: 0.9) : Colors.grey[800],
),
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 2),
SizedBox(height: 2.h),
Text(
singer,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
overflow: TextOverflow.ellipsis,
),

436
lib/modules/agent/views/navigation_card.dart

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../controllers/agent_controller.dart';
@ -30,7 +31,15 @@ String formatDuration(String secondsStr) {
return parts.join('');
}
//导航卡片
/// 导航卡片组件
///
/// 各元素含义分析:
/// - [start]: 起点坐标 (经纬度格式)
/// - [end]: 终点坐标 (经纬度格式)
/// - [destination]: 目的地名称显示
/// - [modes]: 导航方式列表,包含驾车、步行、公交等选项
/// - [navigation]: 当前导航状态信息(可选)
/// - [height]: 卡片高度
Widget navigationCard(
BuildContext context, {
required String start,
@ -38,70 +47,168 @@ Widget navigationCard(
required String destination,
required List<Map<String, dynamic>> modes,
Map<String, dynamic>? navigation,
double height = 300,
double? height, // 改为可选参数,允许自适应高度
}) {
int currentIndex = 0;
final controller = Get.find<AgentController>(); // 获取控制器
final controller = Get.find<AgentController>();
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Container(
height: height,
height: height?.h ?? 350.h, // 如果没有指定高度,使用350作为默认高度
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: Colors.white,
boxShadow: const [
borderRadius: BorderRadius.circular(16.r),
color: isDarkMode ? const Color(0xFF1E1E1E) : Colors.white,
boxShadow: [
BoxShadow(
blurRadius: 8,
color: Colors.black12,
offset: Offset(0, 4),
blurRadius: 12,
color: Colors.black.withValues(alpha: isDarkMode ? 0.4 : 0.08),
offset: const Offset(0, 2),
),
],
),
child: StatefulBuilder(
builder: (context, setInnerState) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 路线图片
ClipRRect(
borderRadius:
const BorderRadius.vertical(top: Radius.circular(16)),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Image.network(
modes[currentIndex]["image"] as String,
key: ValueKey<String>(modes[currentIndex]["ntype"] as String),
height: 160,
width: double.infinity,
fit: BoxFit.cover,
// 顶部:路线预览图片区域
Expanded(
flex: 4, // 增加图片区域的flex值,让图片占更多空间
child: ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)),
child: Stack(
children: [
// 路线图片
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Image.network(
modes[currentIndex]["image"] as String,
key: ValueKey<String>(
modes[currentIndex]["ntype"] as String),
height: double.infinity,
width: double.infinity,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
color: isDarkMode
? Colors.grey[800]
: Colors.grey[200],
child: Icon(
Icons.map_outlined,
size: 48.sp,
color: isDarkMode
? Colors.grey[600]
: Colors.grey[400],
),
);
},
),
),
// 导航状态覆盖层
if (navigation != null)
Positioned(
top: 12.h,
left: 12.w,
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color:
Theme.of(context).primaryColor.withOpacity(0.9),
borderRadius: BorderRadius.circular(12.r),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.navigation,
color: Colors.white,
size: 14.sp,
),
SizedBox(width: 4.w),
Text(
'导航中',
style: TextStyle(
color: Colors.white,
fontSize: 12.sp,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
],
),
),
),
// 导航方式切换按钮
Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
// 中部:导航方式选择器
Container(
padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 16.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(modes.length, (index) {
final mode = modes[index];
final isSelected = index == currentIndex;
return GestureDetector(
onTap: modes[index]['enable'] as bool
? () {
setInnerState(() => currentIndex = index);
}
: null,
child: SizedBox(
width: 50,
child: Text(
(modes[index]["ntype"] as String).tr,
textAlign: TextAlign.center,
style: TextStyle(
color: modes[index]['enable']
? isSelected
? Colors.blue
: Colors.black87
: Colors.grey,
fontSize: 12,
fontWeight: FontWeight.w500,
final isEnabled = mode['enable'] as bool;
return Expanded(
child: GestureDetector(
onTap: isEnabled
? () {
setInnerState(() => currentIndex = index);
}
: null,
child: Container(
margin: EdgeInsets.symmetric(horizontal: 4.w),
padding: EdgeInsets.symmetric(
vertical: 8.h, horizontal: 12.w),
decoration: BoxDecoration(
color: isSelected
? Theme.of(context).primaryColor.withOpacity(0.1)
: Colors.transparent,
borderRadius: BorderRadius.circular(8.r),
border: Border.all(
color: isSelected
? Theme.of(context).primaryColor
: (isDarkMode
? Colors.grey[700]!
: Colors.grey[300]!),
width: isSelected ? 2 : 1,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_getNavigationIcon(mode["ntype"] as String),
color: isEnabled
? (isSelected
? Theme.of(context).primaryColor
: (isDarkMode
? Colors.white70
: Colors.black87))
: Colors.grey,
size: 20.sp,
),
SizedBox(height: 4.h),
Text(
(mode["ntype"] as String).tr,
textAlign: TextAlign.center,
style: TextStyle(
color: isEnabled
? (isSelected
? Theme.of(context).primaryColor
: (isDarkMode
? Colors.white70
: Colors.black87))
: Colors.grey,
fontSize: 11.sp,
fontWeight: isSelected
? FontWeight.w600
: FontWeight.w400,
),
),
],
),
),
),
@ -110,42 +217,191 @@ Widget navigationCard(
),
),
// 距离和耗时
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
// 底部:目的地信息和操作按钮
Container(
padding: EdgeInsets.fromLTRB(20.w, 8.h, 20.w, 16.h),
child: Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('$destination',
style: const TextStyle(
fontSize: 12, color: Colors.black)),
Text(
'${formatDistance(modes[currentIndex]["distance"])}-约${formatDuration(modes[currentIndex]["duration"])}',
style: const TextStyle(
fontSize: 15, color: Colors.blue)),
if (navigation != null)
Text(
"导航中... ${navigation["distanceText"]}进入${navigation["nextRoad"]}",
style: TextStyle(
fontSize: 12,
color:
const Color.fromARGB(255, 164, 164, 164)))
],
// 目的地和距离时间信息
Padding(
padding: EdgeInsets.only(right: 70.w), // 为右下角按钮留出空间
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 路径显示:当前位置 → 目的地
Row(
children: [
// 起点图标
Icon(
Icons.my_location,
size: 16.sp,
color: Theme.of(context).primaryColor,
),
SizedBox(width: 6.w),
// 当前位置文本
Text(
'当前位置',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: isDarkMode
? Colors.white70
: Colors.black54,
),
),
SizedBox(width: 8.w),
// 箭头图标
Icon(
Icons.arrow_forward,
size: 16.sp,
color: isDarkMode
? Colors.white54
: Colors.black45,
),
SizedBox(width: 8.w),
// 目的地图标
Icon(
Icons.location_on,
size: 16.sp,
color: Colors.red,
),
SizedBox(width: 6.w),
// 目的地名称
Expanded(
child: Text(
destination,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode
? Colors.white
: Colors.black87,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
SizedBox(height: 4.h),
// 距离和时间信息
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 距离信息
Row(
children: [
Icon(
Icons.straighten,
size: 14.sp,
color: Theme.of(context).primaryColor,
),
SizedBox(width: 4.w),
Text(
formatDistance(
modes[currentIndex]["distance"]),
style: TextStyle(
fontSize: 14.sp,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.w500,
),
),
],
),
SizedBox(height: 4.h),
// 时间信息
Row(
children: [
Icon(
Icons.access_time,
size: 14.sp,
color: Theme.of(context).primaryColor,
),
SizedBox(width: 4.w),
Text(
'约${formatDuration(modes[currentIndex]["duration"])}',
style: TextStyle(
fontSize: 14.sp,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
// 导航状态信息
if (navigation != null) ...[
SizedBox(height: 6.h),
Container(
padding: EdgeInsets.symmetric(
horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: Theme.of(context)
.primaryColor
.withOpacity(0.1),
borderRadius: BorderRadius.circular(6.r),
),
child: Text(
"${navigation["distanceText"]}进入${navigation["nextRoad"]}",
style: TextStyle(
fontSize: 12.sp,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.w500,
),
),
),
],
],
),
),
CircleAvatar(
backgroundColor: Colors.blue,
radius: 18,
child: IconButton(
icon: const Icon(Icons.turn_left,
color: Colors.white, size: 20),
onPressed: () {
controller.openNavigation(start, end, "");
},
// 开始导航按钮 - 绝对定位到右下角
Positioned(
right: 0,
bottom: 0,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).primaryColor,
Theme.of(context).primaryColor.withOpacity(0.8),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Theme.of(context)
.primaryColor
.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(12.r),
onTap: () {
controller.openNavigation(start, end, "");
},
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 16.w,
vertical: 12.h,
),
child: Icon(
Icons.navigation,
color: Colors.white,
size: 20.sp,
),
),
),
),
),
)
),
],
),
),
@ -155,3 +411,25 @@ Widget navigationCard(
),
);
}
/// 根据导航类型获取对应图标
IconData _getNavigationIcon(String ntype) {
switch (ntype.toLowerCase()) {
case 'driving':
case '驾车':
return Icons.directions_car;
case 'walking':
case '步行':
return Icons.directions_walk;
case 'transit':
case 'tsransit': // 修正拼写错误
case '公交':
return Icons.directions_transit;
case 'bicycling': // 修正为正确的英文值
case 'cycling':
case '骑行':
return Icons.directions_bike;
default:
return Icons.navigation;
}
}

110
lib/modules/agent/views/travel_card.dart

@ -1,29 +1,32 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
/// 构建旅游内容卡片
Widget buildTravelContent(
BuildContext context, List<Map<String, dynamic>> travellist) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'以下是一些推搜索结果:',
Text(
'以下是一些推荐搜索结果:',
style: TextStyle(
color: Colors.black87,
fontSize: 14,
color: isDarkMode ? Colors.white.withValues(alpha: 0.9) : Colors.black87,
fontSize: 14.sp,
height: 1.4,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 12),
SizedBox(height: 12.h),
Container(
padding: const EdgeInsets.all(12),
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
color: isDarkMode ? const Color(0xFF1E1E1E) : Colors.white,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
color: Colors.black.withValues(alpha: isDarkMode ? 0.2 : 0.03),
blurRadius: 3,
spreadRadius: 1,
offset: const Offset(0, 1),
@ -53,16 +56,16 @@ Widget buildTravelContent(
// ),
// const SizedBox(height: 10),
// 动态生成音乐条目
// 动态生成旅游景点条目
...travellist.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
final name = item["name"] ?? "未知歌曲"; //名字
final name = item["name"] ?? "未知景点"; //名字
final addr = item["addr"] ?? ""; //地址
final tag = item["tag"] ?? "未知歌手"; //标签
final rating = item["rating"] ?? ""; //评分
final distance = item["distance"] ?? ""; //距离
final images = item["images"] ?? []; // 图片列表
// final tag = item["tag"] ?? "未知标签"; //标签
// final rating = item["rating"] ?? ""; //评分
// final distance = item["distance"] ?? ""; //距离
// final images = item["images"] ?? []; // 图片列表
return Column(
children: [
buildLocationItem(
@ -71,7 +74,11 @@ Widget buildTravelContent(
addr,
),
if (index != travellist.length - 1)
Divider(height: 1, thickness: 1, color: Colors.grey[100]),
Divider(
height: 1.h,
thickness: 1,
color: isDarkMode ? Colors.grey[700] : Colors.grey[100]
),
],
);
}).toList(),
@ -82,19 +89,19 @@ Widget buildTravelContent(
// Divider(height: 1, thickness: 1, color: Colors.grey[100]),
// _buildLocationItem('浅草寺', '东京最古老的寺庙', context),
const SizedBox(height: 8),
SizedBox(height: 8.h),
Center(
child: TextButton(
onPressed: () {},
style: TextButton.styleFrom(
minimumSize: Size.zero,
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Text(
'查看更多景点',
style: TextStyle(
fontSize: 12,
fontSize: 12.sp,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.w500,
),
@ -104,41 +111,41 @@ Widget buildTravelContent(
],
),
),
const SizedBox(height: 10),
SizedBox(height: 10.h),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () {},
icon: Icon(Icons.map, size: 16),
label: Text('查看地图'),
icon: Icon(Icons.map, size: 16.sp),
label: const Text('查看地图'),
style: OutlinedButton.styleFrom(
foregroundColor: Theme.of(context).primaryColor,
side: BorderSide(
color: Theme.of(context).primaryColor.withOpacity(0.5)),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
color: Theme.of(context).primaryColor.withValues(alpha: 0.5)),
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 4.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(16.r),
),
textStyle: TextStyle(fontSize: 12),
textStyle: TextStyle(fontSize: 12.sp),
),
),
),
SizedBox(width: 8),
SizedBox(width: 8.w),
Expanded(
child: OutlinedButton.icon(
onPressed: () {},
icon: Icon(Icons.bookmark_border, size: 16),
label: Text('保存到收藏'),
icon: Icon(Icons.bookmark_border, size: 16.sp),
label: const Text('保存到收藏'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.grey[700],
side: BorderSide(color: Colors.grey[400]!),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
foregroundColor: isDarkMode ? Colors.grey[400] : Colors.grey[700],
side: BorderSide(color: isDarkMode ? Colors.grey[600]! : Colors.grey[400]!),
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 4.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(16.r),
),
textStyle: TextStyle(fontSize: 12),
textStyle: TextStyle(fontSize: 12.sp),
),
),
),
@ -154,23 +161,25 @@ Widget buildLocationItem(
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
padding: EdgeInsets.symmetric(vertical: 8.h),
child: Row(
children: [
Container(
width: 38,
height: 38,
width: 38.w,
height: 38.h,
decoration: BoxDecoration(
color: isDarkMode ? Color(0xFF1A365D) : Colors.blue[50],
borderRadius: BorderRadius.circular(8),
color: isDarkMode
? const Color(0xFF2A2A2A)
: Theme.of(context).primaryColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8.r),
),
child: Icon(
Icons.place,
size: 18,
color: Color(0xFFE11D48),
size: 18.sp,
color: Theme.of(context).primaryColor,
),
),
const SizedBox(width: 12),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -178,19 +187,19 @@ Widget buildLocationItem(
Text(
name,
style: TextStyle(
fontSize: 13,
fontSize: 13.sp,
fontWeight: FontWeight.w500,
color: isDarkMode
? Colors.white.withOpacity(0.9)
? Colors.white.withValues(alpha: 0.9)
: Colors.grey[800],
),
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 2),
SizedBox(height: 2.h),
Text(
description,
style: TextStyle(
fontSize: 12,
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
overflow: TextOverflow.ellipsis,
@ -200,10 +209,13 @@ Widget buildLocationItem(
),
IconButton(
onPressed: () {},
icon: Icon(Icons.arrow_forward, color: Colors.grey[400]),
icon: Icon(
Icons.arrow_forward,
color: isDarkMode ? Colors.grey[500] : Colors.grey[400]
),
padding: EdgeInsets.zero,
constraints: BoxConstraints(),
iconSize: 20,
constraints: const BoxConstraints(),
iconSize: 20.sp,
),
],
),

213
lib/modules/agent/views/weather_card.dart

@ -1,5 +1,6 @@
import 'package:common_utils/common_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
/// 构建天气内容卡片
Widget buildWeatherContent(BuildContext context, String city,
@ -12,31 +13,31 @@ Widget buildWeatherContent(BuildContext context, String city,
Text(
'查询天气情况:',
style: TextStyle(
color: isDarkMode ? Colors.white.withOpacity(0.9) : Colors.black87,
fontSize: 14,
color: isDarkMode ? Colors.white.withValues(alpha: 0.9) : Colors.black87,
fontSize: 14.sp,
height: 1.4,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 12),
SizedBox(height: 12.h),
Container(
padding: const EdgeInsets.all(16),
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
gradient: isDarkMode
? LinearGradient(
? const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF1E40AF), Color(0xFF1E3A8A)],
)
: LinearGradient(
: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF60A5FA), Color(0xFF3B82F6)],
),
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(isDarkMode ? 0.3 : 0.1),
color: Colors.black.withValues(alpha: isDarkMode ? 0.3 : 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
@ -64,37 +65,37 @@ Widget buildWeatherContent(BuildContext context, String city,
Text(
now["dayweather"],
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(0.9),
fontSize: 14.sp,
color: Colors.white.withValues(alpha: 0.9),
),
),
SizedBox(height: 8),
SizedBox(height: 8.h),
Row(
children: [
Text(
"${now["daytemp"]}°",
style: const TextStyle(
fontSize: 36,
style: TextStyle(
fontSize: 36.sp,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
SizedBox(width: 8),
SizedBox(width: 8.w),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'最高: ${now["daytemp"]}°',
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(0.9),
fontSize: 12.sp,
color: Colors.white.withValues(alpha: 0.9),
),
),
Text(
'最低: ${now["nighttemp"]}°',
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(0.9),
fontSize: 12.sp,
color: Colors.white.withValues(alpha: 0.9),
),
),
],
@ -105,26 +106,26 @@ Widget buildWeatherContent(BuildContext context, String city,
),
Icon(
getWeatherIcon(now['dayweather']),
size: 48,
size: 48.sp,
color: Colors.white,
),
],
),
SizedBox(height: 16),
SizedBox(height: 16.h),
Container(
height: 1,
color: Colors.white.withOpacity(isDarkMode ? 0.15 : 0.2),
color: Colors.white.withValues(alpha: isDarkMode ? 0.15 : 0.2),
),
SizedBox(height: 16),
const Text(
SizedBox(height: 16.h),
Text(
'未来预报',
style: TextStyle(
fontSize: 14,
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
SizedBox(height: 8),
SizedBox(height: 8.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(
@ -145,35 +146,8 @@ Widget buildWeatherContent(BuildContext context, String city,
},
),
),
SizedBox(height: 16),
Container(
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 12),
decoration: BoxDecoration(
color: isDarkMode
? Colors.white.withOpacity(0.15)
: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: const Row(
children: [
Icon(
Icons.info_outline,
size: 16,
color: Colors.white,
),
SizedBox(width: 8),
Flexible(
child: Text(
'今日紫外线指数较高,外出请做好防晒',
style: TextStyle(
fontSize: 12,
color: Colors.white,
),
),
),
],
),
),
SizedBox(height: 16.h),
_buildWeatherTip(context, now, isDarkMode),
],
),
),
@ -181,6 +155,125 @@ Widget buildWeatherContent(BuildContext context, String city,
);
}
/// 构建智能天气提示
Widget _buildWeatherTip(BuildContext context, Map<String, dynamic> now, bool isDarkMode) {
final weatherTip = _generateWeatherTip(now);
return Container(
padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 12.w),
decoration: BoxDecoration(
color: isDarkMode
? Colors.white.withValues(alpha: 0.15)
: Colors.white.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8.r),
),
child: Row(
children: [
Icon(
weatherTip.icon,
size: 16.sp,
color: Colors.white,
),
SizedBox(width: 8.w),
Flexible(
child: Text(
weatherTip.message,
style: TextStyle(
fontSize: 12.sp,
color: Colors.white,
),
),
),
],
),
);
}
/// 天气提示数据类
class WeatherTip {
final IconData icon;
final String message;
const WeatherTip({required this.icon, required this.message});
}
/// 根据天气数据生成智能提示
WeatherTip _generateWeatherTip(Map<String, dynamic> now) {
final dayWeather = (now['dayweather'] as String? ?? '').trim();
final dayTemp = int.tryParse(now['daytemp']?.toString() ?? '0') ?? 0;
final nightTemp = int.tryParse(now['nighttemp']?.toString() ?? '0') ?? 0;
// 高温提示 (>= 35°C)
if (dayTemp >= 35) {
return const WeatherTip(
icon: Icons.local_fire_department,
message: '今日高温炎热,请注意防暑降温,多喝水避免中暑',
);
}
// 低温提示 (<= 5°C)
if (dayTemp <= 5) {
return const WeatherTip(
icon: Icons.ac_unit,
message: '今日气温较低,外出请注意保暖,小心感冒',
);
}
// 雨天提示
if (dayWeather.contains('雨') || ['阵雨', '雷阵雨', '小雨', '中雨', '大雨', '暴雨'].any((rain) => dayWeather.contains(rain))) {
return const WeatherTip(
icon: Icons.umbrella,
message: '今日有雨,外出请携带雨具,注意出行安全',
);
}
// 雪天提示
if (dayWeather.contains('雪') || ['阵雪', '小雪', '中雪', '大雪', '暴雪'].any((snow) => dayWeather.contains(snow))) {
return const WeatherTip(
icon: Icons.ac_unit,
message: '今日有雪,路面湿滑请注意安全,做好保暖措施',
);
}
// 雾霾提示
if (dayWeather.contains('雾') || dayWeather.contains('霾') || ['雾', '浓雾', '霾', '中度霾', '重度霾'].any((fog) => dayWeather.contains(fog))) {
return const WeatherTip(
icon: Icons.blur_on,
message: '今日有雾霾,能见度较低,外出请佩戴口罩',
);
}
// 大风提示
if (dayWeather.contains('风') || ['大风', '强风', '疾风', '烈风'].any((wind) => dayWeather.contains(wind))) {
return const WeatherTip(
icon: Icons.air,
message: '今日风力较大,外出请注意安全,固定好物品',
);
}
// 晴天紫外线提示 (温度 >= 25°C)
if (['晴', '少云', '晴间多云'].any((sunny) => dayWeather.contains(sunny)) && dayTemp >= 25) {
return const WeatherTip(
icon: Icons.wb_sunny,
message: '今日阳光充足紫外线较强,外出请做好防晒',
);
}
// 温差大提示 (>= 10°C)
if ((dayTemp - nightTemp).abs() >= 10) {
return const WeatherTip(
icon: Icons.thermostat,
message: '今日昼夜温差较大,请适时增减衣物',
);
}
// 默认舒适天气提示
return const WeatherTip(
icon: Icons.wb_sunny_outlined,
message: '今日天气适宜,是外出活动的好时光',
);
}
/// 构建单日天气预报
Widget _buildForecastDay(String day, IconData icon, String temp) {
return Column(
@ -188,21 +281,21 @@ Widget _buildForecastDay(String day, IconData icon, String temp) {
Text(
day,
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(0.9),
fontSize: 12.sp,
color: Colors.white.withValues(alpha: 0.9),
),
),
SizedBox(height: 4),
SizedBox(height: 4.h),
Icon(
icon,
size: 20,
size: 20.sp,
color: Colors.white,
),
SizedBox(height: 4),
SizedBox(height: 4.h),
Text(
temp,
style: TextStyle(
fontSize: 12,
fontSize: 12.sp,
fontWeight: FontWeight.w500,
color: Colors.white,
),

14
lib/modules/devices/controllers/devices_controller.dart

@ -13,12 +13,14 @@ import 'package:get/get_state_manager/src/simple/get_controllers.dart';
class DevicesController extends GetxController {
late final BleManager _bleManager;
//耳机绑定
final isearphone = false.obs;
final earphonestate = 0.obs;
final earphoneName = "".obs;
final earphoneMac = "".obs;
final earphoneLicense = "".obs;
final earphoneCode = "".obs;
//录音笔
final voicepenstate = 0.obs;
final isvoicepen = false.obs;
final voicepenName = "".obs;
final voicepenMac = "".obs;
@ -29,9 +31,13 @@ class DevicesController extends GetxController {
_bleManager = Get.find<BleManager>();
earphoneName.value = User.instance.earphonename;
earphoneMac.value = User.instance.earphonemac;
earphoneLicense.value = User.instance.earphonelicense;
earphoneCode.value = User.instance.earphoneauthcode;
if (earphoneCode.value != "") {
isearphone.value = true;
earphonestate.value = 1;
}
if (earphoneLicense.value != "") {
earphonestate.value = 2;
}
}
@ -48,8 +54,8 @@ class DevicesController extends GetxController {
if (response != null) {
User.instance.earphonename = "";
User.instance.earphonemac = "";
User.instance.earphoneauthcode = "";
isearphone.value = false;
User.instance.earphonelicense = "";
earphonestate.value = 0;
_bleManager.clearPairedDevices();
_bleManager.disconnect();
Get.toNamed(Routes.pairing);

6
lib/modules/devices/views/devices_view.dart

@ -93,10 +93,10 @@ class DevicesView extends GetView<DevicesController> {
() =>
// 耳机列表
_buildSimpleNavigationSetting(
title: controller.isearphone.value
title: controller.earphonestate.value != 0
? controller.earphoneName.value
: "未绑定",
subtitle: controller.isearphone.value
subtitle: controller.earphonestate.value != 0
? controller.earphoneMac.value
: "还未绑定设备",
icon: Icons.headphones,
@ -110,7 +110,7 @@ class DevicesView extends GetView<DevicesController> {
titleColor: isDarkMode ? Colors.white : null,
subtitleColor: isDarkMode ? Colors.white70 : null,
isDarkMode: isDarkMode,
showbutt: controller.isearphone.value,
showbutt: controller.earphonestate == 2,
onPressed: () {
controller.unbindearphone();
},

6
lib/modules/home/controllers/home_controller.dart

@ -129,7 +129,7 @@ class HomeController extends GetxController {
update();
} catch (e) {
Logger.e(_tag, '检查连接状态失败: $e');
Logger.e(_tag, '${'checkConnectionStatusFailed'.tr}: $e');
if (_isFullyConnected.value) {
_isFullyConnected.value = false;
_isFullyConnected.refresh();
@ -217,7 +217,7 @@ class HomeController extends GetxController {
rightStatus =
deviceInfo.value!.rightEarCharging ? '$rightStatus ⚡' : rightStatus;
return '左耳: $leftStatus / 右耳: $rightStatus';
return '${'leftEar'.tr}: $leftStatus / ${'rightEar'.tr}: $rightStatus'; // 左耳: xxx / 右耳: xxx
}
String getCaseText() {
@ -232,7 +232,7 @@ class HomeController extends GetxController {
caseStatus = '$caseStatus ⚡';
}
return '充电仓: $caseStatus';
return '${'chargingCase'.tr}: $caseStatus'; // 充电仓: xxx
}
String getChargingText() {

6
lib/modules/home/views/home_view.dart

@ -109,7 +109,7 @@ class HomeView extends GetView<HomeController> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'DeepSound',
'DeapSound',
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.bold,
@ -395,7 +395,7 @@ class HomeView extends GetView<HomeController> {
children: [
// 左耳机
_buildCompactBatteryItem(
label: '左',
label: 'left'.tr, // 左
batteryLevel: leftBattery.value,
isCharging: leftEarCharging.value,
isDarkMode: isDarkMode,
@ -410,7 +410,7 @@ class HomeView extends GetView<HomeController> {
// 右耳机
_buildCompactBatteryItem(
label: '右',
label: 'right'.tr, // 右
batteryLevel: rightBattery.value,
isCharging: rightEarCharging.value,
isDarkMode: isDarkMode,

5
lib/modules/login/controllers/login_controller.dart

@ -11,7 +11,6 @@ import 'weixinLogin.dart';
import '../../../data/services/network/api.dart';
import '../../../../data/models/user_Info.dart';
import '../../common/webview_page.dart';
//import 'package:shared_preferences/shared_preferences.dart';
import 'package:get_storage/get_storage.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'dart:io';
@ -914,10 +913,6 @@ class LoginController extends GetxController {
print('getAppConfig:进来了');
// 根据API设计调整
final resp = UserGetAppConfigResp.fromJson(response);
// 存储用户信息(示例)
// _storage.write('ENV', resp.env);
// _storage.write('AGENTS', resp.agents);
// _storage.write('MCPS', resp.mcps);
AppConfig.initialize(resp);
return true;
} catch (e) {

20
lib/modules/login/views/login_view.dart

@ -236,8 +236,8 @@ class LoginView extends GetView<LoginController> {
controller: controller.contactController,
decoration: InputDecoration(
hintText: controller.selectedCountryFlag == '✉️'
? 'email'.tr // 需要在翻译文件中添加
: 'phoneNumber'.tr, // 需要在翻译文件中添加
? 'email'.tr // 邮箱
: 'phoneNumber'.tr, // 手机号码
hintStyle: TextStyle(
fontSize: 14.sp,
color: isDarkMode
@ -303,7 +303,7 @@ class LoginView extends GetView<LoginController> {
style: TextStyle(fontSize: 24.sp),
),
title: Text(
'${country['name']}'.tr,
'${country['name']}'.tr, // 国家名称
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black,
),
@ -584,7 +584,7 @@ class LoginView extends GetView<LoginController> {
),
SizedBox(width: 4.w),
Text(
'expressConsent'.tr,
'expressConsent'.tr, // 登录/注册即表示你同意
style: TextStyle(
fontSize: 13.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[700],
@ -680,7 +680,7 @@ class LoginView extends GetView<LoginController> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'输入验证码',
'enterVerificationCode'.tr, // 输入验证码
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.w600,
@ -689,7 +689,7 @@ class LoginView extends GetView<LoginController> {
),
SizedBox(height: 8.h),
Obx(() => Text(
'验证码已发送至 ${controller.displayContact}',
'${'verificationCodeSentTo'.tr} ${controller.displayContact}', // 验证码已发送至
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
@ -767,8 +767,8 @@ class LoginView extends GetView<LoginController> {
),
label: Text(
controller.countdownSeconds > 0
? '重新发送验证码 (${controller.countdownSeconds}s)'
: '重新发送验证码',
? '${'resendVerificationCode'.tr} (${controller.countdownSeconds}s)' // 重新发送验证码 (倒计时)
: 'resendVerificationCode'.tr, // 重新发送验证码
style: TextStyle(
fontSize: 14.sp,
color: controller.countdownSeconds > 0
@ -808,7 +808,7 @@ class LoginView extends GetView<LoginController> {
),
)
: Text(
'登录',
'login'.tr, // 登录
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
@ -824,7 +824,7 @@ class LoginView extends GetView<LoginController> {
child: TextButton(
onPressed: controller.usePasswordLogin,
child: Text(
'使用密码登录',
'usePasswordLogin'.tr, // 使用密码登录
style: TextStyle(
fontSize: 14.sp,
color:

109
lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart

@ -63,6 +63,10 @@ class OtaUpgradeController extends GetxController {
final RxDouble progress = 0.0.obs; // 控制器自己的可观察属性
final RxInt type = OtaEvent.NOT_STARTED.obs;
final RxBool isUpgrade = false.obs;
// 新增:详细状态提示
final RxString detailedStatus = ''.obs;
final RxBool showDetailedStatus = false.obs;
@override
void onInit() {
@ -184,7 +188,7 @@ class OtaUpgradeController extends GetxController {
jlota.init();
// API请求
final response = await Api.getequipmentinfo({
final response = await Api.getproduct({
"id": deviceInfo.firmwareProductId,
});
@ -271,6 +275,12 @@ class OtaUpgradeController extends GetxController {
Get.back();
Future.delayed(Duration(milliseconds: 300));
// 检查设备电量
if (!_checkBatteryLevel()) {
return; // 电量不足,不执行升级
}
startDownload();
},
child: Text('更新'),
@ -283,6 +293,9 @@ class OtaUpgradeController extends GetxController {
// 修改后的startDownload方法
Future<void> startDownload() async {
isDownloading.value = true;
showDetailedStatus.value = true;
detailedStatus.value = '准备下载固件包...';
int lastIndex = updatepackageaddress.lastIndexOf('/');
String fileName = updatepackageaddress.substring(lastIndex + 1);
String directoryName = "ota_upgrades";
@ -296,6 +309,7 @@ class OtaUpgradeController extends GetxController {
type.value = DOWNLOADING_UP_FILES;
try {
detailedStatus.value = '正在获取下载目录...';
final Directory? downloadsDir = await getApplicationSupportDirectory();
if (downloadsDir == null) {
throw FileSystemException('无法访问下载目录');
@ -304,12 +318,14 @@ class OtaUpgradeController extends GetxController {
final targetDir =
Directory('${downloadsDir.path}/Download/$directoryName');
if (!await targetDir.exists()) {
detailedStatus.value = '创建下载目录...';
await targetDir.create(recursive: true);
}
final savePath = '${targetDir.path}/$fileName';
// 新增:检查同名文件逻辑
detailedStatus.value = '检查本地文件...';
bool existingSameFile = false;
try {
var files = await targetDir.list().toList();
@ -330,12 +346,13 @@ class OtaUpgradeController extends GetxController {
if (existingSameFile) {
//Get.snackbar('文件已存在', '跳过下载,直接更新');
detailedStatus.value = '文件已存在,准备升级...';
startUpgrading(savePath);
return;
}
// 新增:删除其他文件逻辑
detailedStatus.value = '清理旧文件...';
try {
var files = await targetDir.list().toList();
for (var entity in files) {
@ -351,6 +368,7 @@ class OtaUpgradeController extends GetxController {
print('文件保存路径: $savePath');
detailedStatus.value = '开始下载固件包...';
final options = Options(
receiveTimeout: Duration.zero,
followRedirects: true,
@ -364,14 +382,19 @@ class OtaUpgradeController extends GetxController {
onReceiveProgress: (received, total) {
if (total != -1) {
downloadProgress.value = received / total;
detailedStatus.value = '正在下载固件包... ${(received / total * 100).toStringAsFixed(1)}%';
}
},
);
File file = File(savePath);
if (await file.exists()) {
// Get.snackbar('下载完成', '文件已保存至: $savePath');
detailedStatus.value = '下载完成,准备升级...';
} else {
Get.snackbar('错误', '文件未找到,请检查写入权限');
detailedStatus.value = '下载失败';
showDetailedStatus.value = false;
return;
}
isDownloading.value = false;
startUpgrading(savePath);
@ -381,12 +404,19 @@ class OtaUpgradeController extends GetxController {
} on FileSystemException catch (e) {
Get.snackbar('存储失败', '无法写入文件: ${e.message}');
} catch (e) {
Get.snackbar('未知错误', e.toString());
isDownloading.value = false;
detailedStatus.value = '下载失败,请检查网络连接';
showDetailedStatus.value = false;
Get.snackbar('下载失败', '请检查网络连接: $e');
print('下载失败: $e');
}
}
// 开始升级
void startUpgrading(String path) async {
detailedStatus.value = '准备设备连接...';
await Future.delayed(const Duration(milliseconds: 500));
detailedStatus.value = '正在连接设备...';
jlota.startUpgrading(address: _address, filePath: path);
return;
// isUpgrade.value = false;
@ -399,6 +429,60 @@ class OtaUpgradeController extends GetxController {
// isUpgrade.value = false;
}
// 检查设备电量是否满足升级条件
bool _checkBatteryLevel() {
try {
final deviceInfo = _bleManager.deviceInfo.value;
if (deviceInfo == null) {
Get.snackbar(
'电量检查失败',
'无法获取设备信息,请检查设备连接',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.orange,
colorText: Colors.white,
);
return false;
}
final leftBattery = deviceInfo.leftEarBattery;
final rightBattery = deviceInfo.rightEarBattery;
// 检查左右耳电量是否都大于30%
if (leftBattery <= 30 || rightBattery <= 30) {
String message = '设备电量不足,无法进行固件升级\n';
message += '升级要求:左右耳电量均需大于30%\n';
message += '当前电量:左耳 ${leftBattery}%,右耳 ${rightBattery}%\n';
message += '请充电后再试';
Get.dialog(
AlertDialog(
title: Text('电量不足'),
content: Text(message),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text('确定'),
),
],
),
);
return false;
}
return true;
} catch (e) {
Get.snackbar(
'电量检查失败',
'检查设备电量时发生错误:$e',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
return false;
}
}
void _listenToEvents() {
Ota.instance.eventStream.listen((event) {
if (event.event == 'onProgress' &&
@ -407,29 +491,38 @@ class OtaUpgradeController extends GetxController {
progress.value = event.progress!;
type.value = event.type!;
isUpgrade.value = true; // 开始升级
detailedStatus.value = '升级进度: ${(event.progress! * 100).toStringAsFixed(1)}%';
}
if (event.event == 'onStartOTA') {
type.value = OtaEvent.START_UPGRADING;
type.value = START_UPGRADING;
isUpgrade.value = true; // 开始升级
detailedStatus.value = 'OTA升级已开始...';
}
if (event.event == 'onStopOTA') {
type.value = OtaEvent.UPGRADE_SUCCESS;
type.value = UPGRADE_SUCCESS;
isUpgrade.value = false;
detailedStatus.value = '升级成功完成';
showDetailedStatus.value = false;
Get.snackbar('升级成功', '请退出重新连接设备');
_getCurrentVersion();
}
if (event.event == 'onError') {
type.value = OtaEvent.UPGRADE_FAILED;
type.value = UPGRADE_FAILED;
isUpgrade.value = false;
detailedStatus.value = '升级失败,错误码:${event.code}';
showDetailedStatus.value = false;
Get.snackbar('升级失败', '请检查设备连接或升级包,错误码:${event.code}');
}
if (event.event == 'NOCONNECTION') {
type.value = OtaEvent.CONNECTION_DISCONNECTION;
type.value = CONNECTION_DISCONNECTION;
isUpgrade.value = false;
detailedStatus.value = '设备连接断开';
showDetailedStatus.value = false;
}
if (event.event == 'CONNECTION_SUCCESS') {
type.value = OtaEvent.CONNECTION_SUCCESS;
type.value = CONNECTION_SUCCESS;
isUpgrade.value = true; // 开始升级
detailedStatus.value = '设备连接成功';
}
});
}

310
lib/modules/ota_upgrade/views/ota_upgrade_view.dart

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/ota_upgrade_controller.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../../data/services/ble_manager.dart';
class OtaUpgradeView extends GetView<OtaUpgradeController> {
const OtaUpgradeView({Key? key}) : super(key: key);
@ -89,17 +90,16 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
),
body: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h),
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 12.h),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 设备信息卡片 - 优化后的版本
_buildDeviceCard(isDarkMode),
SizedBox(height: 24.h),
SizedBox(height: 16.h),
// 更新说明 - 当有可用更新时显示
//_buildUpdateNotes(isDarkMode),
SizedBox(height: 16.h),
// 状态显示区域
Obx(() {
@ -150,12 +150,15 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
],
),
child: Padding(
padding: EdgeInsets.all(24.w),
padding: EdgeInsets.all(20.w),
child: Column(
children: [
// 设备图标区域
_buildDeviceIcon(isDarkMode),
SizedBox(height: 24.h),
SizedBox(height: 16.h),
// 设备详细信息区域
_buildDeviceDetailInfo(isDarkMode),
SizedBox(height: 16.h),
// 版本信息区域
_buildVersionInfo(isDarkMode),
],
@ -167,7 +170,7 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
// 设备图标
Widget _buildDeviceIcon(bool isDarkMode) {
return Container(
padding: EdgeInsets.all(20.w),
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isDarkMode
? Colors.white.withValues(alpha: 0.05)
@ -184,12 +187,193 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
isDarkMode
? 'assets/images/headphone_dark.png'
: 'assets/images/headphone_light.png',
height: 120.h,
height: 100.h,
fit: BoxFit.contain,
),
);
}
// 设备详细信息显示
Widget _buildDeviceDetailInfo(bool isDarkMode) {
return Obx(() {
final deviceInfo = Get.find<BleManager>().deviceInfo.value;
return Container(
padding: EdgeInsets.all(14.w),
decoration: BoxDecoration(
color: isDarkMode
? Colors.white.withValues(alpha: 0.03)
: Colors.white.withValues(alpha: 0.8),
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: isDarkMode
? Colors.white.withValues(alpha: 0.08)
: Colors.grey.withValues(alpha: 0.2),
width: 1,
),
),
child: Column(
children: [
// 设备型号信息
_buildDeviceInfoRow(
icon: Icons.headset,
label: '设备型号',
value: deviceInfo?.name ?? '未知设备',
isDarkMode: isDarkMode,
),
// SizedBox(height: 10.h),
// // 电量信息区域
// Row(
// children: [
// // 左耳电量
// Expanded(
// child: _buildBatteryInfo(
// label: '左耳电量',
// batteryLevel: deviceInfo?.leftEarBattery ?? 0,
// isCharging: deviceInfo?.leftEarCharging ?? false,
// isDarkMode: isDarkMode,
// icon: Icons.headset,
// ),
// ),
// SizedBox(width: 10.w),
// // 右耳电量
// Expanded(
// child: _buildBatteryInfo(
// label: '右耳电量',
// batteryLevel: deviceInfo?.rightEarBattery ?? 0,
// isCharging: deviceInfo?.rightEarCharging ?? false,
// isDarkMode: isDarkMode,
// icon: Icons.headset,
// ),
// ),
// ],
// ),
],
),
);
});
}
// 设备信息行组件
Widget _buildDeviceInfoRow({
required IconData icon,
required String label,
required String value,
required bool isDarkMode,
}) {
return Row(
children: [
Container(
padding: EdgeInsets.all(6.w),
decoration: BoxDecoration(
color: isDarkMode
? Colors.blue.withValues(alpha: 0.1)
: Colors.blue.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(6.r),
),
child: Icon(
icon,
size: 16.sp,
color: isDarkMode ? Colors.blue[300] : Colors.blue[600],
),
),
SizedBox(width: 12.w),
Text(
label,
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[300] : Colors.grey[700],
fontWeight: FontWeight.w500,
),
),
const Spacer(),
Text(
value,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
],
);
}
// 电池信息组件
Widget _buildBatteryInfo({
required String label,
required int batteryLevel,
required bool isCharging,
required bool isDarkMode,
required IconData icon,
}) {
Color batteryColor;
if (batteryLevel > 60) {
batteryColor = Colors.green;
} else if (batteryLevel > 30) {
batteryColor = Colors.orange;
} else {
batteryColor = Colors.red;
}
return Container(
padding: EdgeInsets.all(10.w),
decoration: BoxDecoration(
color: isDarkMode
? batteryColor.withValues(alpha: 0.1)
: batteryColor.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(8.r),
border: Border.all(
color: batteryColor.withValues(alpha: 0.3),
width: 1,
),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
isCharging ? Icons.battery_charging_full : Icons.battery_std,
size: 16.sp,
color: batteryColor,
),
SizedBox(width: 4.w),
Text(
label,
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[300] : Colors.grey[700],
fontWeight: FontWeight.w500,
),
),
],
),
SizedBox(height: 4.h),
Text(
'${batteryLevel}%',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.bold,
color: batteryColor,
),
),
if (isCharging) ...[
SizedBox(height: 1.h),
Text(
'充电中',
style: TextStyle(
fontSize: 10.sp,
color: batteryColor,
fontWeight: FontWeight.w500,
),
),
],
],
),
);
}
// 版本信息显示
Widget _buildVersionInfo(bool isDarkMode) {
return Column(
@ -201,7 +385,7 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
isDarkMode: isDarkMode,
isLatest: false,
),
SizedBox(height: 16.h),
SizedBox(height: 8.h),
// 最新版本信息
_buildVersionInfoRow(
label: '最新固件版本'.tr,
@ -210,7 +394,7 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
isLatest: true,
),
// 版本状态指示器
SizedBox(height: 20.h),
SizedBox(height: 14.h),
_buildVersionStatusIndicator(isDarkMode),
],
);
@ -224,7 +408,7 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
required bool isLatest,
}) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 10.h),
decoration: BoxDecoration(
color: isDarkMode
? Colors.white.withValues(alpha: 0.03)
@ -252,7 +436,8 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
children: [
if (isLatest && controller.isUpdateAvailable.value) ...[
Container(
padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 2.h),
padding:
EdgeInsets.symmetric(horizontal: 6.w, vertical: 2.h),
decoration: BoxDecoration(
color: Colors.orange.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8.r),
@ -403,7 +588,9 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
),
child: Icon(
Icons.new_releases,
color: isDarkMode ? Colors.amber[300] : Colors.amber[700],
color: isDarkMode
? Colors.amber[300]
: Colors.amber[700],
size: 18.sp,
),
),
@ -413,7 +600,9 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.amber[200] : Colors.amber[800],
color: isDarkMode
? Colors.amber[200]
: Colors.amber[800],
),
),
],
@ -574,7 +763,8 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
),
),
style: ElevatedButton.styleFrom(
backgroundColor: isDarkMode ? const Color(0xFF0066CC) : Colors.blue,
backgroundColor:
isDarkMode ? const Color(0xFF0066CC) : Colors.blue,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 14.h),
shape: RoundedRectangleBorder(
@ -597,6 +787,11 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
return _buildProgressIndicator(isDarkMode);
}
// 显示详细状态提示
if (controller.showDetailedStatus.value) {
return _buildDetailedStatusIndicator(isDarkMode);
}
return SizedBox(
width: double.infinity,
child: Column(
@ -685,7 +880,9 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
'取出双耳机,并保证电量均为30%以上的状态'.tr,
style: TextStyle(
fontSize: 13.sp,
color: isDarkMode ? Colors.orange[200] : Colors.orange[800],
color: isDarkMode
? Colors.orange[200]
: Colors.orange[800],
height: 1.3,
),
),
@ -700,6 +897,89 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
});
}
// 详细状态指示器
Widget _buildDetailedStatusIndicator(bool isDarkMode) {
return Obx(() {
return Container(
width: double.infinity,
padding: EdgeInsets.all(24.w),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDarkMode
? [
const Color(0xFF1A1A1A).withValues(alpha: 0.9),
const Color(0xFF2A2A2A).withValues(alpha: 0.9),
]
: [
const Color(0xFFF8FAFF),
const Color(0xFFE3F2FD),
],
),
borderRadius: BorderRadius.circular(20.r),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withValues(alpha: 0.3)
: Colors.black.withValues(alpha: 0.08),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: Column(
children: [
// 状态图标和文字
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: EdgeInsets.all(8.w),
decoration: BoxDecoration(
color: Colors.blue.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8.r),
),
child: SizedBox(
width: 20.sp,
height: 20.sp,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
isDarkMode ? const Color(0xFF0066CC) : Colors.blue,
),
),
),
),
SizedBox(width: 12.w),
Expanded(
child: Text(
controller.detailedStatus.value,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.blue[300] : Colors.blue[700],
),
),
),
],
),
SizedBox(height: 16.h),
// 提示信息
Text(
'请保持设备连接,不要关闭应用',
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
),
],
),
);
});
}
// 进度指示器
Widget _buildProgressIndicator(bool isDarkMode) {
return Obx(() {

27
lib/modules/pairing/controllers/pairing_controller.dart

@ -431,9 +431,12 @@ class PairingController extends GetxController {
try {
Logger.i(_tag, '开始验证设备: ${device.name} (${device.address})');
final result = await authbindingDevice(device);
if (result == 2) {
if (result == 10) {
//验证失败 进入手动授权
await connectble(device);
} else if (result == 9) {
Logger.i(_tag, 'BLE已连接,直接完成配对');
_transition(PairingState.completed);
} else if (result == 1) {
//跳转绑定界面
//验证失败 进入手动授权
@ -557,15 +560,22 @@ class PairingController extends GetxController {
//自动绑定设备 0 未合作商 1 合作商 2,已绑定
Future<int> authbindingDevice(BluetoothDevice device) async {
if (User.instance.earphonemac == device.address) {
if (User.instance.earphonelicense != "" &&
User.instance.earphonemac == device.address) {
// 已经绑定
return 2;
return 10;
}
if (User.instance.earphoneauthcode != "" &&
User.instance.earphonemac == device.address) {
// 已经绑定
return 9;
}
// 检查设备名称是否包含 DeepSound 或 DeapSound
final deviceName = device.name;
print('获取到 deviceName: $deviceName');
if (deviceName.contains('DeepSound') || deviceName.contains('DeapSound')) {
final names = AppConfig.allproductNames();
print('获取到 deviceName: $deviceName names:$names');
if (names.contains(deviceName)) {
await _bleManager.startScan(deviceName);
await Future.delayed(const Duration(seconds: 1));
// 尝试最多读取 3 次设备信息,每次间隔 1 秒
@ -610,7 +620,7 @@ class PairingController extends GetxController {
Logger.i(_tag, '绑定设备成功');
User.instance.earphonemac = currentDevice?.address ?? '';
User.instance.earphonename = currentDevice?.name ?? '';
User.instance.earphoneauthcode = deviceCode.value;
User.instance.earphonelicense = deviceCode.value;
_transition(PairingState.verifying);
_verifyDevice(_currentClassicDevice!);
return;
@ -642,8 +652,9 @@ class PairingController extends GetxController {
void onAutoCodeInputTap() async {
// 调用BLE管理器的自动绑定方法
final response = await Api.bindauthcode({
"code": codeInputController.text.trim(),
"earphonname": currentDevice?.name,
"earphonemac": currentDevice?.address,
"earphoneauthcode": codeInputController.text.trim(),
});
Logger.i(_tag, '手动授权结果 ${response}');
if (response != null) {

7
lib/modules/settings/controllers/settings_controller.dart

@ -23,9 +23,7 @@ class SettingsController extends GetxController {
// 语言设置
final List<LanguageModel> languageList = [
LanguageModel(
name: 'followingSystem', // 跟随系统.tr
alias: '',
value: 'system'),
name: 'followingSystem', alias: '', value: 'system'), // 跟随系统.tr
LanguageModel(name: 'english', alias: 'English', value: 'en_US'), // 英语.tr
LanguageModel(name: 'chinese', alias: '中文', value: 'zh_CN'), // 中文.tr
LanguageModel(
@ -78,8 +76,7 @@ class SettingsController extends GetxController {
LanguageModel(name: 'estonian', alias: 'Eesti', value: 'et_EE'), // 爱沙尼亚语
LanguageModel(
name: 'serbian', alias: 'Srpski', value: 'sr_RS'), // 塞尔维亚语(拉丁)
LanguageModel(
name: 'icelandic', alias: 'Íslenska', value: 'is_IS'), // 新增冰岛语
LanguageModel(name: 'icelandic', alias: 'Íslenska', value: 'is_IS'), // 冰岛语
];
Rx<LanguageModel> language = LanguageModel().obs;

49
lib/modules/settings/views/settings_view.dart

@ -203,7 +203,7 @@ class SettingsView extends GetView<SettingsController> {
controller.userInfo.value.viplv] ??
'Unknown';
return Text(
membershipString,
membershipString.tr, // 会员等级
style: TextStyle(
fontSize: 10.sp,
color: Colors.white,
@ -327,7 +327,7 @@ class SettingsView extends GetView<SettingsController> {
: Colors.cyan[100]!,
iconColor:
isDarkMode ? Colors.cyan[300]! : Colors.cyan[600]!,
value: controller.timbreName.value.tr,
value: controller.timbreName.value.tr, // 当前音色
valueStyle: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.grey[500],
@ -345,8 +345,8 @@ class SettingsView extends GetView<SettingsController> {
: Colors.grey[200]),
// 实时对话入口
_buildSimpleNavigationSetting(
title: '实时语音对话',
subtitle: '体验智能语音交互功能(功能预览)',
title: 'realtimeVoiceChat'.tr, // 实时语音对话
subtitle: 'realtimeVoiceChatInfo'.tr, // 实时语音对话信息
icon: Icons.chat_bubble_outline,
iconBgColor: isDarkMode
? Colors.purple[900]!.withOpacity(0.3)
@ -412,7 +412,7 @@ class SettingsView extends GetView<SettingsController> {
Obx(
() => _buildSimpleNavigationSetting(
title: 'languageSetting'.tr, // 语言设置
subtitle: controller.language.value.name.tr,
subtitle: controller.language.value.name.tr, // 当前语言
icon: Icons.language,
iconBgColor: isDarkMode
? Colors.indigo[900]!.withOpacity(0.3)
@ -506,7 +506,7 @@ class SettingsView extends GetView<SettingsController> {
// 清除配对设备
_buildSimpleNavigationSetting(
title: 'deviceManagement'.tr, // 设备管理
subtitle: 'deviceManagementInfo'.tr, // 设备管理列表
subtitle: 'manageConnectedDevices'.tr, // 管理已绑定的蓝牙设备
icon: Icons.settings_bluetooth,
iconBgColor: isDarkMode
? Colors.red[900]!.withOpacity(0.3)
@ -633,8 +633,8 @@ class SettingsView extends GetView<SettingsController> {
// : Colors.grey[200]),
// 测试ble耳机固件版本升级
_buildSimpleNavigationSetting(
title: '固件升级'.tr,
subtitle: 'ble耳机固件版本升级'.tr,
title: 'firmwareUpgrade'.tr, // 固件升级
subtitle: 'bleHeadsetFirmwareUpgrade'.tr, // BLE耳机固件版本升级
icon: Icons.system_update_alt,
iconBgColor: isDarkMode
? Colors.orange[900]!.withOpacity(0.3)
@ -651,14 +651,21 @@ class SettingsView extends GetView<SettingsController> {
: Colors.grey[200]),
// 添加权限管理设置项
_buildSettingItem(
_buildSimpleNavigationSetting(
title: 'permissionManagement'.tr, // 权限管理
subtitle: 'permissionManagementInfo'.tr, // 管理应用权限设置
icon: Icons.security,
iconColor: Colors.orange,
title: '权限管理'.tr,
isDarkMode: isDarkMode,
iconBgColor: isDarkMode
? Colors.orange[900]!.withOpacity(0.3)
: Colors.orange[100]!,
iconColor:
isDarkMode ? Colors.orange[300]! : Colors.orange[600]!,
titleColor: isDarkMode ? Colors.white : null,
subtitleColor: isDarkMode ? Colors.white70 : null,
onTap: () {
Get.toNamed(Routes.permissions);
},
isDarkMode: isDarkMode,
),
],
),
@ -750,8 +757,8 @@ class SettingsView extends GetView<SettingsController> {
Obx(() => Text(
// 如果版本信息为空,显示加载中
controller.appVersion.value.isEmpty
? '${'versions'.tr} 加载中...'
: '${'versions'.tr} ${controller.appVersion.value} (Build ${controller.buildNumber.value})',
? '${'versions'.tr} ${'loading'.tr}' // 版本 加载中
: '${'versions'.tr} ${controller.appVersion.value} (Build ${controller.buildNumber.value})', // 版本号
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode
@ -784,7 +791,7 @@ class SettingsView extends GetView<SettingsController> {
borderRadius: BorderRadius.circular(16.r),
),
child: Text(
'checkUpdates'.tr,
'checkUpdates'.tr, // 检查更新
style: TextStyle(
fontSize: 12.sp,
color:
@ -825,7 +832,7 @@ class SettingsView extends GetView<SettingsController> {
Get.locale?.countryCode == 'CN')
InkWell(
onTap: () => Get.to(
() => const WebViewPage(
() => WebViewPage(
url: 'https://beian.miit.gov.cn',
title: 'ICP备案信息',
),
@ -1191,7 +1198,7 @@ class SettingsView extends GetView<SettingsController> {
return Container(
constraints: BoxConstraints(maxWidth: 120.w), // 限制最大宽度
child: Text(
'$serviceName'.tr,
'$serviceName'.tr, // 语音服务名称
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.grey[600],
@ -1284,7 +1291,7 @@ class SettingsView extends GetView<SettingsController> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${service['name']}'.tr,
'${service['name']}'.tr, // 服务名称
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
@ -1296,7 +1303,7 @@ class SettingsView extends GetView<SettingsController> {
),
SizedBox(height: 4.h),
Text(
'${service['description']}'.tr,
'${service['description']}'.tr, // 服务描述
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode
@ -1421,7 +1428,7 @@ class SettingsView extends GetView<SettingsController> {
children: [
Expanded(
child: Text(
language.name.tr,
language.name.tr, // 语言名称
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
@ -1433,7 +1440,7 @@ class SettingsView extends GetView<SettingsController> {
),
),
Text(
language.alias, // 这里使用原始名称,不加 .tr
language.alias, // 语言别名(原始名称,不加.tr)
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode

175
lib/modules/translation/controllers/translation_controller.dart

@ -27,6 +27,8 @@ enum AudioOutputDevice {
}
class TranslationController extends GetxController {
static const String TAG = 'TranslationController';
// 服务
final AsrService _asrService = Get.find<AsrService>();
final VolcanoTranslationService _translationService =
@ -59,6 +61,14 @@ class TranslationController extends GetxController {
// 主页面显示限制:只显示最近20条,保证界面性能和用户体验
static const int _maxDisplayItems = 20;
// 分页加载相关状态
final isLoadingMore = false.obs; // 是否正在加载更多历史记录
final hasMoreHistory = true.obs; // 是否还有更多历史记录
final currentPage = 1.obs; // 当前页码
final pageSize = 20; // 每页记录数量
final isLoadingMoreError = false.obs; // 加载更多时是否出错
bool _scrollListenerSetup = false; // 滚动监听器设置标志
RxInt activeSpeaker = 0.obs; // 0=无激活, 1=按钮1, 2=按钮2
RxInt lastActiveSpeaker = 0.obs; // 上一次状态
bool isContinuePlaying = false; // 是否继续播放
@ -181,11 +191,9 @@ class TranslationController extends GetxController {
// 初始化服务
_initServices();
suspendOtherServices();
// 使用微任务在布局完成后自动滚动到底部
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
// 🔑 reverse模式下不需要手动滚动,ListView会自然显示最新内容
// 参考Agent模块的实现模式
}
// 加载保存的语言设置
@ -754,18 +762,28 @@ class TranslationController extends GetxController {
}
}
// 滚动到底部
void _scrollToBottom() {
// 滚动到底部(适配reverse模式)
void _scrollToBottom({bool animate = true}) {
if (scrollController.hasClients &&
(translationHistory.isNotEmpty || currentSessionId != null)) {
try {
scrollController.animateTo(
scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
// 🔑 reverse模式下:滚动到顶部(position = 0)来显示最新消息
// 正常模式下:滚动到底部(maxScrollExtent)来显示最新消息
const targetPosition = 0.0; // reverse模式下最新消息在顶部
if (animate) {
// 有动画的滚动,用于新消息添加时
scrollController.animateTo(
targetPosition,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
} else {
// 无动画的跳转,用于初始化时
scrollController.jumpTo(targetPosition);
}
} catch (e) {
Logger.error('滚动到底部失败: ${e.toString()}');
Logger.error('滚动到顶部失败(reverse模式): ${e.toString()}');
}
}
}
@ -879,22 +897,37 @@ class TranslationController extends GetxController {
// 【注意】存储中的记录都是完成态记录(isIntermediate = false)
// 因为保存时已经过滤掉了中间态记录,确保数据质量
// 初始化分页状态
currentPage.value = 1;
isLoadingMore.value = false;
isLoadingMoreError.value = false;
// 主页面只显示最近的20条完成态记录,保证界面性能
if (allHistory.length > _maxDisplayItems) {
translationHistory.value =
allHistory.sublist(allHistory.length - _maxDisplayItems);
// 检查是否还有更多历史记录
hasMoreHistory.value = true;
} else {
translationHistory.value = allHistory;
hasMoreHistory.value = false;
}
// 使用微任务确保在视图完全渲染后滚动到底部
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
// 设置滚动监听器
_setupScrollListener();
// 🔑 reverse模式下不需要滚动到底部,ListView会自然显示最新记录
// 参考Agent模块:使用reverse:true时不需要手动滚动到底部
} catch (e) {
_storage.remove(_historyKey);
Logger.error('加载翻译历史失败: ${e.toString()}');
}
} else {
// 没有历史数据时也要初始化分页状态
hasMoreHistory.value = false;
currentPage.value = 1;
isLoadingMore.value = false;
isLoadingMoreError.value = false;
}
}
@ -924,6 +957,112 @@ class TranslationController extends GetxController {
}
}
// 设置滚动监听器(适配reverse模式)
void _setupScrollListener() {
if (_scrollListenerSetup) return; // 防止重复设置
scrollController.addListener(() {
// 🔑 reverse模式下:检查是否滚动到底部(实际是历史记录的顶部)
// 参考Agent模块的实现
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 100) {
// 当距离底部100像素时触发加载更多历史记录
if (hasMoreHistory.value && !isLoadingMore.value) {
Logger.d(TAG, '触发加载更多翻译历史记录(reverse模式)');
loadMoreHistory();
}
}
});
_scrollListenerSetup = true;
}
// 加载更多历史记录(分页加载)
Future<void> loadMoreHistory() async {
// 防止重复加载
if (isLoadingMore.value || !hasMoreHistory.value) {
Logger.d(TAG,
'跳过加载更多:isLoadingMore=${isLoadingMore.value}, hasMoreHistory=${hasMoreHistory.value}');
return;
}
try {
isLoadingMore.value = true;
isLoadingMoreError.value = false;
final nextPage = currentPage.value + 1;
Logger.i(TAG, '正在加载第 $nextPage 页翻译历史...');
// 从存储中获取所有历史记录
final historyJson = _storage.read(_historyKey);
if (historyJson != null) {
final List<dynamic> historyList = jsonDecode(historyJson);
final List<TranslationItem> allHistory =
historyList.map((item) => TranslationItem.fromJson(item)).toList();
// 计算分页参数
final totalItems = allHistory.length;
final totalPages = (totalItems / pageSize).ceil();
// 修复分页索引计算:从最新记录向前分页
// 第1页:最新20条 (index: totalItems-20 到 totalItems)
// 第2页:接下来20条 (index: totalItems-40 到 totalItems-20)
// 第3页:再接下来20条 (index: totalItems-60 到 totalItems-40)
final endIndex = totalItems - ((nextPage - 1) * pageSize);
final startIndex = totalItems - (nextPage * pageSize);
Logger.d(TAG,
'分页计算: 总记录=$totalItems, 总页数=$totalPages, 当前页=$nextPage, startIndex=$startIndex, endIndex=$endIndex');
if (endIndex > 0) {
// 获取当前页的数据,确保索引不越界
final actualStartIndex = startIndex < 0 ? 0 : startIndex;
final actualEndIndex = endIndex > totalItems ? totalItems : endIndex;
final pageItems =
allHistory.sublist(actualStartIndex, actualEndIndex);
Logger.d(TAG,
'实际索引: start=$actualStartIndex, end=$actualEndIndex, 获取${pageItems.length}条记录');
if (pageItems.isNotEmpty) {
// 🔑 简化延迟,参考Agent模块(300ms vs 1000ms)
await Future.delayed(const Duration(milliseconds: 300));
// 🔑 reverse模式下:将新记录插入到现有记录列表的开头
// 由于reverse显示,用户看到的顺序不会改变,无需复杂的位置保持
final updatedHistory = [...pageItems, ...translationHistory];
translationHistory.assignAll(updatedHistory);
// 更新分页状态
currentPage.value = nextPage;
// 修复hasMoreHistory判断:当startIndex <= 0时表示已经到达最早的记录
hasMoreHistory.value = startIndex > 0;
Logger.i(TAG,
'成功加载 ${pageItems.length} 条历史记录,当前页: $nextPage/$totalPages, 还有更多: ${hasMoreHistory.value}');
} else {
// 没有更多记录
hasMoreHistory.value = false;
Logger.i(TAG, '没有更多翻译历史记录');
}
} else {
// 索引超出范围,没有更多记录
hasMoreHistory.value = false;
Logger.i(TAG, '已到达历史记录开头');
}
} else {
// 没有存储数据
hasMoreHistory.value = false;
Logger.i(TAG, '存储中没有更多历史记录');
}
} catch (e) {
Logger.e(TAG, '加载更多历史记录失败: $e');
isLoadingMoreError.value = true;
} finally {
isLoadingMore.value = false;
}
}
// 获取所有完成态历史记录(供历史页面使用)
// 【修复】简化实现,合并存储和内存中的完成态记录
List<TranslationItem> getAllHistory() {
@ -1060,7 +1199,7 @@ class TranslationController extends GetxController {
// 滚动到底部 (公共方法,用于向后兼容)
void scrollToBottom({bool animate = true}) {
_scrollToBottom();
_scrollToBottom(animate: animate);
}
}

188
lib/modules/translation/views/translation_detail_view.dart

@ -35,9 +35,10 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? AppColors.darkBackground : Color(0xFFF5F5F8),
backgroundColor:
isDarkMode ? AppColors.darkBackground : Color(0xFFF5F5F8),
appBar: _buildAppBar(isDarkMode),
body: Obx(() {
// 显示加载状态
@ -48,7 +49,7 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
),
);
}
return Column(
children: [
_buildTabButtons(isDarkMode),
@ -58,11 +59,12 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
}),
);
}
// 构建应用栏
PreferredSizeWidget _buildAppBar(bool isDarkMode) {
return AppBar(
backgroundColor: isDarkMode ? AppColors.cardBackground : Color(0xFFF5F5F8),
backgroundColor:
isDarkMode ? AppColors.cardBackground : Color(0xFFF5F5F8),
elevation: 0,
leading: IconButton(
icon: Icon(
@ -93,7 +95,7 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
],
);
}
// 构建选项卡按钮
Widget _buildTabButtons(bool isDarkMode) {
return Padding(
@ -102,11 +104,11 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
children: [
Expanded(
child: Obx(() => _buildTabButton(
title: 'originalRecord'.tr, // 原始记录
isSelected: controller.selectedTabIndex.value == 0,
onTap: () => controller.switchTab(0),
isDarkMode: isDarkMode,
)),
title: 'originalRecord'.tr, // 原始记录
isSelected: controller.selectedTabIndex.value == 0,
onTap: () => controller.switchTab(0),
isDarkMode: isDarkMode,
)),
),
//屏蔽“记录摘要”,需要是开启
// SizedBox(width: 12.w),
@ -122,7 +124,7 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
),
);
}
// 构建内容区域
Widget _buildContentArea(bool isDarkMode) {
return Expanded(
@ -134,13 +136,13 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
children: [
_buildTimestamp(isDarkMode),
SizedBox(height: 8.h),
// 根据选中的选项卡显示不同内容
if (controller.selectedTabIndex.value == 0)
..._buildOriginalRecords(isDarkMode)
else
_buildSummaryWidget(isDarkMode),
SizedBox(height: 24.h),
],
);
@ -148,7 +150,7 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
),
);
}
// 构建时间戳
Widget _buildTimestamp(bool isDarkMode) {
return Padding(
@ -164,12 +166,14 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
),
);
}
// 构建原始记录列表
List<Widget> _buildOriginalRecords(bool isDarkMode) {
return controller.sessionItems.map((sessionItem) => _buildSessionItemWidget(sessionItem, isDarkMode)).toList();
return controller.sessionItems
.map((sessionItem) => _buildSessionItemWidget(sessionItem, isDarkMode))
.toList();
}
// 构建选项卡按钮
Widget _buildTabButton({
required String title,
@ -182,8 +186,8 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
child: Container(
height: 40.h,
decoration: BoxDecoration(
color: isSelected
? (isDarkMode ? AppColors.primary : Colors.blue)
color: isSelected
? (isDarkMode ? AppColors.primary : Colors.blue)
: (isDarkMode ? AppColors.cardBackground : Colors.white),
borderRadius: BorderRadius.circular(20.r),
boxShadow: [
@ -200,15 +204,15 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
style: TextStyle(
fontSize: 15.sp,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected
? Colors.white
color: isSelected
? Colors.white
: (isDarkMode ? Colors.white : Colors.black87),
),
),
),
);
}
// 构建会话详情中的翻译项
Widget _buildSessionItemWidget(TranslationItem item, bool isDarkMode) {
final context = Get.context!;
@ -218,7 +222,8 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
decoration: BoxDecoration(
color: item.isIntermediate
? Colors.grey.withOpacity(0.1) // 中间结果使用浅灰色底色
: (item.sourceLanguageCode == controller.currentItem.sourceLanguageCode
: (item.sourceLanguageCode ==
controller.currentItem.sourceLanguageCode
? Theme.of(context)
.colorScheme
.primary
@ -238,13 +243,35 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 源文本
Text(
item.sourceText,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).textTheme.bodyLarge?.color,
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 源文本
Expanded(
child: Text(
item.sourceText,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).textTheme.bodyLarge?.color,
),
)),
// 删除按钮
if (!item.isIntermediate)
InkWell(
onTap: () => _showDeleteItemConfirmDialog(item),
borderRadius: BorderRadius.circular(6.r),
splashColor: Colors.red.withValues(alpha: 0.1),
highlightColor: Colors.red.withValues(alpha: 0.05),
child: Container(
padding: EdgeInsets.all(6.w),
child: Icon(
Icons.delete_outline,
size: 16.sp,
color: isDarkMode ? Colors.grey[500] : Colors.grey[400],
),
),
),
],
),
if (item.translatedText.isNotEmpty) ...[
const SizedBox(height: 8),
@ -273,22 +300,6 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
Row(
mainAxisSize: MainAxisSize.min,
children: [
// 删除按钮
if (!item.isIntermediate)
InkWell(
onTap: () => _showDeleteItemConfirmDialog(item),
borderRadius: BorderRadius.circular(6.r),
splashColor: Colors.red.withValues(alpha: 0.1),
highlightColor: Colors.red.withValues(alpha: 0.05),
child: Container(
padding: EdgeInsets.all(6.w),
child: Icon(
Icons.delete_outline,
size: 16.sp,
color: isDarkMode ? Colors.grey[500] : Colors.grey[400],
),
),
),
// 播放按钮
if (!item.isIntermediate) ...[
SizedBox(width: 8.w),
@ -310,25 +321,25 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
);
}
// 构建总结视图
Widget _buildSummaryWidget(bool isDarkMode) {
final String combinedSourceText = _getCombinedSourceText();
return Column(
children: [
// 摘要内容容器
_buildSummaryContainer(isDarkMode),
// 提取摘要按钮
Obx(() => _buildExtractSummaryButton(combinedSourceText, isDarkMode)),
// 重新生成按钮
Obx(() => _buildRegenerateSummaryButton(combinedSourceText, isDarkMode)),
Obx(() =>
_buildRegenerateSummaryButton(combinedSourceText, isDarkMode)),
],
);
}
// 构建摘要容器
Widget _buildSummaryContainer(bool isDarkMode) {
return Container(
@ -354,7 +365,7 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
}),
);
}
// 构建空摘要提示
Widget _buildEmptySummaryPrompt(bool isDarkMode) {
return Padding(
@ -372,7 +383,7 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
),
);
}
// 构建摘要内容
Widget _buildSummaryContent(bool isDarkMode) {
return Column(
@ -417,9 +428,12 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
Material(
color: Colors.transparent,
child: InkWell(
onTap: () => controller.playText(controller.summaryResult.value),
onTap: () =>
controller.playText(controller.summaryResult.value),
borderRadius: BorderRadius.circular(20.r),
splashColor: isDarkMode ? AppColors.primary.withOpacity(0.2) : Colors.blue.withOpacity(0.2),
splashColor: isDarkMode
? AppColors.primary.withOpacity(0.2)
: Colors.blue.withOpacity(0.2),
child: Padding(
padding: EdgeInsets.all(8.r),
child: Icon(
@ -437,7 +451,9 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
child: InkWell(
onTap: _copySummaryToClipboard,
borderRadius: BorderRadius.circular(20.r),
splashColor: isDarkMode ? AppColors.primary.withOpacity(0.2) : Colors.blue.withOpacity(0.2),
splashColor: isDarkMode
? AppColors.primary.withOpacity(0.2)
: Colors.blue.withOpacity(0.2),
child: Padding(
padding: EdgeInsets.all(8.r),
child: Icon(
@ -454,7 +470,7 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
],
);
}
// 复制摘要到剪贴板
void _copySummaryToClipboard() {
Clipboard.setData(ClipboardData(text: controller.summaryResult.value));
@ -465,16 +481,19 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
duration: Duration(seconds: 2),
);
}
// 构建提取摘要按钮
Widget _buildExtractSummaryButton(String combinedSourceText, bool isDarkMode) {
final bool isDisabled = controller.isGeneratingSummary.value || controller.summaryResult.value.isNotEmpty;
Widget _buildExtractSummaryButton(
String combinedSourceText, bool isDarkMode) {
final bool isDisabled = controller.isGeneratingSummary.value ||
controller.summaryResult.value.isNotEmpty;
return Container(
width: double.infinity,
margin: EdgeInsets.only(bottom: 16.h),
child: ElevatedButton(
onPressed: isDisabled ? null : () => _extractSummary(combinedSourceText),
onPressed:
isDisabled ? null : () => _extractSummary(combinedSourceText),
style: ElevatedButton.styleFrom(
backgroundColor: isDarkMode ? AppColors.primary : Colors.blue,
foregroundColor: Colors.white,
@ -483,14 +502,18 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
borderRadius: BorderRadius.circular(16.r),
),
elevation: 0,
disabledBackgroundColor: controller.isGeneratingSummary.value
? isDarkMode ? AppColors.primary.withAlpha(128) : Colors.blue.withAlpha(128)
disabledBackgroundColor: controller.isGeneratingSummary.value
? isDarkMode
? AppColors.primary.withAlpha(128)
: Colors.blue.withAlpha(128)
: Colors.grey.withAlpha(128),
),
child: controller.isGeneratingSummary.value
? _buildGeneratingIndicator(isDarkMode)
: Text(
controller.summaryResult.value.isEmpty ? 'extractSummary'.tr : 'summaryGenerated'.tr, // 提取摘要 : 已生成摘要
controller.summaryResult.value.isEmpty
? 'extractSummary'.tr
: 'summaryGenerated'.tr, // 提取摘要 : 已生成摘要
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
@ -499,7 +522,7 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
),
);
}
// 构建生成中指示器
Widget _buildGeneratingIndicator(bool isDarkMode) {
return Row(
@ -524,13 +547,15 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
],
);
}
// 构建重新生成按钮
Widget _buildRegenerateSummaryButton(String combinedSourceText, bool isDarkMode) {
if (controller.summaryResult.value.isEmpty || controller.isGeneratingSummary.value) {
Widget _buildRegenerateSummaryButton(
String combinedSourceText, bool isDarkMode) {
if (controller.summaryResult.value.isEmpty ||
controller.isGeneratingSummary.value) {
return SizedBox.shrink();
}
return Container(
width: double.infinity,
margin: EdgeInsets.only(bottom: 16.h),
@ -554,17 +579,17 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
),
);
}
// 提取摘要方法
void _extractSummary(String text) async {
final result = await controller.extractSummary(text);
// 检查是否超时,如果超时则显示重试选项
if (result.contains('超时')) {
_showTimeoutDialog(text);
}
}
// 显示超时对话框
void _showTimeoutDialog(String text) {
Get.dialog(
@ -720,11 +745,13 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
// 分享原始记录
final String sourceText = _getCombinedSourceText();
final String translatedText = _getCombinedTranslatedText();
shareText = '${'originalText'.tr}:\n$sourceText\n\n${'translatedText'.tr}:\n$translatedText'; // 原文:译文:
shareText =
'${'originalText'.tr}:\n$sourceText\n\n${'translatedText'.tr}:\n$translatedText'; // 原文:译文:
} else {
// 分享摘要
if (controller.summaryResult.value.isNotEmpty) {
shareText = '${'sessionSummary'.tr}:\n${controller.summaryResult.value}'; // 会话摘要:
shareText =
'${'sessionSummary'.tr}:\n${controller.summaryResult.value}'; // 会话摘要:
} else {
Get.snackbar(
'cannotShare'.tr, // 无法分享
@ -735,11 +762,12 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
return;
}
}
// 调用系统分享
Share.share(
shareText,
subject: '${'translationSessionRecord'.tr} - ${DateFormat('yyyy-MM-dd HH:mm').format(controller.currentItem.timestamp)}', // 翻译会话记录
subject:
'${'translationSessionRecord'.tr} - ${DateFormat('yyyy-MM-dd HH:mm').format(controller.currentItem.timestamp)}', // 翻译会话记录
);
}
}
}

91
lib/modules/translation/views/translation_history_view.dart

@ -15,9 +15,10 @@ class TranslationHistoryView extends GetView<TranslationHistoryController> {
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? AppColors.darkBackground : Color(0xFFF5F5F8),
backgroundColor:
isDarkMode ? AppColors.darkBackground : Color(0xFFF5F5F8),
appBar: _buildAppBar(isDarkMode),
body: Obx(() {
if (!controller.hasHistory) {
@ -46,7 +47,7 @@ class TranslationHistoryView extends GetView<TranslationHistoryController> {
itemBuilder: (context, index) {
final date = controller.sortedDates[index];
final items = controller.groupedHistory.value[date]!;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -70,9 +71,10 @@ class TranslationHistoryView extends GetView<TranslationHistoryController> {
}),
);
}
// 构建日期组容器
Widget _buildDateGroupContainer(List<TranslationItem> items, bool isDarkMode) {
Widget _buildDateGroupContainer(
List<TranslationItem> items, bool isDarkMode) {
return Container(
margin: EdgeInsets.only(bottom: 16.h),
decoration: BoxDecoration(
@ -104,7 +106,7 @@ class TranslationHistoryView extends GetView<TranslationHistoryController> {
),
);
}
// 构建组内的历史记录项
Widget _buildHistoryItemInGroup(TranslationItem item, bool isDarkMode) {
return InkWell(
@ -117,10 +119,11 @@ class TranslationHistoryView extends GetView<TranslationHistoryController> {
borderRadius: BorderRadius.circular(8.r),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
child: Stack(
children: [
Expanded(
// 主要内容区域
Padding(
padding: EdgeInsets.only(right: 50.w), // 为右侧图标留出空间
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -155,7 +158,8 @@ class TranslationHistoryView extends GetView<TranslationHistoryController> {
_formatSessionTime(item.timestamp),
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[500] : Colors.grey[600],
color:
isDarkMode ? Colors.grey[500] : Colors.grey[600],
),
),
SizedBox(width: 12.w),
@ -165,7 +169,9 @@ class TranslationHistoryView extends GetView<TranslationHistoryController> {
_formatLanguagePair(item),
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[500] : Colors.grey[600],
color: isDarkMode
? Colors.grey[500]
: Colors.grey[600],
),
overflow: TextOverflow.ellipsis,
),
@ -175,37 +181,45 @@ class TranslationHistoryView extends GetView<TranslationHistoryController> {
],
),
),
SizedBox(width: 12.w),
// 删除会话按钮
InkWell(
onTap: () => _showDeleteSessionConfirmDialog(item.sessionId),
borderRadius: BorderRadius.circular(6.r),
splashColor: Colors.red.withOpacity(0.1),
highlightColor: Colors.red.withOpacity(0.05),
child: Container(
padding: EdgeInsets.all(8.w),
child: Icon(
Icons.delete_outline,
color: isDarkMode ? Colors.grey[500] : Colors.grey[400],
size: 18.sp,
// 右上角删除按钮
Positioned(
top: 0,
right: -8.w,
child: InkWell(
onTap: () => _showDeleteSessionConfirmDialog(item.sessionId),
borderRadius: BorderRadius.circular(6.r),
splashColor: Colors.red.withOpacity(0.1),
highlightColor: Colors.red.withOpacity(0.05),
child: Container(
padding: EdgeInsets.all(8.w),
child: Icon(
Icons.delete_outline,
color: isDarkMode ? Colors.grey[500] : Colors.grey[400],
size: 18.sp,
),
),
),
),
SizedBox(width: 8.w),
Icon(
Icons.chevron_right,
color: isDarkMode ? Colors.grey[600] : Colors.grey[350],
size: 20.sp,
// 右下角箭头图标
Positioned(
bottom: 0,
right: 0.w, // 与删除图标垂直对齐
child: Icon(
Icons.chevron_right,
color: isDarkMode ? Colors.grey[600] : Colors.grey[350],
size: 20.sp,
),
),
],
),
),
);
}
void _showTranslationDetail(TranslationItem item) {
// 获取该会话的所有翻译项
final List<TranslationItem> sessionItems = controller.getSessionItems(item.sessionId);
final List<TranslationItem> sessionItems =
controller.getSessionItems(item.sessionId);
Get.to(
() => TranslationDetailView(),
@ -238,8 +252,12 @@ class TranslationHistoryView extends GetView<TranslationHistoryController> {
final languageManager = Get.find<LanguageManager>();
// 获取源语言和目标语言的中文名称
final sourceName = languageManager.getChineseNameByAsrCode(item.sourceLanguageCode) ?? 'unknownLanguage'.tr; // 未知语言
final targetName = languageManager.getChineseNameByAsrCode(item.targetLanguageCode) ?? 'unknownLanguage'.tr; // 未知语言
final sourceName =
languageManager.getChineseNameByAsrCode(item.sourceLanguageCode) ??
'unknownLanguage'.tr; // 未知语言
final targetName =
languageManager.getChineseNameByAsrCode(item.targetLanguageCode) ??
'unknownLanguage'.tr; // 未知语言
// 使用双向箭头符号表示翻译方向
return '$sourceName ⇄ $targetName';
@ -355,11 +373,12 @@ class TranslationHistoryView extends GetView<TranslationHistoryController> {
),
);
}
// 构建应用栏
PreferredSizeWidget _buildAppBar(bool isDarkMode) {
return AppBar(
backgroundColor: isDarkMode ? AppColors.cardBackground : Color(0xFFF5F5F8),
backgroundColor:
isDarkMode ? AppColors.cardBackground : Color(0xFFF5F5F8),
elevation: 0,
leading: IconButton(
icon: Icon(
@ -392,4 +411,4 @@ class TranslationHistoryView extends GetView<TranslationHistoryController> {
],
);
}
}
}

344
lib/modules/translation/views/translation_view.dart

@ -109,6 +109,7 @@ class TranslationView extends GetView<TranslationController> {
)),
centerTitle: true,
actions: [
Obx(() {
if (controller.isRecording.value) {
// 录音中:显示红色计时器样式,点击可停止录音
@ -169,19 +170,19 @@ class TranslationView extends GetView<TranslationController> {
},
tooltip: 'viewAudioFiles'.tr, // 查看录音文件
),
// 添加历史记录按钮
IconButton(
icon: Icon(
Icons.history,
color: isDarkMode ? Colors.white : Colors.black,
size: 24.sp,
),
onPressed: () async {
await controller.stopAll();
Get.toNamed(Routes.translationHistory);
},
tooltip: 'translationHistory'.tr, // 翻译历史
),
// 历史记录按钮
// IconButton(
// icon: Icon(
// Icons.history,
// color: isDarkMode ? Colors.white : Colors.black,
// size: 24.sp,
// ),
// onPressed: () async {
// await controller.stopAll();
// Get.toNamed(Routes.translationHistory);
// },
// tooltip: 'translationHistory'.tr, // 翻译历史
// ),
],
);
}
@ -289,10 +290,12 @@ class TranslationView extends GetView<TranslationController> {
Widget _buildTranslationContent(bool isDarkMode) {
return Obx(() {
final itemCount = controller.translationHistory.length;
final itemCount = controller.translationHistory.length +
(controller.hasMoreHistory.value ? 1 : 0);
return ListView.builder(
controller: controller.scrollController,
reverse: true, // 🔑 关键优化:采用reverse模式,参考Agent模块
padding: EdgeInsets.only(
left: 16.w,
right: 16.w,
@ -301,8 +304,21 @@ class TranslationView extends GetView<TranslationController> {
),
itemCount: itemCount,
itemBuilder: (context, index) {
// 如果是最后一项且有更多历史记录,显示加载指示器(reverse模式下)
if (index == controller.translationHistory.length && controller.hasMoreHistory.value) {
return _buildLoadMoreIndicator();
}
// reverse模式下需要反转索引来获取正确的历史记录
final historyIndex = controller.translationHistory.length - 1 - index;
// 检查索引是否有效(reverse模式下的索引验证)
if (historyIndex < 0 || historyIndex >= controller.translationHistory.length) {
return const SizedBox.shrink();
}
// 获取当前项
final historyItem = controller.translationHistory[index];
final historyItem = controller.translationHistory[historyIndex];
// 构建项目widget
return _buildTranslationItemWidget(context, historyItem);
@ -339,13 +355,34 @@ class TranslationView extends GetView<TranslationController> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 源文本
Text(
item.sourceText,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).textTheme.bodyLarge?.color,
),
// 源文本和删除按钮行
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 源文本
Expanded(
child: Text(
item.sourceText,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).textTheme.bodyLarge?.color,
),
),
),
// 删除按钮 - 只对完成态记录显示,与源文本平齐
if (!item.isIntermediate)
GestureDetector(
onTap: () => _showDeleteItemConfirmDialog(item),
child: Container(
padding: EdgeInsets.all(4.w),
child: Icon(
Icons.delete_outline,
size: 18.sp,
color: Colors.grey[500],
),
),
),
],
),
if (item.translatedText.isNotEmpty) ...[
const SizedBox(height: 8),
@ -364,20 +401,26 @@ class TranslationView extends GetView<TranslationController> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${item.sourceLanguageName.tr} → ${item.targetLanguageName.tr}',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
Expanded(
child: Text(
'${item.sourceLanguageName.tr} → ${item.targetLanguageName.tr}',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
),
// 播放按钮
if (!item.isIntermediate)
GestureDetector(
onTap: () => controller.playTranslationItem(item),
child: Icon(
Icons.volume_up,
size: 20,
color: Theme.of(context).colorScheme.primary,
child: Container(
padding: EdgeInsets.all(4.w),
child: Icon(
Icons.volume_up,
size: 20.sp,
color: Theme.of(context).colorScheme.primary,
),
),
),
],
@ -716,4 +759,243 @@ class TranslationView extends GetView<TranslationController> {
),
);
}
// 构建加载更多指示器
Widget _buildLoadMoreIndicator() {
return Obx(() {
if (controller.isLoadingMore.value) {
// 正在加载状态 - 增强视觉效果
return Container(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2.5,
valueColor: const AlwaysStoppedAnimation<Color>(
AppColors.primary,
),
),
),
const SizedBox(height: 12),
Text(
'正在加载历史记录...',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
),
],
),
),
);
} else if (controller.isLoadingMoreError.value) {
// 加载失败状态 - 增强视觉效果
return Container(
padding: const EdgeInsets.symmetric(vertical: 18),
child: Center(
child: GestureDetector(
onTap: () => controller.loadMoreHistory(),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.red[50],
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.red[200]!, width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.refresh,
size: 18.sp,
color: Colors.red[600],
),
SizedBox(width: 8.w),
Text(
'加载失败,点击重试',
style: TextStyle(
fontSize: 14.sp,
color: Colors.red[600],
fontWeight: FontWeight.w500,
),
),
],
),
),
),
),
);
} else {
// 默认状态(有更多记录但未加载)- 增强视觉效果
return Container(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(16),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.keyboard_arrow_up,
size: 16.sp,
color: Colors.grey[500],
),
SizedBox(width: 4.w),
Text(
'向上滑动查看更多历史',
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey[600],
),
),
],
),
),
),
);
}
});
}
// 显示删除单条记录确认对话框
void _showDeleteItemConfirmDialog(TranslationItem item) {
final isDarkMode = Theme.of(Get.context!).brightness == Brightness.dark;
Get.dialog(
AlertDialog(
backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white,
title: Text(
'deleteTranslationRecord'.tr, // 删除翻译记录
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black,
),
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'deleteTranslationConfirm'.tr, // 确定要删除这条翻译记录吗?此操作无法撤销。
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.grey[300] : Colors.black87,
),
),
SizedBox(height: 12.h),
// 显示要删除的记录预览
Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: isDarkMode
? Colors.grey[800]?.withOpacity(0.5)
: Colors.grey[100],
borderRadius: BorderRadius.circular(8.r),
border: Border.all(
color: isDarkMode ? Colors.grey[700]! : Colors.grey[300]!,
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.sourceText,
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black,
fontWeight: FontWeight.w500,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (item.translatedText.isNotEmpty) ...[
SizedBox(height: 4.h),
Text(
item.translatedText,
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[300] : Colors.grey[700],
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
],
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'cancel'.tr,
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[700],
),
),
),
TextButton(
onPressed: () {
Get.back();
// 删除指定的翻译记录
_deleteTranslationItem(item);
},
child: Text(
'confirm'.tr,
style: TextStyle(
fontSize: 16.sp,
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
),
],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
),
);
}
// 删除翻译记录的具体实现
void _deleteTranslationItem(TranslationItem item) {
try {
// 调用控制器的删除方法
controller.deleteTranslationItem(item.sessionId, item.timestamp);
// 显示删除成功提示
Get.snackbar(
'删除成功', // deleteSuccess
'已删除该翻译记录', // deleteTranslationSuccess
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Get.isDarkMode ? Colors.grey[800] : Colors.grey[200],
colorText: Get.isDarkMode ? Colors.white : Colors.black,
duration: const Duration(seconds: 2),
);
} catch (e) {
// 显示删除失败提示
Get.snackbar(
'删除失败', // deleteFailed
'删除翻译记录时发生错误: ${e.toString()}', // deleteTranslationError
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red[400],
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
}
}
}

14
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt

@ -734,6 +734,7 @@ object AgentService : CoroutineScope {
) {
Log.d(TAG, "内部方法:通用的ChatAPI处理逻辑")
// 如果有正在进行的AI流式输出,先停止它
audioPlayer?.stopAudio()
stopAiStream()
stopTts()
// 记录新的AI任务
@ -748,6 +749,7 @@ object AgentService : CoroutineScope {
val responseBuilder = StringBuilder()
var aiMetadata:String = ""
var userMetadata = JSONObject()
var reply = false; //是否回复
var broadcast = true //是否播发
// 如果有图片,添加图片信息到元数据
if (hasImage) {
@ -814,10 +816,10 @@ object AgentService : CoroutineScope {
return
}
responseBuilder.append(token)
if (speakResponse && broadcast) {
if (speakResponse && reply && broadcast) {
ttsService?.speakStream(token)
}
if (broadcast) {
if (reply && token.isNotEmpty()) {
// 发送流式回复token
sendEvent("assistant_token", mapOf("token" to token))
}
@ -829,7 +831,7 @@ object AgentService : CoroutineScope {
override fun onComplete(sessionid:String) {
try {
// 视情况决定是否朗读回复
if (speakResponse && broadcast && sessionid == currsessionId){
if (speakResponse && reply && broadcast && sessionid == currsessionId){
ttsService?.flushStream()
audioPlayer?.stopAudio()
}
@ -842,7 +844,10 @@ object AgentService : CoroutineScope {
if (hasImage) {
responseData["hasImage"] = true
}
sendEvent("assistant_response", responseData)
if (reply){
sendEvent("assistant_response", responseData)
}
if (response.isNotEmpty()) {
// 添加AI回复到历史记录
addToHistoryMessages(createAssistantMessage(response))
@ -903,6 +908,7 @@ object AgentService : CoroutineScope {
val resultmap = autoHandleFcunCallResult(name, functionCallResult);
aiMetadata = resultmap["meta"] as String;
broadcast = resultmap["broadcast"] as Boolean;
reply = !(resultmap["notreply"] as Boolean);
sendEvent(
"function_call_result", mapOf(
"function_call" to functionCall.toString(),

144
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt

@ -8,6 +8,7 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.location.LocationManager
import android.media.AudioManager
import android.net.Uri
import android.os.Build
import android.provider.AlarmClock
@ -232,6 +233,42 @@ class SystemFunctionHandler(private val context: Context? = null) {
),
handler = ExitNavigationHandler(context)
)
// 增加音量
client.registerLocalFunction(
name = "add_volume",
description = "增大音量",
parameters = mapOf(
"type" to "object",
"properties" to emptyMap<String, Any>(),
"required" to emptyList<String>()
),
handler = AddVolumeHandler(context)
)
// 增加音量
client.registerLocalFunction(
name = "reduce_volume",
description = "降低音量",
parameters = mapOf(
"type" to "object",
"properties" to emptyMap<String, Any>(),
"required" to emptyList<String>()
),
handler = ReduceVolumeHandler(context)
)
// 设置静音
client.registerLocalFunction(
name = "mute_volume",
description = "设置静音",
parameters = mapOf(
"type" to "object",
"properties" to emptyMap<String, Any>(),
"required" to emptyList<String>()
),
handler = MuteVolumeHandler(context)
)
}
/**
@ -1320,4 +1357,109 @@ private class OpenRecorderHandler(private val context: Context?) : FunctionHandl
}.toString()
}
}
}
}
/**
* 添加音量处理类
*/
private class AddVolumeHandler(private val context: Context?) : FunctionHandler {
//设置音量
fun setVolume(context: Context, streamType: Int, volume: Int) {
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val maxVolume = audioManager.getStreamMaxVolume(streamType)
val safeVolume = volume.coerceIn(0, maxVolume)
audioManager.setStreamVolume(streamType, safeVolume, AudioManager.FLAG_SHOW_UI)
}
fun getCurrentVolume(context: Context, streamType: Int): Int {
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
return audioManager.getStreamVolume(streamType)
}
fun getMaxVolume(context: Context, streamType: Int): Int {
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
return audioManager.getStreamMaxVolume(streamType)
}
//增加音量/减少音量
fun adjustVolume(context: Context, direction: Int) {
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, direction, AudioManager.FLAG_SHOW_UI)
}
override suspend fun handle(arguments: Map<String, Any>): String {
if (context == null) {
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "context_error")
put("message", "上下文未初始化")
})
}.toString()
}
// 发送广播通知退出交互
val result = adjustVolume(context, AudioManager.ADJUST_RAISE)
return JSONObject().apply {
put("result", "增加音量成功!")
put("broadcast", false)
}.toString()
}
}
/**
* 添加音量处理类
*/
private class ReduceVolumeHandler(private val context: Context?) : FunctionHandler {
//增加音量/减少音量
fun adjustVolume(context: Context, direction: Int) {
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, direction, AudioManager.FLAG_SHOW_UI)
}
override suspend fun handle(arguments: Map<String, Any>): String {
if (context == null) {
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "context_error")
put("message", "上下文未初始化")
})
}.toString()
}
// 发送广播通知退出交互
adjustVolume(context, AudioManager.ADJUST_LOWER)
return JSONObject().apply {
put("result", "降低音量成功!")
put("broadcast", false)
}.toString()
}
}
/**
* 添加音量处理类
*/
private class MuteVolumeHandler(private val context: Context?) : FunctionHandler {
//设置音量
fun setVolume(context: Context, streamType: Int, volume: Int) {
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val maxVolume = audioManager.getStreamMaxVolume(streamType)
val safeVolume = volume.coerceIn(0, maxVolume)
audioManager.setStreamVolume(streamType, safeVolume, AudioManager.FLAG_SHOW_UI)
}
override suspend fun handle(arguments: Map<String, Any>): String {
if (context == null) {
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "context_error")
put("message", "上下文未初始化")
})
}.toString()
}
// 发送广播通知退出交互
setVolume(context, AudioManager.STREAM_MUSIC, 0)
return JSONObject().apply {
put("result", "设置静音成功!")
put("broadcast", false)
}.toString()
}
}

102
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt

@ -30,6 +30,7 @@ import androidx.core.content.ContextCompat
import android.app.ActivityManager
import android.app.Application
import android.content.ComponentName
import android.media.AudioManager
import android.os.Process
/**
* 系统功能处理器 - 负责调用系统API执行各种功能以及注册系统功能
@ -78,6 +79,13 @@ class SystemFunctionHandler(private val context: Context) {
// 注册关闭导航服务功能
registerStopNavigationFunction(client)
//增加音量
registerAddVolumeFunction(client)
//减少音量
registerReduceVolumeFunction(client)
//静音
registerMuteVolumeFunction(client)
Log.d(TAG, "所有系统功能注册完成")
} catch (e: Exception) {
Log.e(TAG, "注册系统功能失败: ${e.message}", e)
@ -438,6 +446,78 @@ class SystemFunctionHandler(private val context: Context) {
)
Log.d(TAG, "已注册获取当前时间功能")
}
/**
* 注册添加音量函数
*/
private fun registerAddVolumeFunction(client: MCPClient) {
client.registerLocalFunction(
"add_volume",
"增大音量",
JSONObject("""
{
"type": "object",
"properties": {},
"required": []
}
"""),
object : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
val result = adjustVolume(context, AudioManager.ADJUST_RAISE)
return "{\"result\": \"增加音量成功!\"}"
}
}
)
Log.d(TAG, "已注册音量调节功能")
}
/**
* 注册减少音量函数
*/
private fun registerReduceVolumeFunction(client: MCPClient) {
client.registerLocalFunction(
"reduce_volume",
"减少音量",
JSONObject("""
{
"type": "object",
"properties": {},
"required": []
}
"""),
object : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
val result = adjustVolume(context, AudioManager.ADJUST_LOWER)
return "{\"result\": \"减少音量成功!\"}"
}
}
)
Log.d(TAG, "已注册音量调节功能")
}
/**
* 注册静音函数
*/
private fun registerMuteVolumeFunction(client: MCPClient) {
client.registerLocalFunction(
"mute_volume",
"静音设置",
JSONObject("""
{
"type": "object",
"properties": {},
"required": []
}
"""),
object : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
val result = setVolume(context, AudioManager.STREAM_MUSIC, 0)
return "{\"result\": \"减少音量成功!\"}"
}
}
)
Log.d(TAG, "已注册音量调节功能")
}
/**
* 控制媒体播放
* @param action 操作类型:"play", "pause", "previous", "next"
@ -1241,6 +1321,28 @@ class SystemFunctionHandler(private val context: Context) {
}
}
//设置音量
fun setVolume(context: Context, streamType: Int, volume: Int) {
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val maxVolume = audioManager.getStreamMaxVolume(streamType)
val safeVolume = volume.coerceIn(0, maxVolume)
audioManager.setStreamVolume(streamType, safeVolume, AudioManager.FLAG_SHOW_UI)
}
fun getCurrentVolume(context: Context, streamType: Int): Int {
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
return audioManager.getStreamVolume(streamType)
}
fun getMaxVolume(context: Context, streamType: Int): Int {
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
return audioManager.getStreamMaxVolume(streamType)
}
//增加音量/减少音量
fun adjustVolume(context: Context, direction: Int) {
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, direction, AudioManager.FLAG_SHOW_UI)
}
/**
* 操作结果数据类
*/

Loading…
Cancel
Save