修复用户端个人头像展示
This commit is contained in:
77
apps/user_app/lib/ui/features/profile/profile_avatar.dart
Normal file
77
apps/user_app/lib/ui/features/profile/profile_avatar.dart
Normal file
@@ -0,0 +1,77 @@
|
||||
// 功能描述:展示用户真实头像,并在头像为空或加载失败时回退用户名首字母。
|
||||
// 版本:1.1.0
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 安全读取头像;失败时返回空值,确保个人中心可继续展示其他资料。
|
||||
Future<Uint8List?> loadProfileAvatarSafely(
|
||||
Future<Uint8List?> Function() loader,
|
||||
) async {
|
||||
try {
|
||||
return await loader();
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 用户资料头像,保持圆形裁剪并提供稳定的文字占位状态。
|
||||
class ProfileAvatar extends StatelessWidget {
|
||||
const ProfileAvatar({
|
||||
required this.name,
|
||||
required this.imageBytes,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final Uint8List? imageBytes;
|
||||
|
||||
/// 从用户名生成占位字符,空名称统一显示“用”。
|
||||
String get _fallbackText {
|
||||
final normalized = name.trim();
|
||||
return normalized.isEmpty ? '用' : normalized.substring(0, 1);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bytes = imageBytes;
|
||||
final hasImage = bytes != null && bytes.isNotEmpty;
|
||||
final fallback = Center(
|
||||
child: Text(
|
||||
_fallbackText,
|
||||
key: const Key('profile-avatar-fallback'),
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return Semantics(
|
||||
image: true,
|
||||
label: '${name.trim().isEmpty ? '用户' : name.trim()}头像',
|
||||
child: ExcludeSemantics(
|
||||
child: Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: !hasImage
|
||||
? fallback
|
||||
: Image(
|
||||
key: const Key('profile-avatar-image'),
|
||||
image: MemoryImage(bytes),
|
||||
width: 56,
|
||||
height: 56,
|
||||
fit: BoxFit.cover,
|
||||
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) =>
|
||||
wasSynchronouslyLoaded || frame != null ? child : fallback,
|
||||
errorBuilder: (context, error, stackTrace) => fallback,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
// 功能描述:展示用户资料、头像、钱包概览及账户服务入口。
|
||||
// 版本:1.2.0
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
@@ -6,7 +10,9 @@ import '../../../app/dependencies.dart';
|
||||
import '../../../data/repositories/client_repository.dart';
|
||||
import '../../../domain/models/client_models.dart';
|
||||
import '../../core/widgets.dart';
|
||||
import 'profile_avatar.dart';
|
||||
|
||||
/// 用户端个人中心页面。
|
||||
class ProfilePage extends StatefulWidget {
|
||||
const ProfilePage({required this.session, required this.repository, super.key});
|
||||
|
||||
@@ -17,8 +23,9 @@ class ProfilePage extends StatefulWidget {
|
||||
State<ProfilePage> createState() => _ProfilePageState();
|
||||
}
|
||||
|
||||
/// 管理个人资料加载及账户服务操作。
|
||||
class _ProfilePageState extends State<ProfilePage> {
|
||||
late Future<(UserProfile, WalletSummary)> _future;
|
||||
late Future<(UserProfile, WalletSummary, Uint8List?)> _future;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -26,8 +33,16 @@ class _ProfilePageState extends State<ProfilePage> {
|
||||
_future = _load();
|
||||
}
|
||||
|
||||
Future<(UserProfile, WalletSummary)> _load() async =>
|
||||
(await widget.repository.profile(), await widget.repository.wallet());
|
||||
/// 并行加载资料和钱包,并在资料存在头像时读取受保护图片内容。
|
||||
Future<(UserProfile, WalletSummary, Uint8List?)> _load() async {
|
||||
final profileFuture = widget.repository.profile();
|
||||
final walletFuture = widget.repository.wallet();
|
||||
final profile = await profileFuture;
|
||||
final avatarFuture = profile.avatar.trim().isEmpty
|
||||
? Future<Uint8List?>.value()
|
||||
: loadProfileAvatarSafely(widget.repository.profileAvatar);
|
||||
return (profile, await walletFuture, await avatarFuture);
|
||||
}
|
||||
|
||||
Future<void> _addAddress() async {
|
||||
final controller = TextEditingController();
|
||||
@@ -87,7 +102,7 @@ class _ProfilePageState extends State<ProfilePage> {
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(title: const Text('我的')),
|
||||
body: FutureBuilder<(UserProfile, WalletSummary)>(
|
||||
body: FutureBuilder<(UserProfile, WalletSummary, Uint8List?)>(
|
||||
future: _future,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
@@ -100,18 +115,16 @@ class _ProfilePageState extends State<ProfilePage> {
|
||||
}
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final (profile, wallet) = snapshot.data!;
|
||||
final (profile, wallet, avatarBytes) = snapshot.data!;
|
||||
return ListView(
|
||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 32),
|
||||
children: [
|
||||
SurfaceSection(
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 28,
|
||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
foregroundColor: Theme.of(context).colorScheme.primary,
|
||||
child: Text(profile.name.isEmpty ? '用' : profile.name.substring(0, 1)),
|
||||
ProfileAvatar(
|
||||
name: profile.name,
|
||||
imageBytes: avatarBytes,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
|
||||
Reference in New Issue
Block a user