feat(frontend): 实现我的页面其他设置4项功能

- 消息通知: 添加开关控制,状态持久化到SharedPreferences
- 深色模式: 添加开关控制,状态持久化到SharedPreferences
- 帮助中心: 新建页面,包含常见问题FAQ和联系方式
- 关于我们: 新建页面,包含应用简介、功能特点、联系方式和法律条款

新增文件:
- settings_providers.dart: 设置状态管理
- help_center_page.dart: 帮助中心页面
- about_page.dart: 关于我们页面

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-17 01:08:21 -08:00
parent 1e33ab178d
commit d5f3f3b868
8 changed files with 821 additions and 9 deletions

View File

@ -78,6 +78,8 @@ services:
KAFKA_BROKERS: kafka:29092
# JWT 配置 (与 auth-service 共享密钥以验证 token)
JWT_SECRET: ${JWT_SECRET:-your-jwt-secret-change-in-production}
# 2.0 内部服务调用
CONTRIBUTION_SERVICE_URL: http://contribution-service:3020
ports:
- "3021:3021"
healthcheck:

View File

@ -21,6 +21,8 @@ import '../../presentation/pages/c2c/c2c_publish_page.dart';
import '../../presentation/pages/c2c/c2c_order_detail_page.dart';
import '../../presentation/pages/profile/team_page.dart';
import '../../presentation/pages/profile/trading_records_page.dart';
import '../../presentation/pages/profile/help_center_page.dart';
import '../../presentation/pages/profile/about_page.dart';
import '../../presentation/widgets/main_shell.dart';
import '../../presentation/providers/user_providers.dart';
import 'routes.dart';
@ -155,6 +157,14 @@ final appRouterProvider = Provider<GoRouter>((ref) {
path: Routes.tradingRecords,
builder: (context, state) => const TradingRecordsPage(),
),
GoRoute(
path: Routes.helpCenter,
builder: (context, state) => const HelpCenterPage(),
),
GoRoute(
path: Routes.about,
builder: (context, state) => const AboutPage(),
),
ShellRoute(
builder: (context, state, child) => MainShell(child: child),
routes: [

View File

@ -23,4 +23,7 @@ class Routes {
static const String myTeam = '/my-team';
//
static const String tradingRecords = '/trading-records';
//
static const String helpCenter = '/help-center';
static const String about = '/about';
}

View File

@ -0,0 +1,437 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart';
import 'package:package_info_plus/package_info_plus.dart';
///
class AboutPage extends StatefulWidget {
const AboutPage({super.key});
@override
State<AboutPage> createState() => _AboutPageState();
}
class _AboutPageState extends State<AboutPage> {
static const Color _orange = Color(0xFFFF6B00);
static const Color _darkText = Color(0xFF1F2937);
static const Color _grayText = Color(0xFF6B7280);
static const Color _bgGray = Color(0xFFF3F4F6);
String _version = '';
String _buildNumber = '';
@override
void initState() {
super.initState();
_loadPackageInfo();
}
Future<void> _loadPackageInfo() async {
try {
final info = await PackageInfo.fromPlatform();
setState(() {
_version = info.version;
_buildNumber = info.buildNumber;
});
} catch (e) {
setState(() {
_version = '1.0.0';
_buildNumber = '1';
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: _bgGray,
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: _darkText, size: 20),
onPressed: () => context.pop(),
),
title: const Text(
'关于我们',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: _darkText,
),
),
centerTitle: true,
),
body: SingleChildScrollView(
child: Column(
children: [
const SizedBox(height: 32),
// Logo和应用名称
_buildAppHeader(),
const SizedBox(height: 32),
//
_buildIntroSection(),
const SizedBox(height: 16),
//
_buildFeaturesSection(),
const SizedBox(height: 16),
//
_buildContactSection(),
const SizedBox(height: 16),
//
_buildLegalSection(),
const SizedBox(height: 24),
//
_buildCopyright(),
const SizedBox(height: 24),
],
),
),
);
}
Widget _buildAppHeader() {
return Column(
children: [
//
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: _orange,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: _orange.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: const Icon(
Icons.eco,
color: Colors.white,
size: 48,
),
),
const SizedBox(height: 16),
//
const Text(
'股行',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: _darkText,
),
),
const SizedBox(height: 8),
//
Text(
'Version $_version${_buildNumber.isNotEmpty ? ' ($_buildNumber)' : ''}',
style: const TextStyle(
fontSize: 14,
color: _grayText,
),
),
],
);
}
Widget _buildIntroSection() {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle('应用简介'),
const SizedBox(height: 12),
Text(
'股行是一款创新的数字资产管理平台,致力于为用户提供便捷、安全的榴莲树认种和积分管理服务。'
'通过认种榴莲树,用户可以获得贡献值,并根据贡献值占比获得每日积分股分配。',
style: TextStyle(
fontSize: 14,
color: _grayText.withOpacity(0.9),
height: 1.6,
),
),
],
),
);
}
Widget _buildFeaturesSection() {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle('核心功能'),
const SizedBox(height: 16),
_buildFeatureItem(
icon: Icons.eco,
title: '榴莲认种',
description: '认种真实榴莲树,获得贡献值奖励',
),
const SizedBox(height: 12),
_buildFeatureItem(
icon: Icons.trending_up,
title: '每日收益',
description: '根据贡献值占比,每日自动分配积分股',
),
const SizedBox(height: 12),
_buildFeatureItem(
icon: Icons.swap_horiz,
title: '便捷交易',
description: '支持积分股卖出兑换,积分值自由转账',
),
const SizedBox(height: 12),
_buildFeatureItem(
icon: Icons.people,
title: '团队收益',
description: '邀请好友,获得团队贡献值奖励',
),
],
),
);
}
Widget _buildFeatureItem({
required IconData icon,
required String title,
required String description,
}) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: _orange.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: _orange, size: 22),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: _darkText,
),
),
const SizedBox(height: 2),
Text(
description,
style: TextStyle(
fontSize: 12,
color: _grayText.withOpacity(0.9),
),
),
],
),
),
],
);
}
Widget _buildContactSection() {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle('联系方式'),
const SizedBox(height: 16),
_buildContactItem(
icon: Icons.email_outlined,
label: '客服邮箱',
value: 'support@guhang.com',
onTap: () {
Clipboard.setData(const ClipboardData(text: 'support@guhang.com'));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('邮箱已复制'),
duration: Duration(seconds: 1),
),
);
},
),
const Divider(height: 24),
_buildContactItem(
icon: Icons.language,
label: '官方网站',
value: 'www.guhang.com',
onTap: () {
// TODO:
},
),
],
),
);
}
Widget _buildContactItem({
required IconData icon,
required String label,
required String value,
VoidCallback? onTap,
}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Icon(icon, size: 20, color: _orange),
const SizedBox(width: 12),
Text(
'$label: ',
style: const TextStyle(
fontSize: 14,
color: _grayText,
),
),
Expanded(
child: Text(
value,
style: const TextStyle(
fontSize: 14,
color: _darkText,
fontWeight: FontWeight.w500,
),
),
),
if (onTap != null)
const Icon(Icons.chevron_right, size: 20, color: _grayText),
],
),
),
);
}
Widget _buildLegalSection() {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle('法律条款'),
const SizedBox(height: 16),
_buildLegalItem(
title: '用户协议',
onTap: () {
// TODO:
},
),
const Divider(height: 24),
_buildLegalItem(
title: '隐私政策',
onTap: () {
// TODO:
},
),
],
),
);
}
Widget _buildLegalItem({
required String title,
required VoidCallback onTap,
}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Expanded(
child: Text(
title,
style: const TextStyle(
fontSize: 14,
color: _darkText,
),
),
),
const Icon(Icons.chevron_right, size: 20, color: _grayText),
],
),
),
);
}
Widget _buildSectionTitle(String title) {
return Row(
children: [
Container(
width: 4,
height: 16,
decoration: BoxDecoration(
color: _orange,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 8),
Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: _darkText,
),
),
],
);
}
Widget _buildCopyright() {
return Column(
children: [
Text(
'Copyright © ${DateTime.now().year} 股行',
style: const TextStyle(
fontSize: 12,
color: _grayText,
),
),
const SizedBox(height: 4),
const Text(
'All Rights Reserved',
style: TextStyle(
fontSize: 12,
color: _grayText,
),
),
],
);
}
}

