feat(settings): 编辑个人资料头像上传全量实现(#49 / #50)
- EditProfileState: 新增 isUploadingAvatar 字段 - EditProfileViewModel: pickAndUploadAvatar()(ImagePicker→裁剪→CDN→state) - EditProfilePage: 完整重写 - 88pt 圆形头像 + 8色渐变占位 + 相机角标 + 上传进度环 - _showAvatarSourceSheet()(相册 / 拍照) - Card 分组表单:昵称(50字计数)/ 个人简介(200字多行) - 保存按钮(昵称空或上传中禁用) - 错误 Banner - 保存成功 → 刷新 SettingsViewModel + pop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,22 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:image_cropper/image_cropper.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
import 'package:im_app/app/di/network_provider.dart';
|
||||
import 'package:im_app/data/remote/upload_file_request.dart';
|
||||
import 'package:im_app/features/settings/di/settings_providers.dart';
|
||||
|
||||
/// 编辑个人资料页状态
|
||||
/// 编辑个人资料页状态(#49 / #50)
|
||||
class EditProfileState {
|
||||
final String nickname;
|
||||
final String bio;
|
||||
final String? avatarUrl;
|
||||
final bool isLoading;
|
||||
final bool isSaving;
|
||||
final bool isUploadingAvatar;
|
||||
final String? error;
|
||||
|
||||
const EditProfileState({
|
||||
@@ -17,6 +25,7 @@ class EditProfileState {
|
||||
this.avatarUrl,
|
||||
this.isLoading = false,
|
||||
this.isSaving = false,
|
||||
this.isUploadingAvatar = false,
|
||||
this.error,
|
||||
});
|
||||
|
||||
@@ -27,6 +36,7 @@ class EditProfileState {
|
||||
bool clearAvatarUrl = false,
|
||||
bool? isLoading,
|
||||
bool? isSaving,
|
||||
bool? isUploadingAvatar,
|
||||
String? error,
|
||||
bool clearError = false,
|
||||
}) {
|
||||
@@ -36,12 +46,36 @@ class EditProfileState {
|
||||
avatarUrl: clearAvatarUrl ? null : (avatarUrl ?? this.avatarUrl),
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
isSaving: isSaving ?? this.isSaving,
|
||||
isUploadingAvatar: isUploadingAvatar ?? this.isUploadingAvatar,
|
||||
error: clearError ? null : (error ?? this.error),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 编辑个人资料 ViewModel
|
||||
/// 编辑个人资料 ViewModel(#49 / #50)
|
||||
///
|
||||
/// ## 数据流
|
||||
/// ```
|
||||
/// EditProfilePage (build)
|
||||
/// └─ ref.watch(editProfileViewModelProvider) 读取 nickname/bio/avatarUrl
|
||||
///
|
||||
/// 初始化:
|
||||
/// _loadCurrentProfile()
|
||||
/// → FetchProfileUseCase.execute() → GET /app/api/user/profile
|
||||
/// → 填充 nickname, bio, avatarUrl
|
||||
///
|
||||
/// 头像上传(#49):
|
||||
/// pickAndUploadAvatar()
|
||||
/// → ImagePicker.pickImage(gallery | camera)
|
||||
/// → ImageCropper.cropImage() (iOS UIImagePickerController 圆形预览)
|
||||
/// → UploadFileRequest FormData → POST /app/api/upload/file
|
||||
/// → state.avatarUrl = result.url
|
||||
///
|
||||
/// 保存:
|
||||
/// save()
|
||||
/// → UpdateProfileUseCase.execute(nickname, bio, profilePicUrl)
|
||||
/// → POST /app/api/user/update-profile
|
||||
/// ```
|
||||
class EditProfileViewModel extends Notifier<EditProfileState> {
|
||||
@override
|
||||
EditProfileState build() {
|
||||
@@ -49,6 +83,8 @@ class EditProfileViewModel extends Notifier<EditProfileState> {
|
||||
return const EditProfileState(isLoading: true);
|
||||
}
|
||||
|
||||
// ── 初始化 ────────────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> _loadCurrentProfile() async {
|
||||
try {
|
||||
final profile = await ref.read(fetchProfileUseCaseProvider).execute();
|
||||
@@ -63,6 +99,8 @@ class EditProfileViewModel extends Notifier<EditProfileState> {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 字段更新 ──────────────────────────────────────────────────────────────
|
||||
|
||||
void updateNickname(String value) {
|
||||
state = state.copyWith(nickname: value, clearError: true);
|
||||
}
|
||||
@@ -71,8 +109,64 @@ class EditProfileViewModel extends Notifier<EditProfileState> {
|
||||
state = state.copyWith(bio: value);
|
||||
}
|
||||
|
||||
// TODO Issue #6: 头像上传(CDN 流程)
|
||||
// void pickAndUploadAvatar() async { ... }
|
||||
// ── 头像选取与上传(#49) ──────────────────────────────────────────────────
|
||||
|
||||
/// 弹出选图 Sheet(相册 / 拍照)→ 裁剪 → 上传 CDN → 更新预览
|
||||
///
|
||||
/// 上传中 [isUploadingAvatar] = true,完成后恢复 false。
|
||||
/// 失败时设置 [error](由 UI 显示 SnackBar)。
|
||||
Future<void> pickAndUploadAvatar(ImageSource source) async {
|
||||
final picker = ImagePicker();
|
||||
final picked = await picker.pickImage(
|
||||
source: source,
|
||||
maxWidth: 900,
|
||||
maxHeight: 900,
|
||||
imageQuality: 85,
|
||||
);
|
||||
if (picked == null) return;
|
||||
|
||||
// 圆形裁剪(iOS UIImagePickerController 圆形预览对齐)
|
||||
final cropped = await ImageCropper().cropImage(
|
||||
sourcePath: picked.path,
|
||||
aspectRatio: const CropAspectRatio(ratioX: 1, ratioY: 1),
|
||||
uiSettings: [
|
||||
IOSUiSettings(
|
||||
title: '裁剪头像',
|
||||
aspectRatioLockEnabled: true,
|
||||
resetAspectRatioEnabled: false,
|
||||
rotateButtonsHidden: false,
|
||||
),
|
||||
AndroidUiSettings(
|
||||
toolbarTitle: '裁剪头像',
|
||||
lockAspectRatio: true,
|
||||
),
|
||||
],
|
||||
);
|
||||
if (cropped == null) return;
|
||||
|
||||
state = state.copyWith(isUploadingAvatar: true, clearError: true);
|
||||
try {
|
||||
final result = await ref.read(networkSdkApiProvider).executeRequest(
|
||||
UploadFileRequest(filePath: cropped.path),
|
||||
);
|
||||
final url = result?.url ?? '';
|
||||
if (url.isEmpty) throw Exception('上传返回空 URL');
|
||||
state = state.copyWith(isUploadingAvatar: false, avatarUrl: url);
|
||||
} catch (e) {
|
||||
debugPrint('[EditProfileViewModel] avatar upload error: $e');
|
||||
state = state.copyWith(
|
||||
isUploadingAvatar: false,
|
||||
error: '头像上传失败: $e',
|
||||
);
|
||||
} finally {
|
||||
// 清理临时裁剪文件
|
||||
try {
|
||||
await File(cropped.path).delete();
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 保存 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
Future<bool> save() async {
|
||||
if (state.nickname.trim().isEmpty) {
|
||||
|
||||
@@ -1,20 +1,121 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
import 'package:im_app/features/settings/presentation/edit_profile_view_model.dart';
|
||||
import 'package:im_app/features/settings/presentation/settings_view_model.dart';
|
||||
|
||||
/// 编辑个人资料页
|
||||
/// 编辑个人资料页(#49 / #50)
|
||||
///
|
||||
/// 对应 Gitea issue #6
|
||||
class EditProfilePage extends ConsumerWidget {
|
||||
/// ## 结构
|
||||
/// - AppBar 右侧:保存按钮(昵称为空或上传中时禁用)
|
||||
/// - 头像区:88pt 圆形 + 渐变占位 + 相机角标 + 上传进度环
|
||||
/// - Card 分组表单:昵称(50字)/ 个人简介(200字,多行)
|
||||
/// - 底部错误 Banner
|
||||
///
|
||||
/// ## 数据流
|
||||
/// - 加载:`editProfileViewModelProvider` build → `_loadCurrentProfile()`
|
||||
/// - 头像:`_showAvatarSourceSheet()` → `vm.pickAndUploadAvatar(source)`
|
||||
/// - 保存:`vm.save()` → 成功 → `settingsViewModelProvider.loadProfile()` + pop
|
||||
class EditProfilePage extends ConsumerStatefulWidget {
|
||||
const EditProfilePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ConsumerState<EditProfilePage> createState() => _EditProfilePageState();
|
||||
}
|
||||
|
||||
class _EditProfilePageState extends ConsumerState<EditProfilePage> {
|
||||
late final TextEditingController _nicknameCtrl;
|
||||
late final TextEditingController _bioCtrl;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_nicknameCtrl = TextEditingController();
|
||||
_bioCtrl = TextEditingController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nicknameCtrl.dispose();
|
||||
_bioCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Sync controllers once profile is loaded (isLoading → false, nickname populated)
|
||||
void _syncControllers(EditProfileState state) {
|
||||
if (!state.isLoading) {
|
||||
if (_nicknameCtrl.text != state.nickname) {
|
||||
_nicknameCtrl.value = TextEditingValue(
|
||||
text: state.nickname,
|
||||
selection: TextSelection.collapsed(offset: state.nickname.length),
|
||||
);
|
||||
}
|
||||
if (_bioCtrl.text != state.bio) {
|
||||
_bioCtrl.value = TextEditingValue(
|
||||
text: state.bio,
|
||||
selection: TextSelection.collapsed(offset: state.bio.length),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showAvatarSourceSheet() async {
|
||||
final vm = ref.read(editProfileViewModelProvider.notifier);
|
||||
await showModalBottomSheet<void>(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (_) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: 36,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.photo_library_rounded),
|
||||
title: const Text('从相册选取'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
vm.pickAndUploadAvatar(ImageSource.gallery);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.camera_alt_rounded),
|
||||
title: const Text('拍照'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
vm.pickAndUploadAvatar(ImageSource.camera);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = ref.watch(editProfileViewModelProvider);
|
||||
final vm = ref.read(editProfileViewModelProvider.notifier);
|
||||
|
||||
// Sync text controllers after profile loads
|
||||
_syncControllers(state);
|
||||
|
||||
final canSave =
|
||||
state.nickname.trim().isNotEmpty && !state.isUploadingAvatar;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('编辑资料'),
|
||||
@@ -30,14 +131,17 @@ class EditProfilePage extends ConsumerWidget {
|
||||
)
|
||||
else
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
onPressed: canSave
|
||||
? () async {
|
||||
final ok = await vm.save();
|
||||
if (ok && context.mounted) {
|
||||
// 刷新我的页面资料卡
|
||||
ref.read(settingsViewModelProvider.notifier).loadProfile();
|
||||
ref
|
||||
.read(settingsViewModelProvider.notifier)
|
||||
.loadProfile();
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
}
|
||||
: null,
|
||||
child: const Text('保存'),
|
||||
),
|
||||
],
|
||||
@@ -45,21 +149,132 @@ class EditProfilePage extends ConsumerWidget {
|
||||
body: state.isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
|
||||
children: [
|
||||
// 头像区域
|
||||
Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 44,
|
||||
backgroundImage: state.avatarUrl != null
|
||||
? NetworkImage(state.avatarUrl!)
|
||||
: null,
|
||||
child: state.avatarUrl == null
|
||||
? const Icon(Icons.person, size: 40)
|
||||
: null,
|
||||
// ── 头像区 ──────────────────────────────────────────────────
|
||||
Center(child: _AvatarSection(state: state, onTap: _showAvatarSourceSheet)),
|
||||
|
||||
const SizedBox(height: 28),
|
||||
|
||||
// ── 昵称卡片 ─────────────────────────────────────────────
|
||||
_FormCard(
|
||||
label: '昵称',
|
||||
child: TextField(
|
||||
controller: _nicknameCtrl,
|
||||
maxLength: 50,
|
||||
maxLengthEnforcement: MaxLengthEnforcement.enforced,
|
||||
onChanged: vm.updateNickname,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
decoration: InputDecoration(
|
||||
hintText: '请输入昵称',
|
||||
border: InputBorder.none,
|
||||
counterText: '',
|
||||
suffixText: '${state.nickname.length}/50',
|
||||
suffixStyle: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// ── 个人简介卡片 ──────────────────────────────────────────
|
||||
_FormCard(
|
||||
label: '个人简介',
|
||||
child: TextField(
|
||||
controller: _bioCtrl,
|
||||
maxLength: 200,
|
||||
maxLengthEnforcement: MaxLengthEnforcement.enforced,
|
||||
maxLines: null,
|
||||
minLines: 1,
|
||||
keyboardType: TextInputType.multiline,
|
||||
onChanged: vm.updateBio,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
decoration: InputDecoration(
|
||||
hintText: '介绍一下自己',
|
||||
border: InputBorder.none,
|
||||
counterText: '',
|
||||
suffixText: '${state.bio.length}/200',
|
||||
suffixStyle: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ── 错误 Banner ───────────────────────────────────────────
|
||||
if (state.error != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
_ErrorBanner(message: state.error!),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 头像区(88pt 圆 + 渐变占位 + 相机角标 + 上传进度环)────────────────────
|
||||
|
||||
class _AvatarSection extends StatelessWidget {
|
||||
const _AvatarSection({required this.state, required this.onTap});
|
||||
|
||||
final EditProfileState state;
|
||||
final VoidCallback onTap;
|
||||
|
||||
// 与 ProfileHeroCard 保持一致的 8 色渐变方案
|
||||
static const _gradients = [
|
||||
[Color(0xFF4776E6), Color(0xFF8E54E9)],
|
||||
[Color(0xFF11998E), Color(0xFF38EF7D)],
|
||||
[Color(0xFFFF6B6B), Color(0xFFFFE66D)],
|
||||
[Color(0xFF2193B0), Color(0xFF6DD5ED)],
|
||||
[Color(0xFFCC2B5E), Color(0xFF753A88)],
|
||||
[Color(0xFFEB5757), Color(0xFF000000)],
|
||||
[Color(0xFF56CCF2), Color(0xFF2F80ED)],
|
||||
[Color(0xFFF7971E), Color(0xFFFFD200)],
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const size = 88.0;
|
||||
const radius = size / 2;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: SizedBox(
|
||||
width: size + 4,
|
||||
height: size + 4,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// 上传中进度环
|
||||
if (state.isUploadingAvatar)
|
||||
SizedBox(
|
||||
width: size + 4,
|
||||
height: size + 4,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.5,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
|
||||
// 头像圆
|
||||
ClipOval(
|
||||
child: SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
child: state.avatarUrl != null
|
||||
? Image.network(
|
||||
state.avatarUrl!,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) =>
|
||||
_GradientPlaceholder(size: size, gradients: _gradients),
|
||||
)
|
||||
: _GradientPlaceholder(size: size, gradients: _gradients),
|
||||
),
|
||||
),
|
||||
|
||||
// 相机角标(右下)
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
@@ -69,59 +284,123 @@ class EditProfilePage extends ConsumerWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.camera_alt_rounded,
|
||||
size: 14,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GradientPlaceholder extends StatelessWidget {
|
||||
const _GradientPlaceholder({required this.size, required this.gradients});
|
||||
|
||||
final double size;
|
||||
final List<List<Color>> gradients;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pair = gradients[0]; // 默认第一组,加载完用真实头像替换
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: pair,
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
child: const Icon(Icons.person_rounded, size: 40, color: Colors.white70),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 表单分组卡片 ─────────────────────────────────────────────────────────────
|
||||
|
||||
class _FormCard extends StatelessWidget {
|
||||
const _FormCard({required this.label, required this.child});
|
||||
|
||||
final String label;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
child,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 错误 Banner ──────────────────────────────────────────────────────────────
|
||||
|
||||
class _ErrorBanner extends StatelessWidget {
|
||||
const _ErrorBanner({required this.message});
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.warning_rounded,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.onErrorContainer,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
message,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onErrorContainer,
|
||||
),
|
||||
child: const Icon(Icons.camera_alt, size: 16, color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
// TODO Issue #6: 头像上传(CDN 流程)
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('头像上传功能开发中')),
|
||||
);
|
||||
},
|
||||
child: const Text('更换头像'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// 昵称
|
||||
TextFormField(
|
||||
initialValue: state.nickname,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '昵称',
|
||||
border: OutlineInputBorder(),
|
||||
counterText: '',
|
||||
),
|
||||
maxLength: 50,
|
||||
onChanged: vm.updateNickname,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// 个人简介
|
||||
TextFormField(
|
||||
initialValue: state.bio,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '个人简介',
|
||||
border: OutlineInputBorder(),
|
||||
alignLabelWithHint: true,
|
||||
),
|
||||
maxLines: 3,
|
||||
maxLength: 200,
|
||||
onChanged: vm.updateBio,
|
||||
),
|
||||
if (state.error != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
state.error!,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user