Commit 48dff085 by tanghuan

注销用户相关整改

1 parent 8081bf8e
......@@ -79,6 +79,22 @@ class AccountCubit extends Cubit<AccountState> {
}
}
Future<void> goSetUserInfo() async {
dynamic result = await router.push(
'/account/user',
extra: {
'name': state.name,
'nickname': state.nickname,
'avatar': state.imgIcon,
},
);
if (result != null && result.isNotEmpty) {
Map<String, dynamic> resultMap = Map<String, dynamic>.from(result);
emit(state.copyWith(imgIcon: resultMap['avatar'], name: resultMap['name'], nickname: resultMap['nickname']));
}
}
Future<void> goBind() async {
String? result = await router.push(
'/account/phone',
......@@ -91,15 +107,35 @@ class AccountCubit extends Cubit<AccountState> {
}
}
Future<void> unbind() async {
void goLogoff() {
router.push(
'/account/logoff',
extra: {
'phone': state.phone,
},
);
}
var sharedPreferences = getIt.get<SharedPreferences>();
var userCode = sharedPreferences.getString('auth_userCode') ?? '';
_phoneAuthRepository.unbind(userCode);
Future<void> unbind() async {
// var sharedPreferences = getIt.get<SharedPreferences>();
// var userCode = sharedPreferences.getString('auth_userCode') ?? '';
// _phoneAuthRepository.unbind(userCode);
// 当前只会成功,不会失败
// 解绑成功,跳转登录界面
// router.go('/loginMain');
// emit(state.copyWith(showSnackBar: true, snackBarMsg: '操作成功'));
// emit(state.copyWith(showSnackBar: false));
// await Future.delayed(Duration(seconds: 1));
var sharedPreferences = getIt.get<SharedPreferences>();
sharedPreferences.getKeys().forEach((key) {
if (key.startsWith('auth_')) {
sharedPreferences.remove(key);
}
});
router.go('/loginMain');
}
}
import 'dart:async';
import 'package:appframe/config/locator.dart';
import 'package:appframe/config/routes.dart';
import 'package:appframe/data/repositories/phone_auth_repository.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AccountLogoffState extends Equatable {
final String phone;
final bool showSnackBar;
final String snackBarMsg;
final bool allowSend;
final int seconds;
final bool isLoading;
const AccountLogoffState({
this.phone = '',
this.showSnackBar = false,
this.snackBarMsg = '',
this.allowSend = true,
this.seconds = 0,
this.isLoading = false,
});
AccountLogoffState copyWith({
String? phone,
bool? showSnackBar,
String? snackBarMsg,
bool? allowSend,
int? seconds,
bool? isLoading,
}) {
return AccountLogoffState(
phone: phone ?? this.phone,
showSnackBar: showSnackBar ?? this.showSnackBar,
snackBarMsg: snackBarMsg ?? this.snackBarMsg,
allowSend: allowSend ?? this.allowSend,
seconds: seconds ?? this.seconds,
isLoading: isLoading ?? this.isLoading,
);
}
@override
List<Object?> get props => [
phone,
showSnackBar,
snackBarMsg,
allowSend,
seconds,
isLoading,
];
}
class AccountLogoffCubit extends Cubit<AccountLogoffState> {
late TextEditingController _codeController;
Timer? _timer;
int countdown = 60;
late final PhoneAuthRepository _phoneAuthRepository;
TextEditingController get codeController => _codeController;
AccountLogoffCubit(super.initialState) {
_codeController = TextEditingController();
_codeController.text = '';
_phoneAuthRepository = getIt.get<PhoneAuthRepository>();
}
/// 开始倒计时
void startCountdown() {
countdown = 60;
emit(state.copyWith(allowSend: false, seconds: countdown));
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
countdown--;
if (countdown <= 0) {
_timer?.cancel();
emit(state.copyWith(allowSend: true, seconds: 60));
} else {
emit(state.copyWith(seconds: countdown));
}
});
}
/// 发送验证码
Future<void> sendVerificationCode() async {
if (state.allowSend) {
if (!RegExp(r'^1[3-9][0-9]{9}$').hasMatch(state.phone)) {
emit(state.copyWith(showSnackBar: true, snackBarMsg: '手机号码信息错误'));
emit(state.copyWith(showSnackBar: false));
return;
}
var result = await _phoneAuthRepository.verifyCode(state.phone, 1);
if (result['code'] != 0) {
emit(state.copyWith(showSnackBar: true, snackBarMsg: result['error']));
emit(state.copyWith(showSnackBar: false));
return;
}
emit(state.copyWith(allowSend: false, seconds: 60));
startCountdown();
}
}
/// 注销账户
Future<void> logoff() async {
// String verifyCode = _codeController.text;
if (!RegExp(r'^1[3-9][0-9]{9}$').hasMatch(state.phone)) {
emit(state.copyWith(showSnackBar: true, snackBarMsg: '手机号码信息错误'));
emit(state.copyWith(showSnackBar: false));
return;
}
// if (!RegExp(r'^\d{4}$').hasMatch(verifyCode)) {
// emit(state.copyWith(showSnackBar: true, snackBarMsg: '请输入正确的验证码'));
// emit(state.copyWith(showSnackBar: false));
// return;
// }
emit(state.copyWith(isLoading: true));
// var sharedPreferences = getIt.get<SharedPreferences>();
// var userCode = sharedPreferences.getString('auth_userCode') ?? '';
// var result = await _phoneAuthRepository.unbindWithVerifyCode(userCode, verifyCode);
var result = await _phoneAuthRepository.unbindPhone(state.phone);
emit(state.copyWith(isLoading: false));
if (result['code'] != 0) {
emit(state.copyWith(showSnackBar: true, snackBarMsg: result['error']));
emit(state.copyWith(showSnackBar: false));
return;
}
emit(state.copyWith(showSnackBar: true, snackBarMsg: '操作成功'));
emit(state.copyWith(showSnackBar: false));
await Future.delayed(Duration(seconds: 1));
var sharedPreferences = getIt.get<SharedPreferences>();
sharedPreferences.getKeys().forEach((key) {
if (key.startsWith('auth_')) {
sharedPreferences.remove(key);
}
});
router.go('/loginMain');
}
@override
Future<void> close() async {
_timer?.cancel();
try {
_codeController.dispose();
} catch (e) {
print(e);
}
await super.close();
}
}
import 'dart:io';
import 'dart:typed_data';
import 'package:appframe/config/constant.dart';
import 'package:appframe/config/env_config.dart';
import 'package:appframe/config/locator.dart';
import 'package:appframe/config/routes.dart';
import 'package:appframe/data/repositories/phone_auth_repository.dart';
import 'package:appframe/services/api_service.dart';
import 'package:dio/dio.dart';
import 'package:equatable/equatable.dart';
import 'package:ffmpeg_kit_flutter_new/ffmpeg_kit.dart';
import 'package:ffmpeg_kit_flutter_new/return_code.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:uuid/uuid.dart';
class AccountUserState extends Equatable {
final String name;
final String nickname;
final String avatar;
final bool showSnackBar;
final String snackBarMsg;
final bool isLoading;
const AccountUserState({
this.name = '',
this.nickname = '',
this.avatar = '',
this.showSnackBar = false,
this.snackBarMsg = '',
this.isLoading = false,
});
AccountUserState copyWith({
String? name,
String? nickname,
String? avatar,
bool? showSnackBar,
String? snackBarMsg,
bool? isLoading,
}) {
return AccountUserState(
name: name ?? this.name,
nickname: nickname ?? this.nickname,
avatar: avatar ?? this.avatar,
showSnackBar: showSnackBar ?? this.showSnackBar,
snackBarMsg: snackBarMsg ?? this.snackBarMsg,
isLoading: isLoading ?? this.isLoading,
);
}
@override
List<Object?> get props => [
name,
nickname,
avatar,
showSnackBar,
snackBarMsg,
isLoading,
];
}
class AccountUserCubit extends Cubit<AccountUserState> {
late TextEditingController _nameController;
late TextEditingController _nickNameController;
late final PhoneAuthRepository _phoneAuthRepository;
TextEditingController get nameController => _nameController;
TextEditingController get nickNameController => _nickNameController;
AccountUserCubit(super.initialState) {
_nameController = TextEditingController(text: state.name);
_nickNameController = TextEditingController(text: state.nickname);
_phoneAuthRepository = getIt.get<PhoneAuthRepository>();
}
void updateAvatar(String avatarPath) {
emit(state.copyWith(avatar: avatarPath));
}
Future<void> save() async {
String name = _nameController.text.trim();
String nickname = _nickNameController.text.trim();
if (name.isEmpty) {
emit(state.copyWith(showSnackBar: true, snackBarMsg: '请输入姓名'));
emit(state.copyWith(showSnackBar: false));
return;
}
if (nickname.isEmpty) {
emit(state.copyWith(showSnackBar: true, snackBarMsg: '请输入昵称'));
emit(state.copyWith(showSnackBar: false));
return;
}
emit(state.copyWith(isLoading: true));
String imgPath = state.avatar;
if (!imgPath.startsWith('http')) {
imgPath = await _handleUpload(imgPath);
}
var sharedPreferences = getIt.get<SharedPreferences>();
var userCode = sharedPreferences.getString('auth_userCode') ?? '';
var result = await _phoneAuthRepository.updateUser(userCode, name, nickname, imgPath);
emit(state.copyWith(isLoading: false));
if (result['code'] != 0) {
emit(state.copyWith(showSnackBar: true, snackBarMsg: result['error']));
emit(state.copyWith(showSnackBar: false));
return;
}
router.pop({"name": name, "nickname": nickname, "avatar": imgPath});
}
Future<String> _handleUpload(String srcImgPath) async {
/// 1
/// 判断文件
///
//判断文件
File srcFile = File(srcImgPath);
if (!srcFile.existsSync()) {
throw Exception('文件不存在');
}
//暂时仅支持200M的文件上传
var fileSize = srcFile.lengthSync();
if (fileSize > 1024 * 1024 * 200) {
throw Exception('上传的文件过大');
}
// 对图片进行压缩处理
File file = await _compressImage(srcFile);
/// 2
/// bucket 存储桶名称 : bxe-files | bxe-pics | bxe-videos
///
String bucket = "bxe-pics";
/// 3
/// objectKey
///
var uuid = Uuid();
String logicPrefix = _getLoginPrefix();
String objectKey = '$logicPrefix/${uuid.v4()}${path.extension(file.path)}';
///
/// 4 计算分片
///
final chunkSize = Constant.obsUploadChunkSize;
final totalChunks = (fileSize / chunkSize).ceil();
///
/// 5 sig
///
final bxeApiService = ApiService(baseUrl: Constant.iotAppBaseUrl);
late String uploadId;
var signUrls = [];
for (int i = 0; i < totalChunks; i++) {
if (i == 0) {
final initResult = await _init(bxeApiService, objectKey, bucket);
uploadId = initResult['upload_id'] as String;
var signUrl = initResult['signed_url'] as String;
signUrls.add(signUrl);
} else {
final nextResult = await _next(bxeApiService, objectKey, bucket, uploadId, i + 1);
var signUrl = nextResult['signed_url'] as String;
signUrls.add(signUrl);
}
}
///
/// 6 上传
///
final dio = Dio()
..options = BaseOptions(
baseUrl: '',
connectTimeout: Duration(milliseconds: 30000),
receiveTimeout: Duration(milliseconds: 30000),
headers: {'Content-Type': '', 'Accept': ''},
);
final randomAccessFile = await file.open();
Map<int, String> tagsMap = {};
final futures = <Future>[];
for (int i = 0; i < totalChunks; i++) {
final chunkSize = Constant.obsUploadChunkSize;
final start = i * chunkSize;
final actualChunkSize = (i + 1) * chunkSize > fileSize ? fileSize - start : chunkSize;
final chunk = Uint8List(actualChunkSize);
randomAccessFile.setPositionSync(start);
await randomAccessFile.readInto(chunk, 0, actualChunkSize);
futures.add(_uploadChunkWithRetry(dio, signUrls[i], i, chunk));
}
var resultList = await Future.wait(futures);
for (var result in resultList) {
if (result is Map<String, dynamic>) {
tagsMap[result['idx'] as int] = result['etag'] as String;
}
}
futures.clear();
await randomAccessFile.close();
///
/// 7 合并
///
String location = await _merge(bxeApiService, objectKey, bucket, uploadId, tagsMap);
dio.close(force: true);
bxeApiService.close();
return 'https://pics-obs.banxiaoer.com${location.substring(9)}';
}
Future<File> _compressImage(File file) async {
try {
// 获取临时目录用于存储压缩后的图片
final tempDir = await getTemporaryDirectory();
final outputPath = '${tempDir.path}/compressed_${Uuid().v4()}.jpg';
// 构建 FFmpeg 压缩命令
// -i: 输入文件
// -vf scale: 设置缩放尺寸,宽度最大1280,高度自动按比例缩放
// -q:v: JPEG 质量参数 (2-31,数值越小质量越高),这里使用10表示较好的质量
// -y: 覆盖已存在的输出文件
String cmd = '-i "${file.path}" '
'-vf "scale=\'min(1280,iw)\':-2" ' // 宽度最大1280px,高度按比例,-2确保偶数值
'-q:v 10 ' // JPEG 质量设置为10,平衡质量和文件大小
'-y ' // 覆盖输出文件
'"$outputPath"';
// 执行 FFmpeg 命令
final session = await FFmpegKit.execute(cmd);
final returnCode = await session.getReturnCode();
// 检查执行结果
if (ReturnCode.isSuccess(returnCode)) {
File compressedFile = File(outputPath);
// 验证压缩后的文件是否存在
if (await compressedFile.exists()) {
final originalSize = await file.length();
final compressedSize = await compressedFile.length();
print('图片压缩成功: 原始大小 ${originalSize} bytes -> 压缩后 ${compressedSize} bytes');
return compressedFile;
} else {
throw Exception('压缩后的文件不存在');
}
} else {
// 压缩失败,打印错误信息并返回原文件
final output = await session.getOutput();
print('FFmpeg 压缩失败: $output');
throw Exception('图片压缩失败');
}
} catch (e) {
print('图片压缩出错: $e');
// 如果压缩失败,返回原文件
return file;
}
}
///
/// 规则:http://wiki.zbuku.cn/confluence/pages/viewpage.action?pageId=137172780
///
String _getLoginPrefix() {
var now = DateTime.now();
var year = now.year;
var month = now.month;
var day = now.day;
String obsLogicPrefix = "d2";
if (EnvConfig.env == 'pro') {
obsLogicPrefix = "p2";
}
obsLogicPrefix = '$obsLogicPrefix/pridel/user/';
String busiCode = 'app_avatar';
String userCode = getIt.get<SharedPreferences>().getString('auth_userCode') ?? 'na';
String classCode = getIt.get<SharedPreferences>().getString('auth_classCode') ?? 'nac';
return '$obsLogicPrefix$year$month$day/app/$classCode/$busiCode/$userCode';
}
static const _signatureNewUrl = '/api/v1/obs/multipart/signaturenew';
static const _signatureNextUrl = '/api/v1/obs/multipart/signaturenext';
static const _completeUrl = '/api/v1/obs/multipart/complete';
/// 初始化,请求后端获取签名信息和上传任务ID
Future<Map<String, dynamic>> _init(ApiService bxeApiService, String objectKey, String bucket) async {
var endpoint = '$_signatureNewUrl?objectKey=$objectKey&bucket=$bucket';
final resp = await bxeApiService.get(endpoint);
return resp.data;
}
/// 每次上传前,请求后端获取签名信息
Future<Map<String, dynamic>> _next(
ApiService bxeApiService,
String objectKey,
String bucket,
String uploadId,
int partNum,
) async {
var endpoint = '$_signatureNextUrl?objectKey=$objectKey&bucket=$bucket&uploadId=$uploadId&partNum=$partNum';
final resp = await bxeApiService.get(endpoint);
return resp.data;
}
/// 上传段,按照最大重试次数进行上传重试
Future<Map<String, dynamic>> _uploadChunkWithRetry(
Dio dio,
String signUrl,
int chunkIndex,
Uint8List chunk, {
int maxRetries = 3,
}) async {
//print('====================> 分片$chunkIndex , 开始上传 ${DateTime.now()}');
for (int attempt = 0; attempt <= maxRetries; attempt++) {
try {
var starTime = DateTime.now();
final resp = await _uploadChunk(dio, signUrl, chunk, chunkIndex);
var endTime = DateTime.now();
if (resp.statusCode == 200) {
print(
'====================> 分片$chunkIndex${attempt + 1}次, $endTime 上传耗时:${endTime.millisecondsSinceEpoch - starTime.millisecondsSinceEpoch} 毫秒');
final etags = resp.headers['etag'] as List<String>;
return Future.value({'idx': chunkIndex + 1, 'etag': etags[0]}); // 上传成功
} else {
throw Exception('Chunk $chunkIndex upload failed: ${resp.statusCode}');
}
} catch (e) {
print('====================> 分片$chunkIndex${attempt + 1}次, 上传失败:${e.toString()}');
if (attempt == maxRetries) {
throw Exception('Chunk $chunkIndex upload failed after $maxRetries attempts: $e');
}
// 等待后重试
await Future.delayed(Duration(seconds: 2 * attempt));
}
}
throw Exception('上传失败');
}
/// 上传段
Future<Response> _uploadChunk(Dio dio, String signUrl, Uint8List chunk, int chunkIndex) async {
var url = signUrl.replaceFirst('AWSAccessKeyId=', 'AccessKeyId=').replaceFirst(':443', '');
try {
// Response response = await _put(url, chunk);
print('====================> 分片$chunkIndex , 开始上传 ${DateTime.now()}');
final response = await dio.put(
url,
// data: Stream.fromIterable(chunk.map((e) => [e])),
// data: Stream.fromIterable([chunk]),
data: chunk,
);
print('====================> 分片$chunkIndex , 上传成功 ${DateTime.now()}');
return response;
} catch (e) {
throw Exception('Chunk upload failed: $e');
}
}
/// 请求合并文件
Future<String> _merge(
ApiService bxeApiService,
String objectKey,
String bucket,
String uploadId,
Map<int, String> tagsMap,
) async {
final parts = [];
for (int i = 1; i <= tagsMap.length; i++) {
parts.add({'partNumber': i, 'etag': tagsMap[i]});
}
final response = await bxeApiService.post(_completeUrl, {
'objectKey': objectKey,
'bucket': bucket,
'uploadId': uploadId,
'parts': parts,
});
if (response.statusCode != 200) {
throw Exception('合并文件失败');
}
return response.data["location"];
}
@override
Future<void> close() async {
try {
_nameController.dispose();
} catch (e) {
print(e);
}
try {
_nickNameController.dispose();
} catch (e) {
print(e);
}
await super.close();
}
}
......@@ -53,7 +53,7 @@ class Constant {
static const String appVersion = EnvConfig.version;
/// H5的起始终最低版本号规则
static String h5Version = '0.0.0';
static String h5Version = '0.1.6';
/// H5的版本号存储的key
static const String h5VersionKey = 'h5_version';
......
......@@ -9,8 +9,10 @@ import 'package:appframe/ui/pages/login_phone_page.dart';
import 'package:appframe/ui/pages/login_qr_page.dart';
import 'package:appframe/ui/pages/reload_page.dart';
import 'package:appframe/ui/pages/scan_code_page.dart';
import 'package:appframe/ui/pages/setting/account_logoff_page.dart';
import 'package:appframe/ui/pages/setting/account_page.dart';
import 'package:appframe/ui/pages/setting/account_phone_page.dart';
import 'package:appframe/ui/pages/setting/account_user_page.dart';
import 'package:appframe/ui/pages/web_page.dart';
import 'package:appframe/ui/widgets/ios_edge_swipe_detector.dart';
import 'package:flutter/material.dart';
......@@ -69,6 +71,18 @@ final GoRouter router = GoRouter(
},
),
GoRoute(
path: '/account/user',
builder: (BuildContext context, GoRouterState state) {
return const AccountUserPage();
},
),
GoRoute(
path: '/account/logoff',
builder: (BuildContext context, GoRouterState state) {
return const AccountLogoffPage();
},
),
GoRoute(
path: '/adv',
builder: (BuildContext context, GoRouterState state) {
return const AdvPage();
......
......@@ -102,4 +102,33 @@ class PhoneAuthRepository {
);
return resp.data;
}
Future<dynamic> unbindPhone(String phone) async {
Response resp = await _appService.post(
'/api/v1/comm/phone/unbind',
{
"phone": phone,
},
);
return resp.data;
}
///
/// {
/// "code": 0,
/// "error": "操作成功"
/// }
///
Future<dynamic> updateUser(String userid, String name, String nickName, String avatar) async {
Response resp = await _appService.post(
'/api/v1/comm/user/update',
{
"userid": userid,
"name": name,
"nickName": nickName,
"avatar": avatar,
},
);
return resp.data;
}
}
import 'package:appframe/bloc/setting/account_logoff_cubit.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class AccountLogoffPage extends StatelessWidget {
const AccountLogoffPage({super.key});
@override
Widget build(BuildContext context) {
final Map<String, dynamic>? extraData = GoRouterState.of(context).extra as Map<String, dynamic>?;
var phone = extraData?['phone'] ?? '';
return BlocProvider(
create: (context) => AccountLogoffCubit(AccountLogoffState(phone: phone)),
child: BlocConsumer<AccountLogoffCubit, AccountLogoffState>(
builder: (context, state) {
final accountLogoffCubit = context.read<AccountLogoffCubit>();
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text('注销用户', style: TextStyle(color: Colors.white, fontSize: 18)),
centerTitle: true,
backgroundColor: Color(0xFF7691FA),
iconTheme: IconThemeData(
color: Colors.white,
),
),
body: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(60),
child: Column(
children: [
SizedBox(height: 60),
Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
color: Color(0xFFFFF3F3),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Color(0xFFFFCDD2)),
),
child: Row(
children: [
Icon(
Icons.warning_amber_rounded,
color: Color(0xFFE74C3C),
size: 24,
),
SizedBox(width: 12),
Expanded(
child: Text(
'注销后,您的所有数据将被永久删除且无法恢复,请谨慎操作!',
style: TextStyle(
fontSize: 14,
color: Color(0xFFE74C3C),
height: 1.5,
),
),
),
],
),
),
SizedBox(height: 20),
SizedBox(
width: double.infinity,
height: 47,
child: ElevatedButton(
onPressed: state.isLoading
? null
: () {
accountLogoffCubit.logoff();
},
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFFE74C3C),
foregroundColor: Colors.white,
textStyle: TextStyle(fontSize: 19),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(23.5),
),
disabledBackgroundColor: Color(0xFFCCCCCC),
),
child: state.isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Text(
'确认注销',
style: TextStyle(
fontSize: 19,
fontWeight: FontWeight.w400,
color: Color(0xFFFFFFFF),
),
strutStyle: StrutStyle(height: 22 / 19),
),
),
),
],
),
),
),
);
},
listener: (context, state) {
if (state.showSnackBar) {
_showTip(context, state.snackBarMsg);
}
},
),
);
}
void _showTip(BuildContext context, String tip) {
OverlayEntry overlayEntry = OverlayEntry(
builder: (context) => Positioned(
top: 200,
left: 20,
right: 20,
child: Material(
color: Colors.transparent,
child: Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(8),
),
child: Text(
tip,
style: TextStyle(color: Colors.white),
textAlign: TextAlign.center,
),
),
),
),
);
Overlay.of(context).insert(overlayEntry);
Future.delayed(Duration(seconds: 2), () {
overlayEntry.remove();
});
}
}
......@@ -60,6 +60,27 @@ class AccountPage extends StatelessWidget {
borderRadius: BorderRadius.circular(6),
),
child: ListTile(
leading: Icon(Icons.person),
title: Text('用户信息设置'),
subtitle: Text(
'点击设置用户信息',
style: TextStyle(
fontSize: 14.0,
color: Colors.grey,
),
),
trailing: Icon(Icons.arrow_forward_ios, size: 14),
onTap: () {
context.read<AccountCubit>().goSetUserInfo();
},
),
),
Card(
color: Color(0xFFF7F9FF),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
child: ListTile(
leading: Icon(Icons.mobile_friendly),
title: Text('手机号绑定'),
subtitle: Text(
......@@ -85,6 +106,12 @@ class AccountPage extends StatelessWidget {
height: 47,
child: ElevatedButton(
onPressed: () async {
// 判断是否有绑定手机号
if (state.phone != '') {
context.read<AccountCubit>().goLogoff();
return;
}
final accountCubit = context.read<AccountCubit>();
bool? confirm = await showDialog<bool>(
context: context,
......@@ -111,11 +138,17 @@ class AccountPage extends StatelessWidget {
);
if (confirm == true) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('已申请注销,等待流程处理'),
backgroundColor: Colors.green,
),
);
accountCubit.unbind();
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF7691FA),
backgroundColor: Color(0xFFE74C3C),
foregroundColor: Colors.white,
textStyle: TextStyle(fontSize: 19),
shape: RoundedRectangleBorder(
......
......@@ -70,155 +70,157 @@ class AccountPhonePage extends StatelessWidget {
)),
]),
)
: Padding(
padding: EdgeInsets.all(60),
child: Column(
children: [
SizedBox(height: 120),
Container(
height: 60,
decoration: BoxDecoration(
color: Color(0xFFF7F9FF),
borderRadius: BorderRadius.circular(10),
),
child: TextField(
controller: accountPhoneCubit.phoneController,
keyboardType: TextInputType.phone,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(11), // 使用这个来限制长度
FilteringTextInputFormatter.allow(RegExp(r'^1[0-9]{0,10}$')),
],
decoration: InputDecoration(
hintText: '请输入手机号',
hintStyle: TextStyle(
fontSize: 18,
color: Color(0xFFCCCCCC),
),
border: InputBorder.none,
contentPadding: EdgeInsets.fromLTRB(0, 22, 0, 19),
// 左右内边距,垂直居中
prefixIcon: Container(
margin: EdgeInsets.only(left: 15), // 控制左边距
child: Image.asset(
'assets/images/login_v2/phone_small.png',
width: 25.5,
height: 25.5,
fit: BoxFit.contain,
: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(60),
child: Column(
children: [
SizedBox(height: 120),
Container(
height: 60,
decoration: BoxDecoration(
color: Color(0xFFF7F9FF),
borderRadius: BorderRadius.circular(10),
),
child: TextField(
controller: accountPhoneCubit.phoneController,
keyboardType: TextInputType.phone,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(11), // 使用这个来限制长度
FilteringTextInputFormatter.allow(RegExp(r'^1[0-9]{0,10}$')),
],
decoration: InputDecoration(
hintText: '请输入手机号',
hintStyle: TextStyle(
fontSize: 18,
color: Color(0xFFCCCCCC),
),
border: InputBorder.none,
contentPadding: EdgeInsets.fromLTRB(0, 22, 0, 19),
// 左右内边距,垂直居中
prefixIcon: Container(
margin: EdgeInsets.only(left: 15), // 控制左边距
child: Image.asset(
'assets/images/login_v2/phone_small.png',
width: 25.5,
height: 25.5,
fit: BoxFit.contain,
),
),
),
),
style: TextStyle(
fontSize: 18,
color: Color(0xFF000000),
style: TextStyle(
fontSize: 18,
color: Color(0xFF000000),
),
),
),
),
SizedBox(height: 20),
Container(
height: 60,
decoration: BoxDecoration(
color: Color(0xFFF7F9FF),
borderRadius: BorderRadius.circular(10),
),
child: Stack(
children: [
TextField(
controller: accountPhoneCubit.codeController,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(4), // 使用这个来限制长度
],
decoration: InputDecoration(
hintText: '请输入验证码',
hintStyle: TextStyle(
fontSize: 18,
color: Color(0xFFCCCCCC),
),
border: InputBorder.none,
contentPadding: EdgeInsets.fromLTRB(0, 22, 0, 19),
// 左右内边距,垂直居中
prefixIcon: Container(
margin: EdgeInsets.only(left: 15), // 控制左边距
child: Image.asset(
'assets/images/login_v2/secure_small.png',
width: 25.5,
height: 25.5,
fit: BoxFit.contain,
SizedBox(height: 20),
Container(
height: 60,
decoration: BoxDecoration(
color: Color(0xFFF7F9FF),
borderRadius: BorderRadius.circular(10),
),
child: Stack(
children: [
TextField(
controller: accountPhoneCubit.codeController,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(4), // 使用这个来限制长度
],
decoration: InputDecoration(
hintText: '请输入验证码',
hintStyle: TextStyle(
fontSize: 18,
color: Color(0xFFCCCCCC),
),
border: InputBorder.none,
contentPadding: EdgeInsets.fromLTRB(0, 22, 0, 19),
// 左右内边距,垂直居中
prefixIcon: Container(
margin: EdgeInsets.only(left: 15), // 控制左边距
child: Image.asset(
'assets/images/login_v2/secure_small.png',
width: 25.5,
height: 25.5,
fit: BoxFit.contain,
),
),
),
),
style: TextStyle(
fontSize: 18,
color: Color(0xFF000000),
),
),
Positioned(
right: 0,
top: 0,
bottom: 0,
child: Container(
width: 100,
decoration: BoxDecoration(
color: Colors.transparent,
style: TextStyle(
fontSize: 18,
color: Color(0xFF000000),
),
child: TextButton(
onPressed: () {
if (state.allowSend) {
accountPhoneCubit.sendVerificationCode();
}
},
style: TextButton.styleFrom(
backgroundColor: Colors.transparent,
// foregroundColor: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: EdgeInsets.zero,
),
Positioned(
right: 0,
top: 0,
bottom: 0,
child: Container(
width: 100,
decoration: BoxDecoration(
color: Colors.transparent,
),
child: Text(
state.allowSend ? '发送验证码' : '${state.seconds}s后可重发',
style: TextStyle(
fontSize: 16,
color: Color(0xFF7691FA),
fontWeight: FontWeight.normal,
child: TextButton(
onPressed: () {
if (state.allowSend) {
accountPhoneCubit.sendVerificationCode();
}
},
style: TextButton.styleFrom(
backgroundColor: Colors.transparent,
// foregroundColor: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: EdgeInsets.zero,
),
child: Text(
state.allowSend ? '发送验证码' : '${state.seconds}s后可重发',
style: TextStyle(
fontSize: 16,
color: Color(0xFF7691FA),
fontWeight: FontWeight.normal,
),
),
),
),
),
),
],
],
),
),
),
SizedBox(height: 20),
SizedBox(
width: double.infinity,
height: 47,
child: ElevatedButton(
onPressed: () {
context.read<AccountPhoneCubit>().bind();
},
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF7691FA),
foregroundColor: Colors.white,
textStyle: TextStyle(fontSize: 19),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(23.5),
SizedBox(height: 20),
SizedBox(
width: double.infinity,
height: 47,
child: ElevatedButton(
onPressed: () {
context.read<AccountPhoneCubit>().bind();
},
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF7691FA),
foregroundColor: Colors.white,
textStyle: TextStyle(fontSize: 19),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(23.5),
),
),
),
child: Text(
'绑定手机号',
style: TextStyle(
fontSize: 19,
fontWeight: FontWeight.w400,
color: Color(0xFFFFFFFF),
child: Text(
'绑定手机号',
style: TextStyle(
fontSize: 19,
fontWeight: FontWeight.w400,
color: Color(0xFFFFFFFF),
),
strutStyle: StrutStyle(height: 22 / 19),
),
strutStyle: StrutStyle(height: 22 / 19),
),
),
),
],
],
),
),
),
);
......
import 'dart:io';
import 'package:appframe/bloc/setting/account_user_cubit.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
import 'package:wechat_camera_picker/wechat_camera_picker.dart';
class AccountUserPage extends StatelessWidget {
const AccountUserPage({super.key});
@override
Widget build(BuildContext context) {
final Map<String, dynamic>? extraData = GoRouterState.of(context).extra as Map<String, dynamic>?;
var name = extraData?['name'] ?? '';
var nickname = extraData?['nickname'] ?? '';
var avatar = extraData?['avatar'] ?? '';
return BlocProvider(
create: (context) => AccountUserCubit(AccountUserState(name: name, nickname: nickname, avatar: avatar)),
child: BlocConsumer<AccountUserCubit, AccountUserState>(
builder: (context, state) {
final accountUserCubit = context.read<AccountUserCubit>();
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text('设置用户信息', style: TextStyle(color: Colors.white, fontSize: 18)),
centerTitle: true,
backgroundColor: Color(0xFF7691FA),
iconTheme: IconThemeData(
color: Colors.white,
),
),
body: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(20),
child: Column(
children: [
SizedBox(height: 40),
GestureDetector(
onTap: () => _pickImage(context, accountUserCubit),
child: Stack(
children: [
CircleAvatar(
radius: 50,
backgroundColor: Color(0xFFF7F9FF),
backgroundImage: state.avatar.isNotEmpty
? (state.avatar.startsWith('http')
? NetworkImage(state.avatar)
: FileImage(File(state.avatar))) as ImageProvider
: null,
child: state.avatar.isEmpty
? Icon(
Icons.person,
size: 50,
color: Color(0xFFCCCCCC),
)
: null,
),
Positioned(
right: 0,
bottom: 0,
child: Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: Color(0xFF7691FA),
shape: BoxShape.circle,
),
child: Icon(
Icons.camera_alt,
color: Colors.white,
size: 16,
),
),
),
],
),
),
SizedBox(height: 20),
Text(
'点击更换头像',
style: TextStyle(
fontSize: 14,
color: Color(0xFF999999),
),
),
SizedBox(height: 40),
_buildInfoItem(
context,
icon: Icons.person_outline,
label: '姓名',
controller: accountUserCubit.nameController,
hintText: '请输入姓名',
),
SizedBox(height: 20),
_buildInfoItem(
context,
icon: Icons.badge_outlined,
label: '昵称',
controller: accountUserCubit.nickNameController,
hintText: '请输入昵称',
),
SizedBox(height: 60),
SizedBox(
width: double.infinity,
height: 47,
child: ElevatedButton(
onPressed: state.isLoading
? null
: () {
accountUserCubit.save();
},
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF7691FA),
foregroundColor: Colors.white,
textStyle: TextStyle(fontSize: 19),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(23.5),
),
disabledBackgroundColor: Color(0xFFCCCCCC),
),
child: state.isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Text(
'保存',
style: TextStyle(
fontSize: 19,
fontWeight: FontWeight.w400,
color: Color(0xFFFFFFFF),
),
strutStyle: StrutStyle(height: 22 / 19),
),
),
),
],
),
),
),
);
},
listener: (context, state) {
if (state.showSnackBar) {
_showTip(context, state.snackBarMsg);
}
},
),
);
}
Widget _buildInfoItem(
BuildContext context, {
required IconData icon,
required String label,
required TextEditingController controller,
required String hintText,
}) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 15),
decoration: BoxDecoration(
color: Color(0xFFF7F9FF),
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: [
Icon(
icon,
color: Color(0xFF7691FA),
size: 24,
),
SizedBox(width: 15),
Text(
label,
style: TextStyle(
fontSize: 16,
color: Color(0xFF333333),
fontWeight: FontWeight.w500,
),
),
SizedBox(width: 20),
Expanded(
child: TextField(
controller: controller,
decoration: InputDecoration(
hintText: hintText,
hintStyle: TextStyle(
fontSize: 16,
color: Color(0xFFCCCCCC),
),
border: InputBorder.none,
isDense: true,
contentPadding: EdgeInsets.zero,
),
style: TextStyle(
fontSize: 16,
color: Color(0xFF000000),
),
textAlign: TextAlign.center,
),
),
],
),
);
}
Future<void> _pickImage(BuildContext context, AccountUserCubit cubit) async {
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return SafeArea(
child: Wrap(
children: [
ListTile(
leading: Icon(Icons.photo_camera),
title: Text('拍照'),
onTap: () async {
Navigator.pop(context);
AssetEntity? asset = await CameraPicker.pickFromCamera(
context,
pickerConfig: const CameraPickerConfig(),
);
if (asset != null) {
cubit.updateAvatar((await asset.file)!.path);
}
},
),
ListTile(
leading: Icon(Icons.photo_library),
title: Text('从相册选择'),
onTap: () async {
Navigator.pop(context);
List<AssetEntity>? result = await AssetPicker.pickAssets(
context,
pickerConfig: AssetPickerConfig(
maxAssets: 1,
requestType: RequestType.image,
dragToSelect: false,
),
);
if (result != null) {
cubit.updateAvatar((await result.first.file)!.path);
}
},
),
ListTile(
leading: Icon(Icons.cancel),
title: Text('取消'),
onTap: () {
Navigator.pop(context);
},
),
],
),
);
},
);
}
void _showTip(BuildContext context, String tip) {
OverlayEntry overlayEntry = OverlayEntry(
builder: (context) => Positioned(
top: 200,
left: 20,
right: 20,
child: Material(
color: Colors.transparent,
child: Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(8),
),
child: Text(
tip,
style: TextStyle(color: Colors.white),
textAlign: TextAlign.center,
),
),
),
),
);
Overlay.of(context).insert(overlayEntry);
Future.delayed(Duration(seconds: 2), () {
overlayEntry.remove();
});
}
}
......@@ -316,25 +316,21 @@ class WebPage extends StatelessWidget {
child: Column(
children: [
ListTile(
leading: const Icon(Icons.cleaning_services, size: 20),
title: const Text('清理缓存', style: TextStyle(fontSize: 14)),
onTap: () {
Navigator.pop(ctx);
_showClearCacheDialog(ctx);
},
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
dense: true,
visualDensity: VisualDensity.compact,
),
Divider(height: 1, thickness: 0.5, indent: 16, endIndent: 16),
ListTile(
leading: const Icon(Icons.logout, size: 20),
title: const Text('退出登录', style: TextStyle(fontSize: 14)),
leading: const Icon(Icons.headset_mic, size: 20),
title: const Text('在线客服', style: TextStyle(fontSize: 14)),
onTap: () {
Navigator.pop(ctx);
ctx.read<WebCubit>().logout();
router.push(
'/link',
extra: {
'url':
'https://yuanqi.tencent.com/webim/#/chat/DKfyFo?appid=1970738784338535872&experience=true',
'title': '在线客服'
},
);
},
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
trailing: Icon(Icons.arrow_forward_ios, size: 14),
dense: true,
visualDensity: VisualDensity.compact,
),
......@@ -350,20 +346,56 @@ class WebPage extends StatelessWidget {
child: Column(
children: [
ListTile(
leading: const Icon(Icons.timeline, size: 20),
title: const Text('切换日志模式', style: TextStyle(fontSize: 14)),
leading: const Icon(Icons.cleaning_services, size: 20),
title: const Text('清理缓存', style: TextStyle(fontSize: 14)),
onTap: () {
Navigator.pop(ctx);
_showClearCacheDialog(ctx);
},
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
dense: true,
visualDensity: VisualDensity.compact,
),
Divider(height: 1, thickness: 0.5, indent: 16, endIndent: 16),
ListTile(
leading: const Icon(Icons.logout, size: 20),
title: const Text('退出登录', style: TextStyle(fontSize: 14)),
onTap: () {
Navigator.pop(ctx);
ctx.read<WebCubit>().handleToggleDebug();
ctx.read<WebCubit>().logout();
},
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
// trailing: Icon(Icons.arrow_forward_ios, size: 14),
dense: true,
visualDensity: VisualDensity.compact, // 视觉密度设为紧凑
visualDensity: VisualDensity.compact,
),
],
),
),
SizedBox(height: 8),
EnvConfig.isDev()
? Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 2),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
child: Column(
children: [
ListTile(
leading: const Icon(Icons.timeline, size: 20),
title: const Text('切换日志模式', style: TextStyle(fontSize: 14)),
onTap: () {
Navigator.pop(ctx);
ctx.read<WebCubit>().handleToggleDebug();
},
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
// trailing: Icon(Icons.arrow_forward_ios, size: 14),
dense: true,
visualDensity: VisualDensity.compact, // 视觉密度设为紧凑
),
],
),
)
: SizedBox(),
],
),
),
......
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!