Files
xianyan/lib/features/user_center/presentation/user_center_page.dart
Developer f91be94e9c refactor: 完成项目架构重构,统一模块导入路径
- 清理大量废弃的 barrel 导出文件,移除冗余的中间导出层
- 修复所有相对路径导入错误,统一调整为扁平化模块引用
- 更新多平台 pubspec 版本号与依赖库版本
- 补充后端功能问题管理后台与脚本工具
- 调整部分页面的快捷方式文案适配新功能
- 更新部分翻译覆盖率与API文档
2026-06-12 08:53:57 +08:00

554 lines
20 KiB
Dart
Raw Permalink 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.
/// ============================================================
/// 闲言APP — 个人中心页面
/// 创建时间: 2026-05-01
/// 更新时间: 2026-06-07
/// 作用: 用户个人中心 — 头像信息 + 统计栏 + 快捷入口 + 可编辑信息 + 账户设置 + 调试
/// 上次更新: 修复accountInsightsProvider未初始化导致Bad state崩溃增加authState守卫修复_loadDashboard异常静默吞掉问题修复score类型安全
/// ============================================================
import 'package:flutter/cupertino.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:xianyan/core/router/app_nav_extension.dart';
import 'package:xianyan/core/router/navigator_extension.dart';
import 'package:xianyan/core/utils/logger.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/theme/app_spacing.dart';
import '../../../core/theme/app_typography.dart';
import '../../../core/theme/app_radius.dart';
import '../../../core/router/app_routes.dart';
import '../../../shared/widgets/containers/glass_container.dart';
import '../../../shared/widgets/adaptive/adaptive_back_button.dart';
import '../../../shared/widgets/feedback/offline_banner.dart';
import '../../../shared/widgets/feedback/app_toast.dart';
import '../../../shared/widgets/adaptive/responsive_layout.dart';
import '../../../l10n/translation_resolver.dart';
import '../../auth/providers/auth_provider.dart';
import '../services/user_center_service.dart';
import '../providers/account_insights_provider.dart';
import 'favorite_search_sheet.dart';
import 'widgets/account_insights_sheet.dart';
import 'widgets/profile_header_row.dart';
import 'widgets/user_stats_bar.dart';
import 'widgets/quick_action_grid.dart';
import 'widgets/editable_info_section.dart';
import 'widgets/account_section.dart';
class UserCenterPage extends ConsumerStatefulWidget {
const UserCenterPage({super.key});
@override
ConsumerState<UserCenterPage> createState() => _UserCenterPageState();
}
class _UserCenterPageState extends ConsumerState<UserCenterPage> {
Map<String, dynamic>? _dashboardData;
bool _isDashboardLoading = true;
@override
void initState() {
super.initState();
_loadDashboard();
}
Future<void> _loadDashboard() async {
if (!ref.read(authProvider).isLoggedIn) return;
try {
final data = await UserCenterService.getDashboard();
if (mounted) {
setState(() {
_dashboardData = data;
_isDashboardLoading = false;
});
}
} catch (e, st) {
Log.e('UserCenterPage: 加载面板数据失败', e, st);
if (mounted) setState(() => _isDashboardLoading = false);
}
}
Future<void> _refresh() async {
await Future.wait([
_loadDashboard(),
ref.read(authProvider.notifier).refreshUser(),
]);
}
/// 构建账户洞察按钮 — 仅在已登录时监听 accountInsightsProvider
/// 未初始化/未登录时展示简单图标,避免 provider 未初始化崩溃
Widget _buildInsightButton(AppThemeExtension ext, AuthState authState) {
// 未初始化或未登录时,不监听 accountInsightsProvider
if (!authState.isInitialized || !authState.isLoggedIn) {
return Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: ext.bgSecondary,
borderRadius: BorderRadius.circular(999),
),
child: const Icon(
CupertinoIcons.shield_fill,
size: 18,
color: CupertinoColors.systemGrey,
),
);
}
final unreadCount = ref.watch(accountInsightsProvider).unreadCount;
return GestureDetector(
onTap: () => AccountInsightsSheet.show(context, ref),
child: Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: ext.bgSecondary,
borderRadius: BorderRadius.circular(999),
),
child: Stack(
alignment: Alignment.center,
children: [
const Icon(
CupertinoIcons.shield_fill,
size: 18,
color: CupertinoColors.systemGrey,
),
if (unreadCount > 0)
Positioned(
top: 2,
right: -2,
child: Container(
constraints: const BoxConstraints(
minWidth: 16,
minHeight: 16,
),
padding: const EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration(
color: ext.errorColor,
borderRadius: BorderRadius.circular(999),
border: Border.fromBorderSide(
BorderSide(color: ext.bgCard, width: 1.5),
),
),
alignment: Alignment.center,
child: Text(
unreadCount > 9 ? '9+' : '$unreadCount',
style: const TextStyle(
color: CupertinoColors.white,
fontSize: 10,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
);
}
void _showAvatarPicker() {
final ext = AppTheme.ext(context);
final t = ref.read(translationsProvider);
showCupertinoModalPopup<void>(
context: context,
builder: (ctx) => CupertinoActionSheet(
title: Text(
t.profile.changeAvatar,
style: AppTypography.subhead.copyWith(color: ext.textSecondary),
),
actions: [
CupertinoActionSheetAction(
onPressed: () {
Navigator.pop(ctx);
_showAvatarUrlInput();
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(CupertinoIcons.link, size: 18, color: ext.accent),
const SizedBox(width: 8),
Text(t.profile.inputAvatarUrl),
],
),
),
CupertinoActionSheetAction(
onPressed: () {},
isDefaultAction: true,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(CupertinoIcons.lock, size: 18, color: ext.textHint),
const SizedBox(width: 8),
Text(
t.profile.selectFromAlbum,
style: TextStyle(color: ext.textHint),
),
],
),
),
],
cancelButton: CupertinoActionSheetAction(
isDestructiveAction: true,
onPressed: () => Navigator.pop(ctx),
child: Text(t.common.cancel),
),
),
);
}
void _showAvatarUrlInput() {
final controller = TextEditingController();
final dialogExt = AppTheme.ext(context);
final t = ref.read(translationsProvider);
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: Text(t.profile.avatarUnderReview.replaceAll('🔍 ', '🔗 ')),
content: Padding(
padding: const EdgeInsets.only(top: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CupertinoTextField(
controller: controller,
placeholder: 'https://example.com/avatar.jpg',
keyboardType: TextInputType.url,
maxLength: 2048,
clearButtonMode: OverlayVisibilityMode.editing,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: CupertinoColors.systemGrey6,
borderRadius: AppRadius.mdBorder,
),
),
const SizedBox(height: 6),
Text(
t.profile.avatarUrlHint,
style: AppTypography.caption2.copyWith(
color: dialogExt.textHint,
),
),
],
),
),
actions: [
CupertinoDialogAction(
child: Text(t.common.cancel),
onPressed: () {
controller.dispose();
ctx.dismissDialog();
},
),
CupertinoDialogAction(
isDefaultAction: true,
child: Text(t.common.confirm),
onPressed: () async {
final url = controller.text.trim();
ctx.dismissDialog();
controller.dispose();
if (url.isEmpty) {
AppToast.showWarning(t.profile.pleaseInputUrl);
return;
}
if (!url.startsWith('http://') && !url.startsWith('https://')) {
AppToast.showWarning(t.profile.urlMustStartWithHttp);
return;
}
if (url.length > 2048) {
AppToast.showWarning(t.profile.urlTooLong);
return;
}
try {
Uri.parse(url);
} catch (_) {
AppToast.showWarning(t.profile.invalidUrlFormat);
return;
}
final ext = AppTheme.ext(context);
showCupertinoDialog<void>(
context: context,
builder: (dCtx) => CupertinoAlertDialog(
title: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CupertinoActivityIndicator(color: ext.accent, radius: 12),
const SizedBox(width: 10),
Text(t.profile.avatarUnderReview),
],
),
content: Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
t.profile.avatarReviewing,
style: AppTypography.subhead.copyWith(
color: ext.textSecondary,
),
textAlign: TextAlign.center,
),
),
),
);
try {
await UserCenterService.updateProfile(avatarUrl: url);
await ref.read(authProvider.notifier).refreshUser();
if (mounted) {
Navigator.of(context, rootNavigator: true).pop();
_showAvatarResult(true, t.profile.avatarChangeSuccess);
}
} catch (e) {
if (mounted) {
Navigator.of(context, rootNavigator: true).pop();
_showAvatarResult(
false,
'${t.profile.avatarChangeFailed}: $e',
);
}
}
},
),
],
),
);
}
void _showAvatarResult(bool success, String msg) {
final t = ref.read(translationsProvider);
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
success
? CupertinoIcons.checkmark_circle_fill
: CupertinoIcons.xmark_circle_fill,
size: 20,
color: success
? CupertinoColors.systemGreen
: CupertinoColors.systemRed,
),
const SizedBox(width: 8),
Text(success ? t.profile.success : t.profile.failed),
],
),
content: Text(msg),
actions: [
CupertinoDialogAction(
child: Text(t.profile.ok),
onPressed: () => ctx.dismissDialog(),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
final authState = ref.watch(authProvider);
final user = authState.user;
final t = ref.watch(translationsProvider);
return CupertinoPageScaffold(
backgroundColor: ext.bgPrimary,
navigationBar: CupertinoNavigationBar(
middle: Text(
t.profile.title,
style: AppTypography.title3.copyWith(color: ext.textPrimary),
),
backgroundColor: ext.bgPrimary.withValues(alpha: 0.85),
border: null,
leading: const AdaptiveBackButton(),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
// 搜索入口
CupertinoButton(
padding: EdgeInsets.zero,
onPressed: () => FavoriteSearchPage.show(context),
child: Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: ext.bgSecondary,
borderRadius: BorderRadius.circular(999),
),
child: Icon(
CupertinoIcons.search,
size: 18,
color: ext.accent,
),
),
),
const SizedBox(width: 8),
_buildInsightButton(ext, authState),
],
),
),
child: SafeArea(
bottom: false,
child: Column(
children: [
const OfflineBanner(),
Expanded(
child: ResponsiveMaxWidth(
maxWidth: 900,
padding: AppSpacing.md,
child: CustomScrollView(
physics: const BouncingScrollPhysics(),
keyboardDismissBehavior:
ScrollViewKeyboardDismissBehavior.onDrag,
slivers: [
if (!authState.isInitialized && user == null)
SliverFillRemaining(
hasScrollBody: false,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CupertinoActivityIndicator(color: ext.accent),
const SizedBox(height: AppSpacing.md),
Text(
t.profile.loading,
style: AppTypography.subhead.copyWith(
color: ext.textSecondary,
),
),
],
),
),
)
else if (authState.isLoggedIn && user != null) ...[
CupertinoSliverRefreshControl(onRefresh: _refresh),
SliverToBoxAdapter(
child: RepaintBoundary(
child:
ProfileHeaderRow(
ext: ext,
user: user,
displayScore:
(_dashboardData?['score'] as num?)
?.toInt() ??
user.score,
tp: t.profile,
onAvatarTap: _showAvatarPicker,
)
.animate()
.fadeIn(duration: 300.ms)
.slideY(begin: 0.05, end: 0),
),
),
SliverToBoxAdapter(
child: UserStatsBar(
ext: ext,
user: user,
dashboardData: _dashboardData,
isLoading: _isDashboardLoading,
).animate().fadeIn(duration: 300.ms, delay: 80.ms),
),
SliverToBoxAdapter(
child:
QuickActionGrid(
ext: ext,
ref: ref,
profileTranslations: t.profile,
)
.animate()
.fadeIn(duration: 300.ms, delay: 130.ms)
.slideY(begin: 0.05, end: 0),
),
SliverToBoxAdapter(
child:
EditableInfoSection(
ext: ext,
user: user,
onAvatarTap: _showAvatarPicker,
)
.animate()
.fadeIn(duration: 300.ms, delay: 160.ms)
.slideY(begin: 0.05, end: 0),
),
SliverToBoxAdapter(
child: AccountSection(ext: ext, tp: t.profile)
.animate()
.fadeIn(duration: 300.ms, delay: 180.ms)
.slideY(begin: 0.05, end: 0),
),
SliverToBoxAdapter(
child: DebugSection(ext: ext, tp: t.profile)
.animate()
.fadeIn(duration: 300.ms, delay: 220.ms)
.slideY(begin: 0.05, end: 0),
),
SliverToBoxAdapter(
child: AppInfoFooter(
ext: ext,
).animate().fadeIn(duration: 300.ms, delay: 260.ms),
),
SliverToBoxAdapter(
child: SizedBox(
height: MediaQuery.of(context).viewInsets.bottom + 40,
),
),
] else ...[
SliverToBoxAdapter(
child: _buildLoggedOutContent(context, ext),
),
],
],
),
),
),
],
),
),
);
}
Widget _buildLoggedOutContent(BuildContext context, AppThemeExtension ext) {
final t = ref.read(translationsProvider);
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.xxl,
),
child: GlassContainer(
child: Column(
children: [
Icon(
CupertinoIcons.person_crop_circle_badge_xmark,
size: 48,
color: ext.textHint,
),
const SizedBox(height: AppSpacing.md),
Text(
t.profile.tapToLogin,
style: AppTypography.title3.copyWith(color: ext.textPrimary),
),
const SizedBox(height: AppSpacing.sm),
Text(
t.profile.loginToViewProfile,
style: AppTypography.subhead.copyWith(color: ext.textSecondary),
),
const SizedBox(height: AppSpacing.md),
CupertinoButton(
color: ext.accent,
borderRadius: AppRadius.pillBorder,
onPressed: () => context.appPush(AppRoutes.login),
child: Text(
t.profile.goLogin,
style: AppTypography.body.copyWith(
color: CupertinoColors.white,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
);
}
}