View File

@ -0,0 +1,276 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
///
class HelpCenterPage extends StatelessWidget {
const HelpCenterPage({super.key});
static const Color _orange = Color(0xFFFF6B00);
static const Color _darkText = Color(0xFF1F2937);
static const Color _grayText = Color(0xFF6B7280);
static const Color _bgGray = Color(0xFFF3F4F6);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: _bgGray,
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: _darkText, size: 20),
onPressed: () => context.pop(),
),
title: const Text(
'帮助中心',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: _darkText,
),
),
centerTitle: true,
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
//
_buildSection('常见问题', [
_FAQItem(
question: '如何认种榴莲树?',
answer: '在首页点击"去认种"按钮,选择认种数量和支付方式,完成支付后即可认种成功。认种成功后,您将获得对应的贡献值。',
),
_FAQItem(
question: '贡献值是什么?',
answer: '贡献值是您在平台认种榴莲树后获得的一种权益凭证。贡献值越高您每日可获得的积分股分配越多。贡献值有效期为730天。',
),
_FAQItem(
question: '如何获得积分股?',
answer: '系统每日会根据您的贡献值占比,自动分配积分股到您的账户。积分股可用于兑换或交易。',
),
_FAQItem(
question: '积分值和积分股有什么区别?',
answer: '积分股是通过贡献值分配获得的,可用于卖出兑换。积分值是一种通用积分,可以转账给其他用户。两者用途不同,请注意区分。',
),
]),
const SizedBox(height: 16),
_buildSection('交易相关', [
_FAQItem(
question: '如何卖出积分股?',
answer: '进入"兑换"页面输入要卖出的积分股数量确认后即可完成卖出。卖出时会扣除10%进入积分股池。',
),
_FAQItem(
question: '卖出积分股为什么要扣除10%',
answer: '卖出积分股时10%会进入积分股池,用于系统生态建设和价值稳定。这是系统规则的一部分。',
),
_FAQItem(
question: '如何发送积分值给其他用户?',
answer: '进入"资产"页面,点击"发送"按钮,输入对方手机号和转账金额,确认后即可完成转账。注意:积分值转账不可撤销。',
),
]),
const SizedBox(height: 16),
_buildSection('账户安全', [
_FAQItem(
question: '如何修改登录密码?',
answer: '进入"我的"页面,点击"账户安全",可以修改登录密码。建议定期更换密码以确保账户安全。',
),
_FAQItem(
question: '忘记密码怎么办?',
answer: '在登录页面点击"忘记密码",通过手机号验证后可以重置密码。',
),
]),
const SizedBox(height: 16),
_buildSection('团队收益', [
_FAQItem(
question: '如何邀请好友?',
answer: '您的手机号就是您的邀请码。好友注册时填写您的手机号作为邀请人,即可建立引荐关系。',
),
_FAQItem(
question: '团队收益如何计算?',
answer: '当您引荐的好友认种榴莲树后,您将获得团队下级贡献值奖励。引荐的用户越多、认种数量越多,您的团队收益越高。',
),
]),
const SizedBox(height: 16),
//
_buildContactSection(context),
const SizedBox(height: 24),
],
),
),
);
}
Widget _buildSection(String title, List<_FAQItem> items) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Row(
children: [
Container(
width: 4,
height: 16,
decoration: BoxDecoration(
color: _orange,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 8),
Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: _darkText,
),
),
],
),
),
...items.asMap().entries.map((entry) {
final isLast = entry.key == items.length - 1;
return _buildFAQTile(entry.value, showDivider: !isLast);
}),
],
),
);
}
Widget _buildFAQTile(_FAQItem item, {bool showDivider = true}) {
return Theme(
data: ThemeData(
dividerColor: Colors.transparent,
),
child: Column(
children: [
ExpansionTile(
tilePadding: const EdgeInsets.symmetric(horizontal: 16),
childrenPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
title: Text(
item.question,
style: const TextStyle(
fontSize: 14,
color: _darkText,
),
),
iconColor: _orange,
collapsedIconColor: _grayText,
children: [
Text(
item.answer,
style: TextStyle(
fontSize: 13,
color: _grayText.withOpacity(0.9),
height: 1.5,
),
),
],
),
if (showDivider)
const Divider(height: 1, indent: 16, endIndent: 16),
],
),
);
}
Widget _buildContactSection(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 4,
height: 16,
decoration: BoxDecoration(
color: _orange,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 8),
const Text(
'联系我们',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: _darkText,
),
),
],
),
const SizedBox(height: 16),
Text(
'如果您有其他问题,可以通过以下方式联系我们:',
style: TextStyle(
fontSize: 13,
color: _grayText.withOpacity(0.9),
),
),
const SizedBox(height: 16),
_buildContactItem(
icon: Icons.email_outlined,
label: '客服邮箱',
value: 'support@guhang.com',
),
const SizedBox(height: 12),
_buildContactItem(
icon: Icons.access_time,
label: '服务时间',
value: '周一至周五 9:00-18:00',
),
],
),
);
}
Widget _buildContactItem({
required IconData icon,
required String label,
required String value,
}) {
return Row(
children: [
Icon(icon, size: 20, color: _orange),
const SizedBox(width: 12),
Text(
'$label: ',
style: const TextStyle(
fontSize: 13,
color: _grayText,
),
),
Text(
value,
style: const TextStyle(
fontSize: 13,
color: _darkText,
fontWeight: FontWeight.w500,
),
),
],
);
}
}
class _FAQItem {
final String question;
final String answer;
_FAQItem({required this.question, required this.answer});
}

