帮助与反馈页面开发

This commit is contained in:
2026-10-10 08:47:30 +08:00
parent 208147cdf2
commit 167229371f
3 changed files with 806 additions and 81 deletions

View File

@@ -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<HelpFeedbackPage> createState() => _HelpFeedbackPageState();
}
class _HelpFeedbackPageState extends State<HelpFeedbackPage>
with SingleTickerProviderStateMixin {
late TabController _tabController;
final TextEditingController _feedbackController = TextEditingController();
final TextEditingController _contactController = TextEditingController();
int _selectedFeedbackType = 0;
List<String> _selectedImages = [];
static const _feedbackTypes = [
{'icon': Icons.lightbulb_outline, 'label': '功能建议'},
{'icon': Icons.bug_report_outlined, 'label': 'Bug报告'},
{'icon': Icons.sentiment_dissatisfied_outlined, 'label': '体验问题'},
{'icon': Icons.more_horiz, 'label': '其他'},
];
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<SystemUiOverlayStyle>(
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<String, dynamic> 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<String, dynamic>;
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();
});
}
}

View File

@@ -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<ThemeCubit>()
.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<ThemeCubit>().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 跳转到对应页面