diff --git a/assets/images/app_logo_gray_transparent.png b/assets/images/app_logo_gray_transparent.png new file mode 100644 index 00000000..03aea655 Binary files /dev/null and b/assets/images/app_logo_gray_transparent.png differ diff --git a/lib/features/v2/my/presentation/pages/help_feedback_page.dart b/lib/features/v2/my/presentation/pages/help_feedback_page.dart new file mode 100644 index 00000000..bb4a48be --- /dev/null +++ b/lib/features/v2/my/presentation/pages/help_feedback_page.dart @@ -0,0 +1,716 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; +import 'package:maibu_satabot_v2/core/theme/AppTheme.dart'; + +/// 帮助与反馈页面 +class HelpFeedbackPage extends StatefulWidget { + const HelpFeedbackPage({super.key}); + + @override + State createState() => _HelpFeedbackPageState(); +} + +class _HelpFeedbackPageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + final TextEditingController _feedbackController = TextEditingController(); + final TextEditingController _contactController = TextEditingController(); + int _selectedFeedbackType = 0; + List _selectedImages = []; + + static const _feedbackTypes = [ + {'icon': Icons.lightbulb_outline, 'label': '功能建议'}, + {'icon': Icons.bug_report_outlined, 'label': 'Bug报告'}, + {'icon': Icons.sentiment_dissatisfied_outlined, 'label': '体验问题'}, + {'icon': Icons.more_horiz, 'label': '其他'}, + ]; + + static const _faqList = [ + { + 'category': '设备绑定', + 'questions': [ + { + 'q': '扫码绑定失败怎么办?', + 'a': '请检查设备是否已开机并处于待绑定状态,确保手机蓝牙已开启且网络正常。如仍无法绑定,请尝试重启设备后重新扫码。' + }, + { + 'q': '搜不到设备蓝牙信号?', + 'a': '请确认设备已开机且蓝牙指示灯正常闪烁。尝试靠近设备(1米以内)后重新搜索。如多个设备同时绑定,请逐一操作。' + }, + ] + }, + { + 'category': '路径规划', + 'questions': [ + { + 'q': '无法生成路径?', + 'a': '请确保已绘制完整的作业区域(至少3个打点),并检查GPS信号是否良好。如果使用自动规划模式,请确认区域边界没有自相交。' + }, + { + 'q': '地图不显示或定位偏移?', + 'a': '请检查手机GPS是否已开启,并授权App位置权限。在室内或信号较弱的区域可能出现定位偏移,建议到开阔区域使用。' + }, + ] + }, + { + 'category': '远程控制', + 'questions': [ + { + 'q': '远程视频卡顿/连接超时?', + 'a': '请检查手机和设备的网络连接是否稳定。建议在4G/Wi-Fi信号良好的环境下使用远程控制功能。如持续卡顿,可尝试降低视频分辨率。' + }, + { + 'q': '急停功能怎么使用?', + 'a': '在远程控制页面,点击屏幕中央红色急停按钮即可立即停止设备运行。急停后需手动解除才能恢复操作。' + }, + ] + }, + { + 'category': '工单系统', + 'questions': [ + { + 'q': '如何提交工单?', + 'a': '在「工单」页面点击右下角「新建工单」按钮,选择工单类型,填写问题描述并上传相关图片后提交即可。' + }, + { + 'q': '工单状态怎么看?', + 'a': '在「工单」页面可查看所有已提交的工单及其当前状态(待处理/处理中/已完成)。点击具体工单可查看详情和回复。' + }, + ] + }, + { + 'category': '账号问题', + 'questions': [ + { + 'q': '忘记密码怎么办?', + 'a': '在登录页面点击「忘记密码」,通过注册手机号验证后即可重置密码。' + }, + { + 'q': '换设备登录需要重新绑定?', + 'a': '更换手机登录同一账号后,设备绑定关系不会丢失。但需要在新设备上重新搜索和连接设备。' + }, + ] + }, + ]; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + } + + @override + void dispose() { + _tabController.dispose(); + _feedbackController.dispose(); + _contactController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnnotatedRegion( + value: const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: Brightness.light, + ), + child: Scaffold( + backgroundColor: context.appColors.pageBackground, + appBar: AppBar( + elevation: 0, + leading: IconButton( + icon: Icon(Icons.arrow_back, color: context.appColors.textPrimary), + onPressed: () => Navigator.pop(context), + ), + title: Text( + AppLocalizations.of(context).translate('my_v2.help_feedback'), + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: context.appColors.textPrimary, + ), + ), + centerTitle: true, + bottom: TabBar( + controller: _tabController, + labelColor: const Color(0xFF165DFF), + unselectedLabelColor: context.appColors.textTertiary, + indicatorColor: const Color(0xFF165DFF), + indicatorSize: TabBarIndicatorSize.label, + tabs: const [ + Tab(text: '常见问题'), + Tab(text: '意见反馈'), + ], + ), + ), + body: TabBarView( + controller: _tabController, + children: [ + _buildFAQTab(), + _buildFeedbackTab(), + ], + ), + ), + ); + } + + // ==================== 常见问题 ==================== + + Widget _buildFAQTab() { + return ListView( + padding: const EdgeInsets.all(16), + children: [ + ..._faqList.map((category) => _buildFAQCategory(category)), + const SizedBox(height: 20), + _buildContactSection(), + ], + ); + } + + Widget _buildFAQCategory(Map categoryData) { + final questions = categoryData['questions'] as List; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 8, bottom: 8), + child: Text( + categoryData['category'] as String, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: context.appColors.textPrimary, + ), + ), + ), + Container( + decoration: BoxDecoration( + color: context.appColors.cardBackground, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: context.appColors.cardShadow, + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: questions.asMap().entries.map((entry) { + final index = entry.key; + final item = entry.value as Map; + final isLast = index == questions.length - 1; + return _buildFAQItem( + item['q'] as String, + item['a'] as String, + isLast, + ); + }).toList(), + ), + ), + const SizedBox(height: 16), + ], + ); + } + + Widget _buildFAQItem(String question, String answer, bool isLast) { + return Column( + children: [ + ExpansionTile( + tilePadding: const EdgeInsets.symmetric(horizontal: 16), + childrenPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + shape: const Border(), + collapsedShape: const Border(), + title: Text( + question, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: context.appColors.textPrimary, + ), + ), + children: [ + Text( + answer, + style: TextStyle( + fontSize: 13, + height: 1.6, + color: context.appColors.textSecondary, + ), + ), + ], + ), + if (!isLast) + Padding( + padding: const EdgeInsets.only(left: 48), + child: Divider( + height: 1, + thickness: 0.5, + color: context.appColors.divider, + ), + ), + ], + ); + } + + // ==================== 联系客服 ==================== + + Widget _buildContactSection() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: context.appColors.cardBackground, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: context.appColors.cardShadow, + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '联系客服', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: context.appColors.textPrimary, + ), + ), + const SizedBox(height: 16), + _buildContactItem( + Icons.phone_outlined, + '客服热线', + '400-000-0000', + onTap: () => _showPhoneDialog('400-000-0000'), + ), + const Divider(height: 24), + _buildContactItem( + Icons.qr_code_2, + '企业微信', + '扫码添加客服微信', + onTap: () => debugPrint('显示企业微信二维码'), + ), + ], + ), + ); + } + + Widget _buildContactItem( + IconData icon, + String title, + String subtitle, { + VoidCallback? onTap, + }) { + return GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: Row( + children: [ + Icon(icon, size: 22, color: context.appColors.textSecondary), + const SizedBox(width: 12), + Expanded( + child: Text( + title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: context.appColors.textPrimary, + ), + ), + ), + Text( + subtitle, + style: TextStyle( + fontSize: 13, + color: context.appColors.textTertiary, + ), + ), + const SizedBox(width: 6), + Icon( + Icons.chevron_right, + color: context.appColors.textTertiary, + size: 20, + ), + ], + ), + ); + } + + // ==================== 意见反馈 ==================== + + Widget _buildFeedbackTab() { + return ListView( + padding: const EdgeInsets.all(16), + children: [ + // 反馈类型 + _buildSectionCard( + title: '反馈类型', + child: Wrap( + spacing: 8, + runSpacing: 8, + children: _feedbackTypes.asMap().entries.map((entry) { + final index = entry.key; + final type = entry.value; + final isSelected = _selectedFeedbackType == index; + return GestureDetector( + onTap: () => setState(() => _selectedFeedbackType = index), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 8, + ), + decoration: BoxDecoration( + color: isSelected + ? const Color(0xFF165DFF).withOpacity(0.1) + : context.appColors.fillBackground, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: isSelected + ? const Color(0xFF165DFF) + : Colors.transparent, + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + type['icon'] as IconData, + size: 16, + color: isSelected + ? const Color(0xFF165DFF) + : context.appColors.textSecondary, + ), + const SizedBox(width: 6), + Text( + type['label'] as String, + style: TextStyle( + fontSize: 13, + color: isSelected + ? const Color(0xFF165DFF) + : context.appColors.textSecondary, + fontWeight: + isSelected ? FontWeight.w600 : FontWeight.normal, + ), + ), + ], + ), + ), + ); + }).toList(), + ), + ), + const SizedBox(height: 16), + + // 问题描述 + _buildSectionCard( + title: '问题描述', + child: TextField( + controller: _feedbackController, + maxLines: 5, + maxLength: 500, + decoration: InputDecoration( + hintText: '请详细描述您遇到的问题或建议...', + hintStyle: TextStyle( + fontSize: 14, + color: context.appColors.textTertiary, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: context.appColors.divider), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: context.appColors.divider), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide( + color: Color(0xFF165DFF), + width: 1.5, + ), + ), + filled: true, + fillColor: context.appColors.fillBackground, + contentPadding: const EdgeInsets.all(12), + ), + ), + ), + const SizedBox(height: 16), + + // 添加截图 + _buildSectionCard( + title: '添加截图 (${_selectedImages.length}/6)', + child: Wrap( + spacing: 8, + runSpacing: 8, + children: [ + ..._selectedImages.asMap().entries.map((entry) { + return _buildImageItem(entry.key, entry.value); + }), + if (_selectedImages.length < 6) _buildAddImageButton(), + ], + ), + ), + const SizedBox(height: 16), + + // 联系方式 + _buildSectionCard( + title: '联系方式(选填)', + child: TextField( + controller: _contactController, + decoration: InputDecoration( + hintText: '手机号/微信号,方便我们回访', + hintStyle: TextStyle( + fontSize: 14, + color: context.appColors.textTertiary, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: context.appColors.divider), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: context.appColors.divider), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide( + color: Color(0xFF165DFF), + width: 1.5, + ), + ), + filled: true, + fillColor: context.appColors.fillBackground, + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 14, + ), + prefixIcon: const Icon( + Icons.phone_outlined, + size: 20, + color: Color(0xFF86909C), + ), + ), + ), + ), + const SizedBox(height: 24), + + // 提交按钮 + SizedBox( + width: double.infinity, + height: 48, + child: ElevatedButton( + onPressed: _submitFeedback, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF165DFF), + foregroundColor: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text( + '提交反馈', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + ), + ), + const SizedBox(height: 32), + ], + ); + } + + Widget _buildSectionCard({ + required String title, + required Widget child, + }) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: context.appColors.cardBackground, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: context.appColors.cardShadow, + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: context.appColors.textPrimary, + ), + ), + const SizedBox(height: 12), + child, + ], + ), + ); + } + + Widget _buildImageItem(int index, String path) { + return Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.file( + File(path), + width: 80, + height: 80, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => Container( + width: 80, + height: 80, + color: context.appColors.fillBackground, + child: Icon( + Icons.broken_image_outlined, + color: context.appColors.textTertiary, + ), + ), + ), + ), + Positioned( + top: 2, + right: 2, + child: GestureDetector( + onTap: () => setState(() => _selectedImages.removeAt(index)), + child: Container( + padding: const EdgeInsets.all(2), + decoration: const BoxDecoration( + color: Colors.black54, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.close, + size: 14, + color: Colors.white, + ), + ), + ), + ), + ], + ); + } + + Widget _buildAddImageButton() { + return GestureDetector( + onTap: _pickImage, + child: Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: context.appColors.fillBackground, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: context.appColors.divider, + width: 1, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.add_photo_alternate_outlined, + size: 28, + color: context.appColors.textTertiary, + ), + const SizedBox(height: 4), + Text( + '添加截图', + style: TextStyle( + fontSize: 11, + color: context.appColors.textTertiary, + ), + ), + ], + ), + ), + ); + } + + // ==================== 交互方法 ==================== + + void _showPhoneDialog(String phoneNumber) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('客服热线'), + content: SelectableText( + phoneNumber, + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('关闭'), + ), + TextButton( + onPressed: () { + Clipboard.setData(ClipboardData(text: phoneNumber)); + Navigator.pop(context); + ScaffoldMessenger.of(this.context).showSnackBar( + const SnackBar(content: Text('号码已复制')), + ); + }, + child: const Text('复制号码'), + ), + ], + ), + ); + } + + void _pickImage() { + // 占位:接入 image_picker 后实现 + debugPrint('选择图片'); + setState(() { + _selectedImages.add('placeholder_${_selectedImages.length}'); + }); + } + + void _submitFeedback() { + if (_feedbackController.text.trim().isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('请输入问题描述'), + backgroundColor: Color(0xFFF53F3F), + ), + ); + return; + } + + // TODO: 调用后端接口提交反馈 + debugPrint('提交反馈: type=${_feedbackTypes[_selectedFeedbackType]['label']}, ' + 'content=${_feedbackController.text}, ' + 'images=${_selectedImages.length}, ' + 'contact=${_contactController.text}'); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('反馈已提交,感谢您的支持!'), + backgroundColor: context.appColors.success, + ), + ); + + // 重置表单 + _feedbackController.clear(); + _contactController.clear(); + setState(() { + _selectedFeedbackType = 0; + _selectedImages.clear(); + }); + } +} diff --git a/lib/features/v2/my/presentation/pages/my_page.dart b/lib/features/v2/my/presentation/pages/my_page.dart index ffbcfd91..696f4c0d 100644 --- a/lib/features/v2/my/presentation/pages/my_page.dart +++ b/lib/features/v2/my/presentation/pages/my_page.dart @@ -19,6 +19,7 @@ import 'package:maibu_satabot_v2/features/v2/my/presentation/widgets/quick_actio import 'package:maibu_satabot_v2/features/v2/my/presentation/widgets/menu_item_card.dart'; import 'package:maibu_satabot_v2/features/v2/my/presentation/pages/system_settings_page.dart'; import 'package:maibu_satabot_v2/features/v2/my/presentation/pages/profile_detail_page.dart'; +import 'package:maibu_satabot_v2/features/v2/my/presentation/pages/help_feedback_page.dart'; /// 我的页面(100% 还原设计稿) class MyPage extends StatelessWidget { @@ -75,90 +76,91 @@ class _MyPageContent extends StatelessWidget { return SingleChildScrollView( child: Column( - children: [ - // 用户信息卡片(延伸到状态栏) - UserProfileCard( - name: userName, - role: userRole, - avatar: user?.avatar, - onTap: () { - // 跳转到个人信息详情页 - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const ProfileDetailPage(), - ), - ); - }, - ), - // 快捷入口卡片(负margin向上重叠) - Transform.translate( - offset: const Offset(0, -15), - child: QuickActionCard( - onMessageTap: () { - debugPrint('我的消息'); - }, - onFavoriteTap: () { - debugPrint('我的收藏'); - }, - onWorkOrderTap: () { - debugPrint('我的工单'); - }, - ), - ), - // 菜单列表(白色卡片) - Container( - margin: const EdgeInsets.only( - top: 15, - left: 16, - right: 16, - bottom: 16, - ), - decoration: BoxDecoration( - color: context.appColors.cardBackground, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: context.appColors.cardShadow, - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - padding: EdgeInsets.zero, - // 末尾追加退出登录,占据“关于我们”原来的位置 - itemCount: state.menuItems.length + 1, - itemBuilder: (context, index) { - // 退出登录(列表最后一项,在可滚动区域内避免底部溢出) - if (index == state.menuItems.length) { - return _buildLogoutButton(context); - } - final item = state.menuItems[index]; - // 🔥 夜间模式开关值以全局 ThemeCubit 状态为准 - final displayItem = item.id == 'night_mode' - ? item.copyWith(switchValue: isDarkMode) - : item; - return MenuItemCard( - item: displayItem, - onTap: () => _handleMenuItemTap(context, cubit, item), - onSwitchChanged: item.hasSwitch - ? (value) { - // 更新菜单 UI 状态 - cubit.toggleNightMode(value); - // 🔥 真正切换全局主题 - context - .read() - .toggleDarkMode(value); - } - : null, + children: [ + // 用户信息卡片(延伸到状态栏) + UserProfileCard( + name: userName, + role: userRole, + avatar: user?.avatar, + onTap: () { + // 跳转到个人信息详情页 + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const ProfileDetailPage(), + ), ); }, ), - ), - ], + // 快捷入口卡片(负margin向上重叠) + Transform.translate( + offset: const Offset(0, -15), + child: QuickActionCard( + onMessageTap: () { + debugPrint('我的消息'); + }, + onFavoriteTap: () { + debugPrint('我的收藏'); + }, + onWorkOrderTap: () { + debugPrint('我的工单'); + }, + ), + ), + // 菜单列表(白色卡片) + Container( + margin: const EdgeInsets.only( + top: 15, + left: 16, + right: 16, + bottom: 16, + ), + decoration: BoxDecoration( + color: context.appColors.cardBackground, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: context.appColors.cardShadow, + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + // 末尾追加退出登录,占据“关于我们”原来的位置 + itemCount: state.menuItems.length + 1, + itemBuilder: (context, index) { + // 退出登录(列表最后一项,在可滚动区域内避免底部溢出) + if (index == state.menuItems.length) { + return _buildLogoutButton(context); + } + final item = state.menuItems[index]; + // 🔥 夜间模式开关值以全局 ThemeCubit 状态为准 + final displayItem = item.id == 'night_mode' + ? item.copyWith(switchValue: isDarkMode) + : item; + return MenuItemCard( + item: displayItem, + onTap: () => + _handleMenuItemTap(context, cubit, item), + onSwitchChanged: item.hasSwitch + ? (value) { + // 更新菜单 UI 状态 + cubit.toggleNightMode(value); + // 🔥 真正切换全局主题 + context.read().toggleDarkMode( + value, + ); + } + : null, + ); + }, + ), + ), + ], ), ); } @@ -196,6 +198,13 @@ class _MyPageContent extends StatelessWidget { MaterialPageRoute(builder: (context) => const SystemSettingsPage()), ); break; + case 'help_feedback': + // 跳转到帮助与反馈页面 + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const HelpFeedbackPage()), + ); + break; default: debugPrint('点击菜单: ${item.title}'); // TODO: 根据 ID 跳转到对应页面