flutter/lib/pages/my/user_info.dart
2025-09-13 17:01:01 +08:00

511 lines
20 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:bottom_picker/bottom_picker.dart';
import 'package:city_pickers/city_pickers.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:loopin/IM/controller/im_user_info_controller.dart';
import 'package:loopin/api/common_api.dart';
import 'package:loopin/components/network_or_asset_image.dart';
import 'package:loopin/service/http.dart';
import 'package:loopin/styles/index.dart';
import 'package:loopin/utils/index.dart';
import 'package:loopin/utils/permissions.dart';
import 'package:loopin/utils/wxsdk.dart';
import 'package:shirne_dialog/shirne_dialog.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
class UserInfo extends StatefulWidget {
const UserInfo({super.key});
@override
State<UserInfo> createState() => _UserInfoState();
}
class _UserInfoState extends State<UserInfo> {
final userInfoController = Get.find<ImUserInfoController>();
late List<Map<String, dynamic>> items;
@override
void initState() {
super.initState();
items = [
{
'title': '昵称',
'value': userInfoController.nickname,
'onTap': () => Get.toNamed('/nickName'),
},
{
'title': '简介',
'value': userInfoController.signature,
'onTap': () => Get.toNamed('/des'),
},
{
'title': '性别',
'value': userInfoController.gender,
'onTap': () => setGender(context),
},
{
'title': '生日',
'value': userInfoController.birthday,
'onTap': () => selectDate(context),
},
{
'title': '区域',
'value': userInfoController.customInfo,
'onTap': () => pickCity(),
},
{
'title': '绑定微信',
'value': userInfoController.customInfo,
'onTap': () => wechatLogin(),
},
];
}
/// 微信授权
Future<void> wechatLogin() async {
await Wxsdk.login();
}
/// 选性别
void setGender(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return SafeArea(
child: Container(
height: 300,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
child: Column(
children: [
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ListTile(
title: Center(child: Text('')),
onTap: () {
userInfoController.gender.value = 1;
userInfoController.updateGender();
Get.back();
},
),
Divider(),
ListTile(
title: Center(child: Text('')),
onTap: () {
userInfoController.gender.value = 2;
userInfoController.updateGender();
Get.back();
}),
Divider(),
ListTile(
title: Center(child: Text('保密')),
onTap: () {
userInfoController.gender.value = 0;
userInfoController.updateGender();
Get.back();
},
),
Container(
height: 10,
color: Colors.grey[200],
),
],
),
),
ListTile(
title: Center(child: Text('取消')),
onTap: () => Get.back(),
),
],
),
),
);
},
);
}
/// 选生日
void selectDate(BuildContext context) {
DateTime selectedDate = DateTime.now(); // 初始值设为当前时间
BottomPicker.date(
pickerTitle: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(
onPressed: () {
Get.back();
},
child: Text('取消'),
),
TextButton(
onPressed: () {
// 获取选择结果逻辑
final dateStr = "${selectedDate.year}-${selectedDate.month.toString().padLeft(2, '0')}-${selectedDate.day.toString().padLeft(2, '0')}";
print(dateStr);
DateTime birthdayDate = DateTime(selectedDate.year, selectedDate.month, selectedDate.day);
userInfoController.birthday.value = birthdayDate.millisecondsSinceEpoch ~/ 1000; //秒级时间戳
userInfoController.updateBirthday();
Get.back();
},
child: Text('确认'),
),
],
),
displaySubmitButton: false,
displayCloseIcon: false,
dismissable: true, // 允许点击空白关闭
initialDateTime: DateTime.now(),
minDateTime: DateTime(1900),
maxDateTime: DateTime(
DateTime.now().year,
DateTime.now().month,
DateTime.now().day,
23,
59,
59,
),
dateOrder: DatePickerDateOrder.ymd,
pickerTextStyle: const TextStyle(fontSize: 16, color: Colors.black87),
height: 300,
onChange: (date) {
selectedDate = date as DateTime;
},
).show(context);
}
///选所在地
void pickCity() async {
final result = await CityPickers.showCityPicker(
context: context,
showType: ShowType.pca, // 显示省市区
height: 300.0,
borderRadius: 16.0,
barrierDismissible: true,
theme: Theme.of(context).copyWith(
scaffoldBackgroundColor: Colors.white,
),
);
if (result != null) {
final areaName = '${result.provinceName}-${result.cityName}-${result.areaName}';
print(result.toString());
print('${result.provinceName}-${result.cityName}-${result.areaName}-${result.areaId}');
//修改
userInfoController.customInfo['area'] = areaName;
userInfoController.customInfo['areaCode'] = '${result.areaId}';
userInfoController.updateArea();
userInfoController.customInfo.refresh();
}
}
///选背景
void pickCover(BuildContext context) async {
final hasPer = await Permissions.requestPhotoPermission();
if (!hasPer) {
Permissions.showPermissionDialog();
return;
}
final pickedAssets = await AssetPicker.pickAssets(
context,
pickerConfig: AssetPickerConfig(
textDelegate: const AssetPickerTextDelegate(),
pathNameBuilder: (AssetPathEntity album) {
return Utils.translateAlbumName(album);
},
maxAssets: 1,
requestType: RequestType.image,
filterOptions: FilterOptionGroup(
imageOption: const FilterOption(),
),
),
);
if (pickedAssets != null && pickedAssets.isNotEmpty) {
final asset = pickedAssets.first;
final file = await asset.file; // 获取实际文件
if (file != null) {
final fileSizeInBytes = await file.length();
final sizeInMB = fileSizeInBytes / (1024 * 1024);
if (sizeInMB > 200) {
MyDialog.toast('图片大小不能超过200MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
} else {
print("视频合法,大小:$sizeInMB MB");
//走upload(file)上传图片拿到url地址
final istance = MyDialog.loading('上传中');
final res = await Http.upload(CommonApi.uploadFile, filePath: file.path);
userInfoController.customInfo['coverBg'] = res['data']['url'];
userInfoController.updateCover();
userInfoController.customInfo.refresh();
istance.close();
}
}
}
}
///选头像
void pickFaceUrl(BuildContext context) async {
final hasPer = await Permissions.requestPhotoPermission();
if (!hasPer) {
Permissions.showPermissionDialog();
return;
}
final pickedAssets = await AssetPicker.pickAssets(
context,
pickerConfig: AssetPickerConfig(
textDelegate: const AssetPickerTextDelegate(),
pathNameBuilder: (AssetPathEntity album) {
return Utils.translateAlbumName(album);
},
maxAssets: 1,
requestType: RequestType.image,
filterOptions: FilterOptionGroup(
imageOption: const FilterOption(),
),
),
);
if (pickedAssets != null && pickedAssets.isNotEmpty) {
final asset = pickedAssets.first;
final file = await asset.file; // 获取实际文件
if (file != null) {
final fileSizeInBytes = await file.length();
final sizeInMB = fileSizeInBytes / (1024 * 1024);
if (sizeInMB > 20) {
MyDialog.toast('图片大小不能超过20MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
} else {
print("图片合法,大小:$sizeInMB MB");
//走upload(file)上传图片拿到url地址
final istance = MyDialog.loading('上传中');
final res = await Http.upload(CommonApi.uploadFile, filePath: file.path);
userInfoController.faceUrl.value = res['data']['url'];
userInfoController.updateFaceUrl();
userInfoController.customInfo.refresh();
istance.close();
}
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
actions: [
Padding(
padding: const EdgeInsets.only(right: 12.0),
child: GestureDetector(
onTap: () => pickCover(context),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0),
decoration: BoxDecoration(
color: Colors.black.withAlpha(100),
borderRadius: BorderRadius.circular(20.0),
),
child: Row(
children: const [
Icon(Icons.camera_alt, color: Colors.white, size: 16),
SizedBox(width: 4),
Text('更换封面', style: TextStyle(color: Colors.white, fontSize: 14)),
],
),
),
),
),
],
),
body: SizedBox.expand(
child: Stack(
children: [
// 封面图
SizedBox(
height: 240,
width: double.infinity,
child: Obx(() {
final imageUrl = userInfoController.customInfo['coverBg'];
return GestureDetector(
onTap: () => pickCover(context),
// child: Image(
// image: (imageUrl != null && imageUrl.isNotEmpty) ? NetworkImage(imageUrl) : const AssetImage('assets/images/pic2.jpg') as ImageProvider,
// fit: BoxFit.cover,
// ),
child: NetworkOrAssetImage(
imageUrl: imageUrl,
placeholderAsset: 'assets/images/bk.jpg',
),
);
}),
),
// 白色内容容器
Positioned(
top: 220,
left: 0,
right: 0,
bottom: 0,
child: ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(30),
topRight: Radius.circular(30),
),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(30),
topRight: Radius.circular(30),
),
),
child: SingleChildScrollView(
padding: const EdgeInsets.only(top: 60, bottom: 40),
child: Column(
children: [
const SizedBox(height: 0),
const SizedBox(height: 20),
Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
color: Colors.white,
child: Column(
children: items.map((item) {
return Column(
children: [
ListTile(
title: Row(
children: [
Text(
item['title'],
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(width: 50),
Expanded(child: Obx(() {
final val = item['value'];
if (val is RxString) {
return Text(
val.value,
style: const TextStyle(color: Colors.black),
overflow: TextOverflow.ellipsis,
maxLines: 1,
);
} else if (val is RxInt) {
String displayText;
if (item['title'] == '性别') {
displayText = val.value == 0
? '保密'
: val.value == 1
? ''
: val.value == 2
? ''
: '';
} else {
// 生日
// displayText = val.value == 0 ? '' : val.value.toString();
if (val.value == 0) {
displayText = '';
} else {
final date = DateTime.fromMillisecondsSinceEpoch(val.value * 1000);
displayText = "${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}";
}
}
return Text(
displayText,
style: const TextStyle(color: Colors.black),
overflow: TextOverflow.ellipsis,
maxLines: 1,
);
} else if (val is String) {
return Text(
val,
style: const TextStyle(color: Colors.black),
overflow: TextOverflow.ellipsis,
maxLines: 1,
);
} else if (val is Map) {
if (item['title'] == '区域') {
return Text(
val['area'] ?? '',
style: const TextStyle(color: Colors.black),
overflow: TextOverflow.ellipsis,
maxLines: 1,
);
} else {
String wxText = val['openId'] == null || val['openId'] == '' ? '未授权' : '已授权';
return Row(
children: [
Spacer(),
Text(
wxText,
style: const TextStyle(color: Colors.black),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
],
);
}
} else {
return const SizedBox.shrink();
}
}))
],
),
trailing: const Icon(
Icons.chevron_right,
color: FStyle.c999,
),
onTap: item['onTap'],
),
],
);
}).toList(),
),
)
],
),
),
),
),
),
// 头像层级最高放在Stack最后面覆盖白色容器和封面图
Positioned(
top: 220 - 60, // 封面图高度减去头像半径,使头像中线对齐封面底线
left: 0,
right: 0,
child: Obx(() {
final avatar = userInfoController.faceUrl.value;
return Center(
child: GestureDetector(
onTap: () => pickFaceUrl(context),
child: CircleAvatar(
radius: 60,
backgroundColor: Colors.white,
child: ClipOval(
child: NetworkOrAssetImage(
imageUrl: avatar,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
),
),
),
),
);
}),
),
],
),
),
);
}
}