View File

@ -1,10 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../core/router/routes.dart';
import '../../providers/user_providers.dart';
import '../../providers/profile_providers.dart';
import '../../providers/settings_providers.dart';
import '../../widgets/shimmer_loading.dart';
class ProfilePage extends ConsumerWidget {
@ -78,7 +78,7 @@ class ProfilePage extends ConsumerWidget {
const SizedBox(height: 16),
//
_buildOtherSettings(context),
_buildOtherSettings(context, ref),
const SizedBox(height: 24),
@ -463,7 +463,10 @@ class ProfilePage extends ConsumerWidget {
);
}
Widget _buildOtherSettings(BuildContext context) {
Widget _buildOtherSettings(BuildContext context, WidgetRef ref) {
final notificationsEnabled = ref.watch(notificationsEnabledProvider);
final darkModeEnabled = ref.watch(darkModeEnabledProvider);
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
@ -487,24 +490,28 @@ class ProfilePage extends ConsumerWidget {
_buildSwitchItem(
icon: Icons.notifications_outlined,
label: '消息通知',
value: true,
onChanged: (value) {},
value: notificationsEnabled,
onChanged: (value) {
ref.read(notificationsEnabledProvider.notifier).setEnabled(value);
},
),
_buildSwitchItem(
icon: Icons.dark_mode_outlined,
label: '深色模式',
value: false,
onChanged: (value) {},
value: darkModeEnabled,
onChanged: (value) {
ref.read(darkModeEnabledProvider.notifier).setEnabled(value);
},
),
_buildSettingItem(
icon: Icons.help_outline,
label: '帮助中心',
onTap: () {},
onTap: () => context.push(Routes.helpCenter),
),
_buildSettingItem(
icon: Icons.info_outline,
label: '关于我们',
onTap: () {},
onTap: () => context.push(Routes.about),
showDivider: false,
),
],

View File

@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
// ==================== ====================
const String _keyNotificationsEnabled = 'settings_notifications_enabled';
const String _keyDarkModeEnabled = 'settings_dark_mode_enabled';
// ==================== ====================
/// Provider
final notificationsEnabledProvider = StateNotifierProvider<NotificationsNotifier, bool>((ref) {
return NotificationsNotifier();
});
class NotificationsNotifier extends StateNotifier<bool> {
NotificationsNotifier() : super(true) {
_loadFromStorage();
}
Future<void> _loadFromStorage() async {
final prefs = await SharedPreferences.getInstance();
state = prefs.getBool(_keyNotificationsEnabled) ?? true;
}
Future<void> toggle() async {
state = !state;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_keyNotificationsEnabled, state);
}
Future<void> setEnabled(bool enabled) async {
state = enabled;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_keyNotificationsEnabled, enabled);
}
}
// ==================== ====================
/// Provider
final darkModeEnabledProvider = StateNotifierProvider<DarkModeNotifier, bool>((ref) {
return DarkModeNotifier();
});
class DarkModeNotifier extends StateNotifier<bool> {
DarkModeNotifier() : super(false) {
_loadFromStorage();
}
Future<void> _loadFromStorage() async {
final prefs = await SharedPreferences.getInstance();
state = prefs.getBool(_keyDarkModeEnabled) ?? false;
}
Future<void> toggle() async {
state = !state;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_keyDarkModeEnabled, state);
}
Future<void> setEnabled(bool enabled) async {
state = enabled;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_keyDarkModeEnabled, enabled);
}
}
// ==================== Provider ====================
/// Provider
final themeModeProvider = Provider<ThemeMode>((ref) {
final isDarkMode = ref.watch(darkModeEnabledProvider);
return isDarkMode ? ThemeMode.dark : ThemeMode.light;
});

View File

@ -44,6 +44,7 @@ dependencies:
# 其他
intl: ^0.18.0
logger: ^2.0.0
package_info_plus: ^8.0.0
dev_dependencies:
flutter_test: