Files
wushu/lib/views/profile/profile_page.dart
2026-04-02 17:31:53 +08:00

814 lines
26 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// 时间: 2025.03.21
/// 功能: 个人页面(类似朋友圈布局)
/// 介绍: 展示用户头像、个性签名、昵称、统计信息和设置列表,支持左右滑动切换页面
/// 最新变化: 重新设计布局,实现朋友圈风格的个人页面,添加底部内边距适配液态玻璃导航栏
import 'dart:io' as io;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:get/get.dart';
import '../../constants/app_constants.dart';
import '../../config/app_config.dart';
import 'history_page.dart';
import 'per_card.dart';
import 'settings/app_fun.dart';
import 'settings/user-plan.dart';
import 'settings/offline-data.dart';
import 'settings/privacy.dart';
import 'settings/learn-us.dart';
import 'app-info.dart';
import 'level/poetry.dart';
import 'guide/permission.dart';
import 'guide/app-data.dart';
import 'theme/app-diy.dart';
import 'expand/vote.dart';
import 'expand/manu-script.dart';
import 'components/bug_list_page.dart';
import 'components/pop-menu.dart';
import 'components/entire_page.dart';
import '../../services/get/profile_controller.dart';
import '../../services/get/theme_controller.dart';
class ProfilePage extends StatelessWidget {
const ProfilePage({super.key});
@override
Widget build(BuildContext context) {
final controller = Get.put(ProfileController());
final pageController = PageController(initialPage: 1);
final themeController = Get.find<ThemeController>();
return Obx(() {
final isDark = themeController.isDarkMode;
return GetBuilder<ProfileController>(
builder: (controller) {
return Scaffold(
backgroundColor: isDark
? const Color(0xFF1A1A1A)
: const Color(0xFFF5F5F5),
appBar: _buildAppBar(controller, isDark),
body: GestureDetector(
behavior: HitTestBehavior.opaque,
onVerticalDragStart: (details) {
controller.startY.value = details.globalPosition.dy;
},
onVerticalDragEnd: (details) {
double currentY = details.globalPosition.dy;
double deltaY = currentY - controller.startY.value;
if (deltaY > 50) {
if (!controller.isCardExpanded.value) {
controller.isCardExpanded.value = true;
}
} else if (deltaY < -50) {
if (controller.isCardExpanded.value) {
controller.isCardExpanded.value = false;
}
}
},
child: Column(
children: [
_buildProfileHeader(controller, pageController),
Expanded(
child: PageView(
controller: pageController,
onPageChanged: controller.onPageChanged,
children: [
_buildPage1(controller, isDark),
_buildPage2(controller, isDark),
_buildPage3(controller, isDark),
],
),
),
],
),
),
);
},
);
});
}
PreferredSizeWidget _buildAppBar(ProfileController controller, bool isDark) {
return AppBar(
title: Text(
'个人',
style: TextStyle(
color: AppConstants.primaryColor,
fontWeight: FontWeight.bold,
),
),
backgroundColor: isDark ? const Color(0xFF2A2A2A) : Colors.white,
elevation: 0,
centerTitle: true,
actions: [
IconButton(
icon: Icon(Icons.more_horiz, color: AppConstants.primaryColor),
onPressed: () => _showMoreOptions(controller),
),
],
);
}
Widget _buildProfileHeader(
ProfileController controller,
PageController pageController,
) {
// === 个人信息头部:可收起/张开的个人卡片 ===
return Obx(
() => PersonalCard(
userData: controller.userData,
isExpanded: controller.isCardExpanded.value,
onExpandChanged: (value) {
controller.isCardExpanded.value = value;
},
currentPage: controller.currentPage.value,
onPageChanged: (page) {
// 添加 hasClients 检查,避免 dispose 后调用导致黑屏
if (pageController.hasClients) {
pageController.animateToPage(
page,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
},
onAvatarTap: controller.changeAvatar,
),
);
}
Widget _buildPage1(ProfileController controller, bool isDark) {
return SingleChildScrollView(
// 添加底部内边距,让内容延伸到导航栏下方,实现玻璃效果
padding: EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: AppConfig.liquidGlassTotalHeight + 16,
),
child: Column(
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF2A2A2A) : Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: isDark
? Colors.black.withValues(alpha: 0.3)
: Colors.black.withValues(alpha: 0.08),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.analytics_outlined,
color: AppConstants.primaryColor,
size: 20,
),
const SizedBox(width: 8),
Text(
'诗词挑战',
style: TextStyle(
color: AppConstants.primaryColor,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
Obx(() {
if (!controller.isStatsHidden.value) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildInteractionItem(
'今日答题',
'${controller.todayQuestions.value}',
isDark,
),
_buildInteractionItem(
'本周答题',
'${controller.weekQuestions.value}',
isDark,
),
_buildInteractionItem(
'答对次数',
'${controller.correctAnswers.value}',
isDark,
),
],
);
} else {
return Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Text(
'数据已隐藏',
style: TextStyle(
color: isDark ? Colors.grey[500] : Colors.grey[500],
fontSize: 14,
),
),
),
);
}
}),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
Get.to(const PoetryLevelPage());
},
style: ElevatedButton.styleFrom(
backgroundColor: AppConstants.primaryColor,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text(
'开始诗词答题',
style: TextStyle(fontSize: 16, color: Colors.white),
),
),
),
],
),
),
const SizedBox(height: 16),
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF2A2A2A) : Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: isDark
? Colors.black.withValues(alpha: 0.3)
: Colors.black.withValues(alpha: 0.08),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.person_outline,
color: AppConstants.primaryColor,
size: 20,
),
const SizedBox(width: 8),
Text(
'统计',
style: TextStyle(
color: AppConstants.primaryColor,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
IconButton(
icon: Icon(
controller.isStatsHidden.value
? Icons.visibility_off_outlined
: Icons.visibility_outlined,
color: isDark ? Colors.grey[400] : Colors.grey[600],
size: 20,
),
onPressed: controller.toggleStatsHidden,
tooltip: controller.isStatsHidden.value ? '显示数据' : '隐藏数据',
),
],
),
const SizedBox(height: 16),
if (!controller.isStatsHidden.value) ...[
_buildInfoItem(
'今日浏览',
'${controller.todayViews.value}',
isDark,
),
_buildInfoItem(
'今日点赞',
'${controller.todayLikes.value}',
isDark,
),
_buildInfoItem(
'今日答题',
'${controller.todayQuestions.value}',
isDark,
),
_buildInfoItem(
'本周浏览',
'${controller.weekViews.value}',
isDark,
),
_buildInfoItem('数据', controller.dataSize.value, isDark),
_buildInfoItem(
'笔记',
'${controller.noteCount.value}',
isDark,
),
_buildInfoItem('已用', '${controller.useDays.value}', isDark),
] else
Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Text(
'数据已隐藏',
style: TextStyle(
color: isDark ? Colors.grey[500] : Colors.grey[500],
fontSize: 14,
),
),
),
),
],
),
),
],
),
);
}
Widget _buildPage2(ProfileController controller, bool isDark) {
return ListView(
// 添加底部内边距,让内容延伸到导航栏下方,实现玻璃效果
padding: EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: AppConfig.liquidGlassTotalHeight + 16,
),
children: [
_buildSettingsGroup('软件设置', [
_buildSettingsItem(
'功能设置',
Icons.verified_user,
() => Get.to(const AppFunSettingsPage()),
isDark,
),
_buildSettingsItem(
'离线使用',
Icons.volunteer_activism,
() => Get.to(const OfflineDataPage()),
isDark,
),
_buildSettingsItem(
'历史记录',
Icons.history,
() => Get.to(const HistoryPage()),
isDark,
),
_buildSettingsItem(
'主题风格',
Icons.palette,
() => Get.to(const AppDiyPage()),
isDark,
),
], isDark),
const SizedBox(height: 16),
_buildSettingsGroup('隐私设置', [
_buildSettingsItem(
'权限管理',
Icons.block,
() => Get.to(const PermissionPage()),
isDark,
),
_buildSettingsItem(
'应用数据',
Icons.security,
() => Get.to(const AppDataPage()),
isDark,
),
_buildSettingsItem(
'软件协议',
Icons.description,
() => Get.to(const PrivacyPage()),
isDark,
),
], isDark),
const SizedBox(height: 16),
_buildSettingsGroup('软件信息', [
_buildSettingsItem(
'应用信息',
Icons.phone_android,
() => Get.to(const AppInfoPage()),
isDark,
),
_buildSettingsItem(
'了解我们',
Icons.info,
() => Get.to(const LearnUsPage()),
isDark,
),
_buildSettingsItem(
'已知bug',
Icons.cleaning_services,
() => showBugListBottomSheet(Get.context!),
isDark,
),
_buildScreenWakeItem(controller, isDark),
], isDark),
],
);
}
Widget _buildPage3(ProfileController controller, bool isDark) {
return ListView(
// 添加底部内边距,让内容延伸到导航栏下方,实现玻璃效果
padding: EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: AppConfig.liquidGlassTotalHeight + 16,
),
children: [
_buildSettingsGroup('用户体验计划(限免)', [
_buildSettingsItem(
'加入体验',
Icons.edit,
() => Get.to(const UserPlanPage()),
isDark,
),
_buildSettingsItem(
'参与投票',
Icons.how_to_vote,
() => Get.to(const VotePage()),
isDark,
),
_buildSettingsItem(
'查看全站统计',
Icons.history,
() => Get.to(const EntirePage()),
isDark,
),
_buildSettingsItem(
'开发计划',
Icons.analytics,
() => controller.showSnackBar('首个上线版本暂无计划,待收集整理用户建议'),
isDark,
),
_buildSettingsItem(
'去投稿',
Icons.edit_note,
() => Get.to(const ManuscriptPage()),
isDark,
),
], isDark),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF2A2A2A) : Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: isDark
? Colors.black.withValues(alpha: 0.3)
: Colors.black.withValues(alpha: 0.08),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.info, color: AppConstants.primaryColor, size: 20),
const SizedBox(width: 8),
Text(
'关于应用',
style: TextStyle(
color: AppConstants.primaryColor,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
Text(
AppConstants.appName,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: isDark ? Colors.white : Colors.black,
),
),
const SizedBox(height: 8),
Text(
'版本 ${AppConstants.appVersion}',
style: TextStyle(
fontSize: 14,
color: isDark ? Colors.grey[400] : Colors.grey,
),
),
const SizedBox(height: 16),
Text(
'基于Flutter开发的现代化诗词应用致力于为用户提供优质的诗词阅读和创作体验。',
style: TextStyle(
fontSize: 14,
color: isDark ? Colors.grey[300] : Colors.black87,
height: 1.5,
),
textAlign: TextAlign.center,
),
],
),
),
],
);
}
Widget _buildInfoItem(String label, String value, bool isDark) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(
fontSize: 14,
color: isDark ? Colors.grey[400] : const Color(0xFF9E9E9E),
),
),
Text(
value,
style: TextStyle(
fontSize: 14,
color: isDark ? Colors.white : Colors.black87,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
Widget _buildSettingsGroup(String title, List<Widget> items, bool isDark) {
return Container(
decoration: BoxDecoration(
color: isDark ? const Color(0xFF2A2A2A) : Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: isDark
? Colors.black.withValues(alpha: 0.3)
: Colors.black.withValues(alpha: 0.08),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Icon(
Icons.settings_outlined,
color: AppConstants.primaryColor,
size: 20,
),
const SizedBox(width: 8),
Text(
title,
style: TextStyle(
color: AppConstants.primaryColor,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
),
Divider(height: 1, color: isDark ? Colors.grey[800] : null),
...items,
],
),
);
}
Widget _buildSettingsItem(
String title,
IconData icon,
VoidCallback onTap,
bool isDark,
) {
return ListTile(
leading: Icon(icon, color: AppConstants.primaryColor, size: 20),
title: Text(
title,
style: TextStyle(
fontSize: 14,
color: isDark ? Colors.white : Colors.black87,
),
),
trailing: Icon(
Icons.chevron_right,
color: isDark ? Colors.grey[600] : Colors.grey,
size: 20,
),
onTap: () {
HapticFeedback.lightImpact();
onTap();
},
);
}
Widget _buildScreenWakeItem(ProfileController controller, bool isDark) {
if (kIsWeb) {
return const SizedBox.shrink();
}
return ListTile(
leading: Icon(
Icons.screen_lock_rotation,
color: AppConstants.primaryColor,
size: 20,
),
title: Text(
'屏幕常亮',
style: TextStyle(
fontSize: 14,
color: isDark ? Colors.white : Colors.black87,
),
),
trailing: Obx(
() => Switch(
value: controller.isScreenWakeEnabled.value,
onChanged: (value) async {
final originalValue = controller.isScreenWakeEnabled.value;
controller.isScreenWakeEnabled.value = value;
if (value) {
await Get.dialog(
AlertDialog(
backgroundColor: isDark
? const Color(0xFF2A2A2A)
: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
title: Text(
'⚠️ 屏幕常亮提示',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppConstants.primaryColor,
),
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
_buildTipItem('OLED屏幕长时间显示相同内容可能会造成不可逆老化', isDark),
const SizedBox(height: 6),
_buildTipItem('开启常亮后,配合自动刷新内容的设置,实现常亮自动加载诗句', isDark),
const SizedBox(height: 6),
_buildTipItem('挂起常亮后,需保持该软件在屏幕中显示,记得关闭常亮哦!', isDark),
],
),
actions: [
TextButton(
onPressed: () {
controller.isScreenWakeEnabled.value = originalValue;
Get.back(result: false);
},
style: TextButton.styleFrom(
foregroundColor: isDark
? Colors.grey[400]
: Colors.grey[600],
),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () {
Get.back(result: true);
},
style: ElevatedButton.styleFrom(
backgroundColor: AppConstants.primaryColor,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text('确定开启'),
),
],
),
).then((result) async {
if (result == true) {
await controller.toggleScreenWake(value);
} else {
controller.isScreenWakeEnabled.value = originalValue;
}
});
} else {
await controller.toggleScreenWake(value);
}
},
activeColor: AppConstants.primaryColor,
),
),
);
}
Widget _buildInteractionItem(String label, String value, bool isDark) {
return Expanded(
child: Column(
children: [
Text(
value,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppConstants.primaryColor,
),
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: 12,
color: isDark ? Colors.grey[400] : Colors.grey,
),
),
],
),
);
}
void _showMoreOptions(ProfileController controller) {
PopMenu.show(
Get.context!,
onRefresh: () => controller.refreshData(),
onEdit: () => controller.showSnackBar('编辑资料'),
onScanQr: () => controller.showSnackBar('了解软件功能'),
onDarkMode: () => controller.showSnackBar('夜间模式'),
onSettings: () {
Future.delayed(const Duration(milliseconds: 100), () {
final pageController = PageController(initialPage: 1);
if (pageController.hasClients) {
pageController.animateToPage(
1,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
});
},
);
}
Widget _buildTipItem(String text, bool isDark) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'',
style: TextStyle(
fontSize: 14,
color: isDark ? Colors.grey[300] : Colors.black87,
),
),
Expanded(
child: Text(
text,
style: TextStyle(
fontSize: 14,
color: isDark ? Colors.grey[300] : Colors.black87,
),
),
),
],
);
}
}