Files
video/app/lib/screens/profile/user_profile_screen.dart
2026-10-15 20:36:59 +08:00

212 lines
6.8 KiB
Dart

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/theme/app_theme.dart';
import '../../models/video_item.dart';
import '../../providers/profile_provider.dart';
/// 他人公开主页(《03-接口文档》4.7/4.8,游客可看):
/// 头像/昵称/简介/作品数/获赞数 + 已发布作品双列网格,点击进单视频播放页
class UserProfileScreen extends ConsumerWidget {
const UserProfileScreen({super.key, required this.userId});
final int userId;
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(profileProvider(userId));
final notifier = ref.read(profileProvider(userId).notifier);
return Scaffold(
appBar: AppBar(title: Text(state.profile?.nickname ?? '用户主页')),
body: state.isLoading && state.profile == null
? const Center(
child: CircularProgressIndicator(color: AppColors.primary),
)
: state.loadFailed
? Center(
child: TextButton(
onPressed: notifier.load,
child: const Text('加载失败,点击重试',
style: TextStyle(color: AppColors.primary)),
),
)
: CustomScrollView(
slivers: [
// 头部资料卡
SliverToBoxAdapter(child: _ProfileHeader(state: state)),
// 作品网格
if (state.works.isEmpty)
const SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 48),
child: Center(
child: Text('暂无作品',
style: TextStyle(
color: AppColors.textSecondary)),
),
),
)
else
SliverPadding(
padding: const EdgeInsets.all(8),
sliver: SliverGrid.builder(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 0.68,
),
itemCount: state.works.length,
itemBuilder: (_, index) =>
_WorkCard(item: state.works[index]),
),
),
],
),
);
}
}
class _ProfileHeader extends StatelessWidget {
const _ProfileHeader({required this.state});
final ProfileState state;
@override
Widget build(BuildContext context) {
final profile = state.profile;
if (profile == null) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
CircleAvatar(
radius: 40,
backgroundColor: AppColors.surface,
backgroundImage: profile.avatarUrl.isNotEmpty
? CachedNetworkImageProvider(profile.avatarUrl)
: null,
child: profile.avatarUrl.isEmpty
? Text(
profile.nickname.isNotEmpty
? profile.nickname.characters.first
: '友',
style: const TextStyle(fontSize: 28),
)
: null,
),
const SizedBox(height: 12),
Text(
profile.nickname,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700),
),
if (profile.bio.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
profile.bio,
textAlign: TextAlign.center,
style: const TextStyle(
color: AppColors.textSecondary,
fontSize: 13,
),
),
],
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_Stat(count: profile.videoCount, label: '作品'),
const SizedBox(width: 40),
_Stat(count: profile.totalLikes, label: '获赞'),
],
),
],
),
);
}
}
class _Stat extends StatelessWidget {
const _Stat({required this.count, required this.label});
final int count;
final String label;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(
count >= 10000
? '${(count / 10000).toStringAsFixed(1)}w'
: '$count',
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w700),
),
Text(
label,
style: const TextStyle(color: AppColors.textSecondary, fontSize: 12),
),
],
);
}
}
class _WorkCard extends StatelessWidget {
const _WorkCard({required this.item});
final VideoItem item;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => context.push('/search/play', extra: item),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Stack(
fit: StackFit.expand,
children: [
Container(
color: AppColors.surface,
child: item.coverUrl.isNotEmpty
? CachedNetworkImage(
imageUrl: item.coverUrl,
fit: BoxFit.cover,
errorWidget: (_, __, ___) => const Icon(
Icons.play_circle_outline,
color: AppColors.textWeak,
size: 40,
),
)
: const Icon(
Icons.play_circle_outline,
color: AppColors.textWeak,
size: 40,
),
),
// 底部标题条
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
color: Colors.black54,
padding: const EdgeInsets.all(6),
child: Text(
item.title.isNotEmpty ? item.title : '未命名视频',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12),
),
),
),
],
),
),
);
}
}