Commit fc5a9772 by tanghuan

手机号绑定设置

1 parent 6dfc85c0
...@@ -105,7 +105,7 @@ class LoginPhoneCubit extends Cubit<LoginPhoneState> { ...@@ -105,7 +105,7 @@ class LoginPhoneCubit extends Cubit<LoginPhoneState> {
} }
// 发送验证码 // 发送验证码
var result = await _phoneAuthRepository.verifyCode(phone); var result = await _phoneAuthRepository.verifyCode(phone, 0);
if (result['code'] != 0) { if (result['code'] != 0) {
emit(state.copyWith(showSnackBar: true, snackBarMsg: result['error'])); emit(state.copyWith(showSnackBar: true, snackBarMsg: result['error']));
emit(state.copyWith(showSnackBar: false)); emit(state.copyWith(showSnackBar: false));
...@@ -143,6 +143,7 @@ class LoginPhoneCubit extends Cubit<LoginPhoneState> { ...@@ -143,6 +143,7 @@ class LoginPhoneCubit extends Cubit<LoginPhoneState> {
var result = await _phoneAuthRepository.login(phone, verifyCode); var result = await _phoneAuthRepository.login(phone, verifyCode);
if (result['code'] != 0) { if (result['code'] != 0) {
emit(state.copyWith(showSnackBar: true, snackBarMsg: result['error'])); emit(state.copyWith(showSnackBar: true, snackBarMsg: result['error']));
emit(state.copyWith(showSnackBar: false));
return; return;
} }
......
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_bloc/flutter_bloc.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AccountState extends Equatable {
final bool loaded;
final String name;
final String phone;
final String nickname;
final String imgIcon;
const AccountState({
this.loaded = false,
this.name = '',
this.phone = '',
this.nickname = '',
this.imgIcon = '',
});
AccountState copyWith({
bool? loaded,
String? name,
String? phone,
String? nickname,
String? imgIcon,
}) {
return AccountState(
loaded: loaded ?? this.loaded,
name: name ?? this.name,
phone: phone ?? this.phone,
nickname: nickname ?? this.nickname,
imgIcon: imgIcon ?? this.imgIcon,
);
}
@override
List<Object?> get props => [
loaded,
name,
phone,
nickname,
imgIcon,
];
}
class AccountCubit extends Cubit<AccountState> {
late final PhoneAuthRepository _phoneAuthRepository;
AccountCubit(super.initialState) {
_phoneAuthRepository = getIt.get<PhoneAuthRepository>();
init();
}
Future<void> init() async {
var sharedPreferences = getIt.get<SharedPreferences>();
var userCode = sharedPreferences.getString('auth_userCode') ?? '';
try {
var result = await _phoneAuthRepository.bindCheck(userCode);
var code = result['code'];
var data = result['data'];
if (code != 0) {
return;
}
emit(
state.copyWith(
loaded: true,
name: data['name'],
phone: data['phone'],
nickname: data['nickname'],
imgIcon: data['imgIcon'],
),
);
} catch (e) {
print(e);
}
}
Future<void> goBind() async {
String? result = await router.push(
'/account/phone',
extra: {
'phone': state.phone,
},
);
if (result != null && result.isNotEmpty) {
emit(state.copyWith(phone: result));
}
}
}
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 AccountPhoneState extends Equatable {
final String phone;
final bool showSnackBar;
final String snackBarMsg;
final bool allowSend;
final int seconds;
const AccountPhoneState({
this.phone = '',
this.showSnackBar = false,
this.snackBarMsg = '',
this.allowSend = true,
this.seconds = 0,
});
AccountPhoneState copyWith({
String? phone,
bool? showSnackBar,
String? snackBarMsg,
bool? allowSend,
int? seconds,
}) {
return AccountPhoneState(
phone: phone ?? this.phone,
showSnackBar: showSnackBar ?? this.showSnackBar,
snackBarMsg: snackBarMsg ?? this.snackBarMsg,
allowSend: allowSend ?? this.allowSend,
seconds: seconds ?? this.seconds,
);
}
@override
List<Object?> get props => [
phone,
showSnackBar,
snackBarMsg,
allowSend,
seconds,
];
}
class AccountPhoneCubit extends Cubit<AccountPhoneState> {
late TextEditingController _phoneController;
late TextEditingController _codeController;
Timer? _timer;
int countdown = 60; // 倒计时秒数
late final PhoneAuthRepository _phoneAuthRepository;
TextEditingController get phoneController => _phoneController;
TextEditingController get codeController => _codeController;
AccountPhoneCubit(super.initialState) {
_phoneController = TextEditingController();
_codeController = TextEditingController();
_phoneController.text = '';
_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) {
// 验证手机号码
String phone = _phoneController.text;
if (!RegExp(r'^1[3-9][0-9]{9}$').hasMatch(phone)) {
emit(state.copyWith(showSnackBar: true, snackBarMsg: '请输入正确的手机号码'));
emit(state.copyWith(showSnackBar: false));
return;
}
// 发送验证码
var result = await _phoneAuthRepository.verifyCode(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> bind() async {
String phone = _phoneController.text;
String verifyCode = _codeController.text;
if (!RegExp(r'^1[3-9][0-9]{9}$').hasMatch(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;
}
var sharedPreferences = getIt.get<SharedPreferences>();
var userCode = sharedPreferences.getString('auth_userCode') ?? '';
var result = await _phoneAuthRepository.bind(userCode, phone, verifyCode);
if (result['code'] != 0) {
emit(state.copyWith(showSnackBar: true, snackBarMsg: result['error']));
emit(state.copyWith(showSnackBar: false));
return;
}
// 绑定成功,返回手机号码
router.pop(phone);
}
@override
Future<void> close() async {
try {
_phoneController.dispose();
} catch (e) {
print(e);
}
try {
_codeController.dispose();
} catch (e) {
print(e);
}
await super.close();
}
}
...@@ -485,6 +485,10 @@ class WebCubit extends Cubit<WebState> { ...@@ -485,6 +485,10 @@ class WebCubit extends Cubit<WebState> {
_controller.reload(); _controller.reload();
} }
void goAccount() {
router.push('/account');
}
Future<void> clearStorage() async { Future<void> clearStorage() async {
// 1 清理 localStorage // 1 清理 localStorage
_controller.clearLocalStorage(); _controller.clearLocalStorage();
......
...@@ -6,6 +6,8 @@ import 'package:appframe/ui/pages/login_phone_page.dart'; ...@@ -6,6 +6,8 @@ import 'package:appframe/ui/pages/login_phone_page.dart';
import 'package:appframe/ui/pages/login_qr_page.dart'; import 'package:appframe/ui/pages/login_qr_page.dart';
import 'package:appframe/ui/pages/reload_page.dart'; import 'package:appframe/ui/pages/reload_page.dart';
import 'package:appframe/ui/pages/scan_code_page.dart'; import 'package:appframe/ui/pages/scan_code_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/web_page.dart'; import 'package:appframe/ui/pages/web_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
...@@ -50,6 +52,18 @@ final GoRouter router = GoRouter( ...@@ -50,6 +52,18 @@ final GoRouter router = GoRouter(
}, },
), ),
GoRoute( GoRoute(
path: '/account',
builder: (BuildContext context, GoRouterState state) {
return const AccountPage();
},
),
GoRoute(
path: '/account/phone',
builder: (BuildContext context, GoRouterState state) {
return const AccountPhonePage();
},
),
GoRoute(
path: '/adv', path: '/adv',
builder: (BuildContext context, GoRouterState state) { builder: (BuildContext context, GoRouterState state) {
return const AdvPage(); return const AdvPage();
......
...@@ -12,6 +12,28 @@ class PhoneAuthRepository { ...@@ -12,6 +12,28 @@ class PhoneAuthRepository {
/// ///
/// { /// {
/// "code": 0, /// "code": 0,
/// "data": {
/// "unionId": "", //用户码
/// "name": "", //用户姓名
/// "phone": "", //用户手机号
/// "nickname": "", //用户昵称
/// "imgIcon": "https://xxxxx", //用户头像
/// }
/// }
///
Future<dynamic> bindCheck(String userid) async {
Response resp = await _appService.get(
'/api/v1/comm/phone/bind/check',
queryParameters: {
"userid": userid,
},
);
return resp.data;
}
///
/// {
/// "code": 0,
/// "error": "绑定成功" /// "error": "绑定成功"
/// } /// }
/// ///
...@@ -33,12 +55,15 @@ class PhoneAuthRepository { ...@@ -33,12 +55,15 @@ class PhoneAuthRepository {
/// "error": "获取成功", /// "error": "获取成功",
/// } /// }
/// ///
Future<dynamic> verifyCode(String phone) async { /// type=1 绑定申请验证码
/// type=0 验证码登陆下发
///
Future<dynamic> verifyCode(String phone, int type) async {
Response resp = await _appService.get( Response resp = await _appService.get(
'/api/v1/comm/phone/verifycode', '/api/v1/comm/phone/verifycode',
queryParameters: { queryParameters: {
"phone": phone, "phone": phone,
"type": 1, "type": type,
}, },
); );
return resp.data; return resp.data;
......
...@@ -29,7 +29,6 @@ class PlayerService { ...@@ -29,7 +29,6 @@ class PlayerService {
try { try {
final player = FlutterSoundPlayer(); final player = FlutterSoundPlayer();
_player = (await player.openPlayer())!; _player = (await player.openPlayer())!;
await _player!.setSpeed(1); // 播放速度,默认1
// 播放进度回调 // 播放进度回调
_player!.setSubscriptionDuration(Duration(seconds: 1)); _player!.setSubscriptionDuration(Duration(seconds: 1));
...@@ -54,7 +53,7 @@ class PlayerService { ...@@ -54,7 +53,7 @@ class PlayerService {
} }
} }
Future<bool> playAudio(String url, int seek, String playId) async { Future<bool> playAudio(String url, int seek, String playId, double playRate) async {
if (!(_playerIsInit ?? false)) { if (!(_playerIsInit ?? false)) {
final initResult = await _initPlayer(); final initResult = await _initPlayer();
if (!initResult) { if (!initResult) {
...@@ -64,6 +63,9 @@ class PlayerService { ...@@ -64,6 +63,9 @@ class PlayerService {
_playId = playId; _playId = playId;
// 播放速度
await _player!.setSpeed(playRate);
await _player!.startPlayer( await _player!.startPlayer(
fromURI: url, fromURI: url,
whenFinished: () async { whenFinished: () async {
...@@ -107,7 +109,7 @@ class PlayerService { ...@@ -107,7 +109,7 @@ class PlayerService {
return true; return true;
} }
Future<bool> resumeAudio() async { Future<bool> resumeAudio(double? playRate) async {
if (!(_playerIsInit ?? false)) { if (!(_playerIsInit ?? false)) {
throw Exception("播放器未初始化"); throw Exception("播放器未初始化");
} }
...@@ -116,6 +118,10 @@ class PlayerService { ...@@ -116,6 +118,10 @@ class PlayerService {
throw Exception("播放器状态错误"); throw Exception("播放器状态错误");
} }
if (playRate != null) {
await _player!.setSpeed(playRate);
}
await _player!.resumePlayer(); await _player!.resumePlayer();
return true; return true;
} }
...@@ -129,6 +135,19 @@ class PlayerService { ...@@ -129,6 +135,19 @@ class PlayerService {
return true; return true;
} }
Future<bool> rateAudio(double playRate) async {
if (!(_playerIsInit ?? false)) {
throw Exception("播放器未初始化");
}
// await pauseAudio();
// await resumeAudio(playRate);
_player!.setSpeed(playRate);
return true;
}
Future<bool> stopAudio() async { Future<bool> stopAudio() async {
if (!(_playerIsInit ?? false)) { if (!(_playerIsInit ?? false)) {
throw Exception("播放器未初始化"); throw Exception("播放器未初始化");
......
import 'package:appframe/bloc/setting/account_cubit.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class AccountPage extends StatelessWidget {
const AccountPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => AccountCubit(AccountState()),
child: BlocConsumer<AccountCubit, AccountState>(
builder: (context, state) {
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: state.loaded
? Column(
children: [
// 用户头像和昵称部分
Container(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
CircleAvatar(
radius: 26.0,
backgroundImage: NetworkImage(state.imgIcon),
),
SizedBox(height: 16.0),
Text(
state.name,
style: TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.bold,
),
),
Text(
state.nickname,
style: TextStyle(fontSize: 12.0),
),
],
),
),
// 设置项列表
Expanded(
child: Padding(
padding: EdgeInsets.all(20),
child: ListView(
children: [
Card(
color: Color(0xFFF7F9FF),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
child: ListTile(
leading: Icon(Icons.mobile_friendly),
title: Text('手机号绑定'),
subtitle: Text(
state.phone != '' ? state.phone : '未绑定',
style: TextStyle(
fontSize: 14.0,
color: Colors.grey,
),
),
trailing: Icon(Icons.arrow_forward_ios),
onTap: () {
context.read<AccountCubit>().goBind();
},
),
),
// Divider(),
],
),
),
),
],
)
: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('加载中...'),
],
),
),
);
},
listener: (context, state) {},
),
);
}
}
import 'package:appframe/bloc/setting/account_phone_cubit.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class AccountPhonePage extends StatelessWidget {
const AccountPhonePage({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) => AccountPhoneCubit(AccountPhoneState(phone: phone)),
child: BlocConsumer<AccountPhoneCubit, AccountPhoneState>(
builder: (context, state) {
final accountPhoneCubit = context.read<AccountPhoneCubit>();
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: state.phone == ''
? Center(
child: Column(children: [
SizedBox(height: 120),
Text('已绑定手机号'),
Text(
state.phone,
style: TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.bold,
),
),
]),
)
: Padding(
padding: EdgeInsets.all(20),
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),
),
),
),
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,
),
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),
),
),
child: 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();
});
}
}
...@@ -77,13 +77,16 @@ class WebPage extends StatelessWidget { ...@@ -77,13 +77,16 @@ class WebPage extends StatelessWidget {
color: Color(0xFF7691FA), color: Color(0xFF7691FA),
), ),
child: Align( child: Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Text('设置', child: Text(
style: TextStyle( '设置',
// color: Theme.of(ctx).colorScheme.onPrimary, style: TextStyle(
fontSize: 24, // color: Theme.of(ctx).colorScheme.onPrimary,
color: Colors.white, fontSize: 24,
))), color: Colors.white,
),
),
),
), ),
Expanded( Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
...@@ -115,6 +118,14 @@ class WebPage extends StatelessWidget { ...@@ -115,6 +118,14 @@ class WebPage extends StatelessWidget {
}, },
),*/ ),*/
ListTile( ListTile(
leading: const Icon(Icons.lock),
title: const Text('账号与安全'),
onTap: () {
Navigator.pop(ctx);
ctx.read<WebCubit>().goAccount();
},
),
/*ListTile(
leading: const Icon(Icons.refresh), leading: const Icon(Icons.refresh),
title: const Text('刷新'), title: const Text('刷新'),
onTap: () { onTap: () {
...@@ -122,7 +133,7 @@ class WebPage extends StatelessWidget { ...@@ -122,7 +133,7 @@ class WebPage extends StatelessWidget {
// ctx.read<WebCubit>().refresh(); // ctx.read<WebCubit>().refresh();
ctx.read<WebCubit>().handleRefreshPage(); ctx.read<WebCubit>().handleRefreshPage();
}, },
), ),*/
ListTile( ListTile(
leading: const Icon(Icons.cleaning_services), leading: const Icon(Icons.cleaning_services),
title: const Text('清理缓存'), title: const Text('清理缓存'),
......
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!