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

222 lines
7.0 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.
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 'package:image_picker/image_picker.dart';
import '../../core/network/api_exception.dart';
import '../../core/theme/app_theme.dart';
import '../../providers/auth_provider.dart';
import '../../services/user_service.dart';
import '../feed/widgets/feed_overlays.dart';
/// 编辑资料(《03-接口文档》4.2 / 4.3):
/// 头像点击即选图上传(jpg/png,≤5MB);昵称/简介/性别点「保存」提交。
class ProfileEditScreen extends ConsumerStatefulWidget {
const ProfileEditScreen({super.key});
@override
ConsumerState<ProfileEditScreen> createState() => _ProfileEditScreenState();
}
class _ProfileEditScreenState extends ConsumerState<ProfileEditScreen> {
late final TextEditingController _nicknameCtrl;
late final TextEditingController _bioCtrl;
late String _avatarUrl;
late int _gender;
bool _uploadingAvatar = false;
bool _saving = false;
final _picker = ImagePicker();
@override
void initState() {
super.initState();
final user = ref.read(authProvider).user;
_nicknameCtrl = TextEditingController(text: user?.nickname ?? '');
_bioCtrl = TextEditingController(text: user?.bio ?? '');
_avatarUrl = user?.avatarUrl ?? '';
_gender = user?.gender ?? 0;
}
@override
void dispose() {
_nicknameCtrl.dispose();
_bioCtrl.dispose();
super.dispose();
}
void _toast(String message) {
if (mounted) showTopToast(context, message);
}
/// 选头像 → 立即上传(上传成功即生效,无需再点保存)
Future<void> _pickAvatar() async {
if (_uploadingAvatar) return;
final picked = await _picker.pickImage(
source: ImageSource.gallery,
maxWidth: 1024,
maxHeight: 1024,
);
if (picked == null) return;
setState(() => _uploadingAvatar = true);
try {
final url = await UserService.uploadAvatar(
filePath: picked.path,
fileName: picked.name,
);
if (!mounted) return;
setState(() => _avatarUrl = url);
// 拉取最新用户信息同步我的页资料卡
ref.read(authProvider.notifier).updateUser(await UserService.fetchMe());
_toast('头像已更新');
} on ApiException catch (e) {
_toast(e.message);
} catch (_) {
_toast('网络异常,请稍后重试');
} finally {
if (mounted) setState(() => _uploadingAvatar = false);
}
}
Future<void> _save() async {
final nickname = _nicknameCtrl.text.trim();
if (nickname.isEmpty) {
_toast('请填写昵称');
return;
}
setState(() => _saving = true);
try {
final updated = await UserService.updateMe(
nickname: nickname,
bio: _bioCtrl.text.trim(),
gender: _gender,
);
ref.read(authProvider.notifier).updateUser(updated);
_toast('已保存');
if (mounted) Navigator.of(context).maybePop();
} on ApiException catch (e) {
_toast(e.message);
} catch (_) {
_toast('网络异常,请稍后重试');
} finally {
if (mounted) setState(() => _saving = false);
}
}
@override
Widget build(BuildContext context) {
// 未登录兜底(正常由入口守卫拦截)
if (!ref.watch(authProvider).isLoggedIn) {
return Scaffold(
appBar: AppBar(title: const Text('编辑资料')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.person_outline,
size: 64, color: AppColors.textWeak),
const SizedBox(height: 12),
const Text('登录后编辑资料',
style: TextStyle(color: AppColors.textSecondary)),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => context.push('/login'),
style:
ElevatedButton.styleFrom(minimumSize: const Size(160, 44)),
child: const Text('去登录 / 注册'),
),
],
),
),
);
}
final busy = _saving || _uploadingAvatar;
return Scaffold(
appBar: AppBar(title: const Text('编辑资料')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
// 头像(点击即选图上传)
Center(
child: GestureDetector(
onTap: _pickAvatar,
child: Stack(
alignment: Alignment.center,
children: [
CircleAvatar(
radius: 44,
backgroundColor: AppColors.surface,
backgroundImage: _avatarUrl.isNotEmpty
? CachedNetworkImageProvider(_avatarUrl)
: null,
child: _avatarUrl.isEmpty
? const Icon(Icons.person_outline,
size: 40, color: AppColors.textWeak)
: null,
),
if (_uploadingAvatar)
const SizedBox(
width: 88,
height: 88,
child: CircularProgressIndicator(
color: AppColors.primary, strokeWidth: 2),
),
],
),
),
),
const SizedBox(height: 8),
const Center(
child: Text('点击更换头像(jpg/png,≤5MB)',
style: TextStyle(color: AppColors.textWeak, fontSize: 12)),
),
const SizedBox(height: 24),
TextField(
controller: _nicknameCtrl,
enabled: !busy,
maxLength: 20,
decoration: const InputDecoration(labelText: '昵称'),
),
const SizedBox(height: 8),
TextField(
controller: _bioCtrl,
enabled: !busy,
maxLength: 200,
maxLines: 3,
decoration: const InputDecoration(labelText: '简介'),
),
const SizedBox(height: 8),
const Text('性别', style: TextStyle(color: AppColors.textSecondary)),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: [
for (final option in const [
(0, '不公开'),
(1, '男'),
(2, '女'),
])
ChoiceChip(
label: Text(option.$2),
selected: _gender == option.$1,
onSelected: busy
? null
: (_) => setState(() => _gender = option.$1),
),
],
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: busy ? null : _save,
style: ElevatedButton.styleFrom(minimumSize: const Size(0, 48)),
child: const Text('保存'),
),
],
),
);
}
}