初始化

This commit is contained in:
谢宇宁
2026-09-15 15:44:13 +07:00
commit a23ba685e9
1065 changed files with 101122 additions and 0 deletions
@@ -0,0 +1,73 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
import 'package:hgdj/tools_base/toast.dart';
import 'package:hgdj/tools_base/global_store/store.dart';
import '../../../tools_base/event_bus/event_bus_util.dart';
import '../../../tools_base/event_bus/events.dart';
import 'bing_phone_page.dart';
class BindPhoneLogic extends GetxController {
late final phoneCtr = TextEditingController();
late final codeCtr = TextEditingController();
late final countDownNof = ValueNotifier(-1);
Timer? _timer;
final PhonePageType pageType;
BindPhoneLogic(this.pageType);
@override
onClose() {
super.onClose();
_timer?.cancel();
}
onSendCode() async {
if (_timer != null) return;
if (phoneCtr.text.isEmpty || phoneCtr.text.length < 11) {
showToast('请输入正确的手机号~');
return;
}
final res = await MineService.postCaptchaCode(phoneCtr.text, pageType.type);
if (res) {
_timer?.cancel();
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
countDownNof.value += 1;
if (countDownNof.value == 60) {
_timer?.cancel();
_timer = null;
countDownNof.value = -1;
}
});
}
}
onBindPhone() async {
if (phoneCtr.text.isEmpty) {
showToast('请输入正确的手机号~');
return;
}
if (codeCtr.text.isEmpty) {
showToast('请输入验证码~');
return;
}
if (pageType == PhonePageType.bind) {
final res = await MineService.bindPhone(phoneCtr.text, codeCtr.text);
if (res) {
showToast('绑定成功');
globalStore.updateUserInfo();
Get.back();
}
} else {
final result =
await globalStore.loginByMobile(phoneCtr.text, codeCtr.text);
if (result != null) {
showToast('登录成功');
eventBus.emit(ReLoginEvent());
}
}
}
}
@@ -0,0 +1,164 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/app_colors.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'bind_phone_logic.dart';
enum PhonePageType {
bind('绑定手机', 1, '立即绑定'),
find('找回账号', 2, '立即找回');
final int type;
final String title;
final String buttonTitle;
const PhonePageType(this.title, this.type, this.buttonTitle);
}
class BindPhonePage extends StatelessWidget {
final PhonePageType pageType;
const BindPhonePage({super.key, this.pageType = PhonePageType.bind});
@override
Widget build(BuildContext context) {
return GetBuilder<BindPhoneLogic>(
init: BindPhoneLogic(pageType),
builder: (controller) => Scaffold(
appBar: AppBar(
title: Text(controller.pageType.title),
),
body: Padding(
padding: EdgeInsets.only(left: 16, right: 16, top: 47),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: double.infinity,
height: 42,
padding: EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
Text(
'手机号',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.9),
fontSize: 16),
),
12.sizeBoxW,
Expanded(
child: TextField(
controller: controller.phoneCtr,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.9),
fontSize: 14,
),
maxLength: 11,
decoration: InputDecoration(
border: InputBorder.none,
hintText: '请输入手机号码',
isCollapsed: true,
contentPadding: EdgeInsets.zero,
counterText: '',
hintStyle: TextStyle(
color: Colors.white.withValues(alpha: 0.5),
fontSize: 14)),
),
)
],
),
),
10.sizeBoxH,
Divider(
height: .5,
color: Colors.white.withValues(alpha: .05),
),
30.sizeBoxH,
Container(
width: double.infinity,
height: 42,
padding: EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
Text(
'验证码',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.9),
fontSize: 16),
),
12.sizeBoxW,
Expanded(
child: TextField(
controller: controller.codeCtr,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.9),
fontSize: 14,
),
maxLength: 6,
decoration: InputDecoration(
border: InputBorder.none,
hintText: '请输入手机验证码',
isCollapsed: true,
contentPadding: EdgeInsets.zero,
counterText: '',
hintStyle: TextStyle(
color: Colors.white.withValues(alpha: 0.5),
fontSize: 14)),
),
),
12.sizeBoxW,
GestureDetector(
onTap: () => controller.onSendCode(),
child: ValueListenableBuilder(
valueListenable: controller.countDownNof,
builder:
(BuildContext context, int value, Widget? child) {
final iscountdown = value > -1;
return Container(
padding: EdgeInsets.symmetric(
horizontal: 4, vertical: 4),
child: Text(
iscountdown ? '${60 - value}' : '获取验证码',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.5),
fontSize: 12),
),
);
},
),
)
],
),
),
10.sizeBoxH,
Divider(
height: .5,
color: Colors.white.withValues(alpha: .05),
),
30.sizeBoxH,
GestureDetector(
onTap: () => controller.onBindPhone(),
child: Container(
width: double.infinity,
height: 44,
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(3),
color: AppColors.actionRed,
),
child: Text(
controller.pageType.buttonTitle,
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w500),
),
),
)
],
),
),
),
);
}
}
@@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
import 'package:hgdj/tools_base/toast.dart';
import 'package:hgdj/tools_base/global_store/store.dart';
/// 绑定邀请码逻辑
class InviteBindLogic extends GetxController {
// 邀请码输入框
final codeCtr = TextEditingController();
/// 立即绑定
void bind() async {
if (codeCtr.text.isEmpty) {
showToast('请输入邀请码~');
return;
}
final res = await MineService.exchangeInviteCode(codeCtr.text);
if (res) {
showToast('绑定成功');
globalStore.updateUserInfo();
Get.back();
} else {
showToast('绑定失败');
}
}
@override
void onClose() {
codeCtr.dispose();
super.onClose();
}
}
@@ -0,0 +1,117 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/app_colors.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'invite_bind_logic.dart';
/// 绑定邀请码页
class InviteBindPage extends StatelessWidget {
const InviteBindPage({super.key});
@override
Widget build(BuildContext context) {
return GetBuilder<InviteBindLogic>(
init: InviteBindLogic(),
builder: (controller) => Scaffold(
appBar: AppBar(
title: Text('绑定邀请码'),
),
body: Padding(
padding: EdgeInsets.only(left: 28, right: 28, top: 28),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
Text(
'输入邀请码',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.9),
fontSize: 30,
fontWeight: FontWeight.w500),
),
12.sizeBoxH,
Text(
'邀请码只能绑定一次 且不能修改',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.35),
fontSize: 16,
fontWeight: FontWeight.w400,
),
),
30.sizeBoxH,
// 邀请码输入
SizedBox(
width: double.infinity,
height: 42,
child: Row(
children: [
Text(
'邀请码',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.9),
fontSize: 16),
),
24.sizeBoxW,
Expanded(
child: TextField(
controller: controller.codeCtr,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.9),
fontSize: 14,
),
maxLength: 11,
decoration: InputDecoration(
border: InputBorder.none,
hintText: '请输入邀请码(字母大写)',
isCollapsed: true,
contentPadding: EdgeInsets.zero,
counterText: '',
hintStyle: TextStyle(
color: Colors.white.withValues(alpha: 0.35),
fontSize: 14)),
),
)
],
),
),
10.sizeBoxH,
Divider(
height: .35,
color: Colors.white.withValues(alpha: .05),
),
18.sizeBoxH,
Text(
'邀请1人 获得1天会员',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.35), fontSize: 14),
),
30.sizeBoxH,
// 立即绑定按钮
GestureDetector(
onTap: controller.bind,
child: Container(
width: double.infinity,
height: 44,
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(3),
color: AppColors.actionRed,
),
child: Text(
'立即绑定',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w500),
),
),
)
],
),
),
),
);
}
}
@@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/hj_model/mine/exchange_record_model.dart';
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
import 'package:hgdj/tools_base/toast.dart';
import 'package:hgdj/tools_base/debug_log.dart';
import 'package:hgdj/tools_base/global_store/store.dart';
import 'package:hgdj/tools_base/loading/loading_alert_widget.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
class MineExchangeCodeLogic extends GetxController {
final int type; //0-填写邀请码 1-填写兑换码
TextEditingController controller = TextEditingController();
List<ExchangeRecordModel> groupList = [];
bool isLoadingHistoryData = true;
RefreshController? refreshController; //由 pullYsRefresh 的 onInit 注入,组件负责释放
int currentPage = 1;
MineExchangeCodeLogic({this.type = 0});
@override
onReady() {
super.onReady();
if (type == 1) loadData();
}
loadData({int page = 1}) async {
try {
final res = await MineService.getExchangeRecord(page, 20);
if (res != null && res.data != null) {
if (page == 1) groupList.clear();
currentPage = page;
groupList.addAll(res.data ?? []);
}
refreshController?.refreshCompleted();
(res?.total ?? 0) > (res?.data?.length ?? 0)
? refreshController?.loadComplete()
: refreshController?.loadNoData();
} catch (e) {
refreshController?.refreshCompleted();
refreshController?.loadComplete();
debugLog(e);
}
update();
isLoadingHistoryData = false;
}
loadMoreData() => loadData(page: currentPage + 1);
void onExChangeCode() async {
if (controller.text.isEmpty) {
showToast('请输入${type == 0 ? '邀请码' : '兑换码'}');
return;
}
if (type == 1) {
try {
LoadingAlertWidget.show();
bool ret = await MineService.postExchangeCode(controller.text);
LoadingAlertWidget.cancel();
if (ret == true) {
controller.text = "";
globalStore.updateUserInfo();
showToast("兑换成功");
loadData();
}
} catch (e) {
LoadingAlertWidget.cancel();
debugLog(e);
}
} else if (type == 0) {
try {
LoadingAlertWidget.show();
bool ret = await MineService.getProxyBind(controller.text);
LoadingAlertWidget.cancel();
if (ret == true) {
globalStore.meInfo?.inviterCode = controller.text;
globalStore.updateUserInfo();
showToast("绑定成功");
Get.back(result: true);
}
} catch (e) {
LoadingAlertWidget.cancel();
debugLog(e);
}
}
}
}
@@ -0,0 +1,250 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/app_colors.dart';
import 'package:hgdj/extension/extensions.dart';
import 'package:hgdj/hj_model/mine/exchange_record_model.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
import 'package:hgdj/tools_base/refresh/pull_refresh.dart';
import 'mine_code_logic.dart';
//邀请码和兑换码
class MineExchangeCodePage extends StatefulWidget {
final int type; //0-填写邀请码 1-填写兑换码
const MineExchangeCodePage({super.key, this.type = 0});
@override
State<MineExchangeCodePage> createState() => _MineExchangeCodePageState();
}
class _MineExchangeCodePageState extends State<MineExchangeCodePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.type == 0 ? '邀请码' : '领取兑换')),
body: GetBuilder<MineExchangeCodeLogic>(
init: MineExchangeCodeLogic(type: widget.type),
builder: (_) => Padding(
padding: EdgeInsets.symmetric(vertical: 28, horizontal: 16),
child: Column(
children: [
Expanded(
child: pullYsRefresh(
onInit: (controller) => _.refreshController = controller,
onRefresh: (controller) => _.loadData(),
onLoading: (controller) => _.loadMoreData(),
child: CustomScrollView(
keyboardDismissBehavior:
ScrollViewKeyboardDismissBehavior.onDrag,
slivers: <Widget>[
SliverToBoxAdapter(
child: _buildContent(_),
),
if (widget.type == 1) ...[
_buildHistoryTable(_),
]
],
),
),
),
InkWell(
enableFeedback: false,
onTap: () => _.onExChangeCode(),
child: Container(
height: 44,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(3)),
color: AppColors.actionRed,
),
child: Center(
child: Text(
"立即兑换",
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
),
),
),
],
)),
),
);
}
Widget _buildHistoryTable(MineExchangeCodeLogic logic) {
if (logic.isLoadingHistoryData) {
return SliverToBoxAdapter(child: LoadingCenterWidget());
} else if (logic.groupList.isEmpty) {
return SliverToBoxAdapter(
child: CErrorWidget(errorMsg: "暂无兑换记录"),
);
} else {
return SliverList.separated(
itemBuilder: (context, index) {
return _buildListItem(logic.groupList[index]);
},
itemCount: logic.groupList.length,
separatorBuilder: (context, index) {
return Divider(
height: 1,
color: Colors.black87.withValues(alpha: 0.1),
);
},
);
}
}
_buildContent(MineExchangeCodeLogic _) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"输入兑换码",
style: TextStyle(
color: Colors.white.withValues(alpha: .9),
fontWeight: FontWeight.w500,
fontSize: 30,
),
),
12.sizeBoxH,
Text(
"每个兑换码只能输入一次",
style: TextStyle(
color: Color(0xff525252),
fontWeight: FontWeight.w500,
fontSize: 16,
),
),
56.sizeBoxH,
Container(
height: 42,
child: Row(
children: [
Text(
"兑换码",
style: TextStyle(
color: Colors.white.withValues(alpha: .9),
fontWeight: FontWeight.w500,
fontSize: 14,
),
),
16.sizeBoxW,
Expanded(
child: TextField(
keyboardType: TextInputType.text,
autofocus: true,
autocorrect: true,
cursorColor: Colors.white,
textAlign: TextAlign.left,
controller: _.controller,
style: TextStyle(color: Colors.white, fontSize: 14),
decoration: InputDecoration(
hintText: '请输入${widget.type == 0 ? '邀请码' : '兑换码'}(字母大写)',
hintStyle: TextStyle(color: Color(0xff434c55)),
border: InputBorder.none,
),
),
)
],
)),
12.sizeBoxH,
1.line,
18.sizeBoxH,
Text(
"官方社群领取更多福利",
style: TextStyle(
color: Color(0xff525252),
fontWeight: FontWeight.w500,
fontSize: 12,
),
),
36.sizeBoxH,
Text(
"兑换记录",
style: TextStyle(
color: Colors.white.withValues(alpha: .9),
fontWeight: FontWeight.w600,
fontSize: 18,
),
),
12.sizeBoxH,
Row(
children: [
Expanded(child: _buildSectionItem('兑换码')),
5.sizeBoxW,
Expanded(child: _buildSectionItem('兑换类型')),
5.sizeBoxW,
Expanded(child: _buildSectionItem('兑换时间')),
],
),
],
);
}
_buildSectionItem(String title) {
return Container(
alignment: Alignment.center,
height: 42,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: .05),
),
child: Text(
title,
style: TextStyle(
color: Colors.white.withValues(alpha: .9),
fontWeight: FontWeight.w400,
fontSize: 14,
),
),
);
}
Widget _buildListItem(ExchangeRecordModel item) {
return Container(
height: 44,
child: Row(
children: [
Flexible(
child: Container(
height: 44,
alignment: Alignment.center,
child: Text(
item.code ?? '',
style: TextStyle(
color: Colors.white.withValues(alpha: .55), fontSize: 14),
),
),
),
Flexible(
child: Container(
height: 44,
alignment: Alignment.center,
child: Text(
item.desc ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white.withValues(alpha: .55), fontSize: 14),
),
),
),
Flexible(
child: Container(
height: 44,
alignment: Alignment.center,
child: Text(
'${item.createdAt.utcToYMD(gap: '.')}',
style: TextStyle(
color: Colors.white.withValues(alpha: .55), fontSize: 14),
),
),
),
],
),
);
}
}
@@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/hj_utils/screen.dart';
import '../../../hj_utils/permission_util.dart';
import '../../../routers/jump_router.dart';
import 'bing_phone_page.dart';
import 'mine_scan_login_page.dart';
class MineFindAccountPage extends StatelessWidget {
late final dataSource = [
{
'title': '手机号找回',
'ontap': () =>
Get.to(() => const BindPhonePage(pageType: PhonePageType.find)),
},
{
'title': '凭证找回',
'ontap': () async {
if (await PermissionUtil.checkCameraPermission()) {
Get.to(() => const MineScanLoginPage());
}
}
},
{
'title': '联系客服',
'ontap': () => pushToCustomService(),
}
];
MineFindAccountPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(elevation: 0, title: Text('找回账号')),
body: Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: .05),
borderRadius: BorderRadius.circular(8),
),
child: ListView.separated(
physics: NeverScrollableScrollPhysics(),
itemCount: dataSource.length,
shrinkWrap: true,
separatorBuilder: (_, __) => Divider(
height: 0.5,
color: Colors.white.withValues(alpha: .1),
),
itemBuilder: (BuildContext context, int index) {
final data = dataSource[index];
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: data['ontap'] as Function()?,
child: Column(
children: [
16.sizeBoxH,
Row(
children: [
Text(
(data['title'] ?? '').toString(),
style: TextStyle(
fontSize: 14,
color: Colors.white.withValues(alpha: 0.9)),
),
Spacer(),
Icon(
Icons.arrow_forward_ios_rounded,
size: 14,
color: Color(0xFFDCDCDC),
)
],
),
16.sizeBoxH,
],
),
);
},
),
)
],
),
),
);
}
}
@@ -0,0 +1,39 @@
import 'package:get/get.dart';
import 'package:hgdj/hj_page/splash/splash_page.dart';
import 'package:hgdj/tools_base/global_store/store.dart';
import 'package:hgdj/tools_base/toast.dart';
/// 手势密码用途:设置 / 关闭 / 进 app 解锁校验
enum MinePasswordType { setting, close, check }
class MinePasswordLogic extends GetxController {
//本页是中间件 redirect 的目标(RouteSettings 只能带 arguments),没法改构造传参;
//用 is 判断而不是隐式强转:路由栈里读到别的页的 arguments 时只降级不抛 TypeError
final MinePasswordType type = _typeFromArgs();
static MinePasswordType _typeFromArgs() {
final args = Get.arguments;
return args is MinePasswordType ? args : MinePasswordType.setting;
}
bool get isCheck => type == MinePasswordType.check;
String get title => isCheck ? '请输入解锁密码' : '请绘制锁屏图形';
/// 画完手势:设置/关闭成功即退出本页,解锁通过则回启动页重走流程
Future<void> onComplete(List<int> result) async {
if (result.length < 4) {
showToast('请至少链接4个点');
return;
}
switch (type) {
case MinePasswordType.setting:
if (await globalStore.setLockPassword(result) == true) Get.back();
case MinePasswordType.close:
if (await globalStore.closeLockPassword(result) == true) Get.back();
case MinePasswordType.check:
if (await globalStore.checkLockPassword(result) == true)
Get.toNamed(SplashPage.routeName);
}
}
}
@@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/hj_utils/screen.dart';
import '../mine_gesture_pwd/widget/gesture_password_widget.dart';
import 'mine_password_logic.dart';
/// 手势密码页:设置 / 关闭 / 解锁校验共用,用途由路由 arguments 传 [MinePasswordType]
class MinePasswordPage extends StatelessWidget {
static const routeName = '/MinePasswordPage';
const MinePasswordPage({super.key});
@override
Widget build(BuildContext context) {
return GetBuilder<MinePasswordLogic>(
init: MinePasswordLogic(),
//必须 falsetype 来自路由 arguments,全局注册会让「关闭」复用上一次「设置/解锁」残留的 logic
global: false,
builder: (logic) => Scaffold(
//解锁校验时还进不了 app,不给返回入口
appBar: logic.isCheck ? null : AppBar(title: const Text('手势密码')),
//必须给满宽约束:Column 横向是按最宽的子节点收缩的(这里 250),而 Scaffold 的 body
//是贴 (0,0) 摆放的,不撑满就整块贴左边;撑满后 Column 默认的居中对齐才真正生效
body: SizedBox(
width: double.infinity,
child: Column(
children: [
//没有 AppBar 顶着,用锁图标占位
if (logic.isCheck) ...[
65.sizeBoxH,
Image.asset('mine_lock_password.png'.mineImgPath, width: 29),
],
55.sizeBoxH,
Text(
logic.title,
style: TextStyle(
color: Colors.black.withValues(alpha: 0.9), fontSize: 18),
),
75.sizeBoxH,
GesturePasswordWidget(
lineColor: const Color(0xFFF52C56),
errorLineColor: const Color(0xffDD001B),
singleLineCount: 3,
identifySize: 60.0,
size: 250,
minLength: 4,
errorItem: Image.asset('error.webp'.mineImgPath,
color: const Color(0xFFF52C56)),
selectedItem: Image.asset('select.png'.mineImgPath,
color: const Color(0xFFF52C56)),
normalItem: Image.asset('normal.png'.mineImgPath),
onComplete: logic.onComplete,
),
],
),
),
),
);
}
}
@@ -0,0 +1,84 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:image_pickers/image_pickers.dart';
import 'package:hgdj/hj_utils/image_util.dart';
import 'package:hgdj/tools_base/event_bus/event_bus_util.dart';
import 'package:hgdj/tools_base/event_bus/events.dart';
import 'package:hgdj/tools_base/global_store/store.dart';
import 'package:hgdj/tools_base/loading/loading_helper.dart';
import 'package:hgdj/tools_base/net/net_manager.dart';
import 'package:hgdj/tools_base/toast.dart';
import 'package:qr_code_scanner/qr_code_scanner.dart';
import '../identity/mine_identity_page.dart';
class MineScanLginLogic extends GetxController {
QRViewController? scanController;
@override
void onClose() {
scanController?.dispose();
super.onClose();
}
onQRViewCreated(QRViewController controller) {
scanController = controller;
scanController?.scannedDataStream.listen((scanData) {
print('扫码结果:${scanData.code}');
loginByQrValue(scanData.code ?? '');
});
}
/// 开启本地相册
openNativePhoto() async {
// image_picker 走系统相册 intent,选图不需要存储权限,直接调起
final images = await ImagePickers.pickerPaths(
uiConfig: UIConfig(uiThemeColor: Colors.white),
selectCount: 1,
showCamera: false,
cropConfig: CropConfig(enableCrop: false),
);
if (images.isNotEmpty) {
final qrValue = await ImageUtil.decodeQr(images[0].path ?? '');
if (qrValue == null || qrValue.isEmpty) {
showToast('二维码错误~~');
return;
}
loginByQrValue(qrValue);
}
}
/// 是否正在处理一次扫码登录。扫码流会按摄像头帧连续 emit,pauseCamera 异步拦不住已缓冲的帧,
/// 不加锁会并发触发 N 次 loginByQr → token 被反复清空/轮换,服务端单会话把旧 token 全废,
/// 满屏 5009/5005「用户信息已经过期」,最终 token 落空、换号失败。
bool _isHandling = false;
/// 开始二维码登录
loginByQrValue(String qrValue) async {
if (_isHandling) return; // 一次扫码只处理一次,挡掉连续帧/重复触发
if (qrValue.isEmpty) {
showToast("二维码为空");
return;
}
_isHandling = true;
scanController?.pauseCamera();
LoadingHelper.showLoading();
var userInfo = await globalStore.loginByQr(qrValue);
LoadingHelper.dismissLoading();
// 必须拿到 token 才算成功:只有 uid 没 token 会留下空 token,下一个请求立刻被判过期
if (userInfo?.uid == null || (userInfo?.token ?? '').isEmpty) {
showToast("切换账号失败");
scanController?.resumeCamera();
_isHandling = false; // 失败放开,允许重试
return;
}
// 刷新ua
netManager.refreshUserAgent();
showToast('登录成功');
eventBus.emit(ReLoginEvent());
}
/// 跳转凭证
jumpToCertificate() => Get.to(MineAccountIdentityPage());
}
@@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:qr_code_scanner/qr_code_scanner.dart';
import 'mine_scan_login_logic.dart';
class MineScanLoginPage extends StatefulWidget {
const MineScanLoginPage({super.key});
@override
State<MineScanLoginPage> createState() => _MineScanLoginPageState();
}
class _MineScanLoginPageState extends State<MineScanLoginPage> {
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
@override
Widget build(BuildContext context) {
return GetBuilder<MineScanLginLogic>(
init: MineScanLginLogic(),
builder: (controller) => Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBar(
elevation: 0,
iconTheme: IconThemeData(color: Colors.white),
backgroundColor: Colors.transparent,
title: Text(
'扫码登录',
style: TextStyle(
color: Colors.white, fontSize: 18, fontWeight: FontWeight.w500),
),
),
body: Stack(
children: [
QRView(
key: qrKey,
onQRViewCreated: controller.onQRViewCreated,
overlay: QrScannerOverlayShape(
overlayColor: Colors.black,
borderColor: Colors.white,
borderRadius: 0,
borderLength: 20,
borderWidth: 5,
cutOutSize: 235),
),
Positioned(
bottom: 100,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: GestureDetector(
onTap: controller.openNativePhoto,
child: Column(
children: [
Image.asset('scan_photo.png'.mineImgPath, width: 48),
12.sizeBoxH,
Text(
'相册',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w500),
)
],
),
),
),
Expanded(
child: GestureDetector(
onTap: controller.jumpToCertificate,
child: Column(
children: [
Image.asset('mine_id.png'.mineImgPath, width: 48),
12.sizeBoxH,
Text(
'我的凭证',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w500),
)
],
),
),
),
],
),
)
],
),
),
);
}
}
@@ -0,0 +1,247 @@
import 'package:flutter/material.dart';
import 'package:hgdj/routers/jump_router.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/config/config.dart';
import 'package:hgdj/tools_base/toast.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/hj_utils/version_util.dart';
import 'package:hgdj/hj_utils/video_cache_manager.dart';
import 'package:hgdj/tools_base/cache/cache_util.dart';
import 'package:hgdj/tools_base/cache/image_cache_manager.dart';
import 'package:hgdj/tools_base/global_store/store.dart';
import 'package:hgdj/tools_base/image/image_data_handle/image_cache_disk.dart';
import 'package:hgdj/tools_base/video_download/video_download_manager.dart';
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
import 'package:provider/provider.dart';
import 'package:hgdj/tools_base/widget/common_alert.dart';
import '../../../alert/splash/update_dialog.dart';
import '../identity/mine_identity_page.dart';
import '../more_question/mine_qa_page.dart';
import 'bing_phone_page.dart';
import 'invite_bind_page.dart';
import 'mine_find_account_page.dart';
import 'mine_setting_profile_page.dart';
import 'setting_avatar_page.dart';
class MineSettingPage extends StatefulWidget {
const MineSettingPage({super.key});
@override
State<MineSettingPage> createState() => _MineSettingPageState();
}
class _MineSettingPageState extends State<MineSettingPage> {
String cacheSize = ''; // 缓存大小
@override
void initState() {
super.initState();
getCacheSize();
}
Future<void> getCacheSize() async {
try {
cacheSize = await loadCache();
setState(() {});
} catch (e) {
debugPrint(e.toString());
}
}
@override
Widget build(BuildContext context) {
final meInfo = context.watch<GlobalStore>().meInfo;
final mobile = meInfo?.mobile ?? '';
final inviterCode = meInfo?.inviterCode ?? '';
return Scaffold(
appBar: AppBar(title: Text('设置中心')),
body: Container(
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: .05),
borderRadius: BorderRadius.circular(8),
),
margin: EdgeInsets.only(
left: 10,
top: 12,
right: 10,
),
child: ListView(
children: [
_buildItem(
title: '头像',
avatarUrl: meInfo?.portrait ?? '',
onTap: () async {
if (!globalStore.isVIP) {
if (await CommonAlert.show(
content: '您还不是VIP无法修改头像',
subContent: '开通会员 即可解锁继续',
showCancel: false,
confirmText: '开通会员',
)) {
pushToWalletPage();
}
return;
}
Get.to(() => const SettingAvatarPage());
}),
_buildLine(),
_buildItem(
title: '昵称',
subTitle: meInfo?.name,
onTap: () async {
if (!globalStore.isVIP) {
if (await CommonAlert.show(
content: '您还不是VIP无法修改昵称!',
subContent: '开通会员 即可解锁继续',
showCancel: false,
confirmText: '开通会员',
)) {
pushToWalletPage();
}
return;
}
Get.to(() =>
const SettingProfilePage(SettingProfileType.nickName));
}),
_buildLine(),
_buildItem(
title: '${Config.appName}ID',
subTitle: meInfo?.uid.toString(),
showCopy: true,
onTap: () {}),
_buildLine(),
_buildItem(
title: '手机号码',
subTitle: mobile.isEmpty ? '立即绑定' : mobile,
onTap: () {
Get.to(() => const BindPhonePage());
}),
_buildLine(),
_buildItem(
title: '账号找回',
onTap: () {
Get.to(() => MineFindAccountPage());
}),
_buildLine(),
_buildItem(
title: '邀请码',
subTitle: inviterCode.isEmpty ? '未设置' : inviterCode,
onTap: () {
if (inviterCode.isEmpty) {
Get.to(() => const InviteBindPage());
} else {
showToast('您已绑定过邀请码');
}
}),
_buildLine(),
_buildItem(
title: '账号凭证',
onTap: () {
Get.to(() => MineAccountIdentityPage());
}),
_buildLine(),
_buildItem(
title: '常见问题',
onTap: () {
Get.to(() => MineQAPage());
}),
_buildLine(),
_buildItem(
title: '清除缓存',
subTitle: cacheSize,
onTap: () async {
await VideoDownloadManager.instance.emptyCache();
await ImageCacheDisk.emptyCache();
await VideoCacheManager().emptyCache();
await ImageCacheManager().emptyCache();
cacheSize = "0.0KB";
if (mounted) setState(() {});
showToast('清理缓存成功');
}),
_buildLine(),
_buildItem(
title: '检查更新',
subTitle: 'V${Config.innerVersion}',
onTap: () {
//比对启动页拉到的版本配置,确实有新版才弹更新框
final target = checkUpdate();
target == null
? showToast("当前已是最新版!")
: UpdateDialog.show(target);
}),
_buildLine(),
],
),
),
);
}
Widget _buildLine() {
return Padding(padding: EdgeInsets.only(left: 16), child: .5.line);
}
Widget _buildItem({
String title = '',
String? subTitle,
bool showCopy = false,
String? avatarUrl,
VoidCallback? onTap,
}) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: onTap,
child: Container(
height: 56,
padding: EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
Text(
title,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.9), fontSize: 14),
),
Spacer(),
if (avatarUrl != null)
NetworkImageLoader(
imageUrl: avatarUrl,
width: 24,
height: 24,
borderRadius: 12,
),
if (subTitle != null)
Text(
subTitle,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.45), fontSize: 12),
),
if (showCopy) ...[
4.sizeBoxW,
GestureDetector(
onTap: () {
Clipboard.setData(
ClipboardData(text: '${globalStore.meInfo?.uid}'));
showToast('复制成功');
},
child: Image.asset(
'mine_copy.png'.mineImgPath,
width: 24,
),
),
],
if (!showCopy) ...[
10.sizeBoxW,
Icon(
Icons.arrow_forward_ios_rounded,
size: 14,
color: Color(0xFF70708C),
),
]
],
),
),
);
}
}
@@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
import 'package:hgdj/tools_base/global_store/store.dart';
import 'package:hgdj/tools_base/loading/loading_helper.dart';
import 'package:hgdj/tools_base/toast.dart';
import 'mine_setting_profile_page.dart';
import 'setting_profile_widget.dart';
abstract class MineSettingProfilePage extends GetxController {
String get title => '';
late final nickNameTfCtr = TextEditingController();
late final sloganTfCtr = TextEditingController();
onSaveProfile();
instanceChildItem(int index);
@override
void onClose() {
nickNameTfCtr.dispose();
sloganTfCtr.dispose();
super.onClose();
}
}
class SettingNickNameController extends MineSettingProfilePage {
@override
String get title => '修改昵称';
@override
onSaveProfile() async {
if (nickNameTfCtr.text.isEmpty) {
showToast('昵称不能为空~');
return;
}
LoadingHelper.showLoading();
final res = await MineService.updateUserInfo({'name': nickNameTfCtr.text});
LoadingHelper.dismissLoading();
if (res) {
globalStore.updateUserInfo();
Get.back();
}
}
@override
instanceChildItem(int index) {
return SettingNickName();
}
}
class SettingSloganController extends MineSettingProfilePage {
@override
String get title => '个性签名';
@override
onSaveProfile() async {
if (sloganTfCtr.text.isEmpty) {
showToast('请输入个性签名~');
return;
}
LoadingHelper.showLoading();
final res = await MineService.updateUserInfo({'summary': sloganTfCtr.text});
LoadingHelper.dismissLoading();
if (res) {
globalStore.updateUserInfo();
Get.back();
}
}
@override
instanceChildItem(int index) {
return SettingSlogan();
}
}
MineSettingProfilePage instanceSettingProfileController(
SettingProfileType type) {
switch (type) {
case SettingProfileType.nickName:
return SettingNickNameController();
case SettingProfileType.slogan:
return SettingSloganController();
default:
throw '';
}
}
@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'mine_setting_profile_logic.dart';
enum SettingProfileType {
nickName,
slogan,
}
class SettingProfilePage extends StatelessWidget {
final SettingProfileType type;
const SettingProfilePage(this.type, {super.key});
@override
Widget build(BuildContext context) {
return GetBuilder<MineSettingProfilePage>(
init: instanceSettingProfileController(type),
builder: (controller) => Scaffold(
appBar: AppBar(
title: Text(controller.title),
actions: [
GestureDetector(
onTap: () => controller.onSaveProfile(),
child: Text(
'保存',
style: TextStyle(color: Color(0xff999999), fontSize: 14),
),
),
16.sizeBoxW
],
),
body: controller.instanceChildItem(0),
),
);
}
}
@@ -0,0 +1,49 @@
import 'package:get/get.dart';
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
import 'package:hgdj/tools_base/base_list_controller.dart';
import 'package:hgdj/tools_base/global_store/store.dart';
import 'package:hgdj/tools_base/toast.dart';
class SettingAvatarLogic extends ListBaseLogic<String> {
int selectIndex = -1;
late final int rawIndex;
@override
void onReady() {
super.onReady();
loadData();
}
void loadData() => fetchData(isRefresh: true, fetch: _fetch);
//头像列表无分页,单次拉取,hasNext 固定 false
Future<(List<String>?, bool)> _fetch(int page) async {
final res = await MineService.getPortrait();
selectIndex = res.indexOf(globalStore.meInfo?.portrait ?? '');
rawIndex = selectIndex;
return (res, false);
}
selectAavatar(int index) {
selectIndex = index;
update();
}
confirmChangeAvatar() async {
if (globalStore.meInfo?.urrPortraitStatus == 1) {
showToast('头像审核中~');
return;
}
if (selectIndex == rawIndex || selectIndex == -1) {
showToast('你都没选择,保存什么~');
return;
}
final res = await MineService.updateUserInfo(
{'portrait': dataList![selectIndex], 'isDefaultSource': true});
if (res) {
globalStore.updateUserInfo();
showToast('更新成功');
Get.back(result: true);
}
}
}
@@ -0,0 +1,101 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/tools_base/global_store/store.dart';
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
import 'package:provider/provider.dart';
import 'setting_avatar_logic.dart';
class SettingAvatarPage extends StatelessWidget {
const SettingAvatarPage({super.key});
@override
Widget build(BuildContext context) {
return GetBuilder<SettingAvatarLogic>(
init: SettingAvatarLogic(),
builder: (controller) => Scaffold(
appBar: AppBar(
title: Text('选择头像'),
actions: [
InkWell(
enableFeedback: false,
onTap: controller.confirmChangeAvatar,
child: Text(
'保存',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.55), fontSize: 12),
),
),
16.sizeBoxW,
],
),
body: () {
if (controller.isLoading) return LoadingCenterWidget();
if (controller.isEmptyData) return CErrorWidget();
final list = controller.dataList!;
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
30.verticalSpace,
NetworkImageLoader(
imageUrl: context.watch<GlobalStore>().meInfo?.portrait ?? '',
width: 90,
height: 90,
borderRadius: 45,
),
10.verticalSpace,
Text(
'当前头像',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.55), fontSize: 12),
),
20.verticalSpace,
Expanded(
child: GridView.builder(
padding: EdgeInsets.only(left: 38, right: 38, top: 0),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
mainAxisSpacing: 20,
crossAxisSpacing: 20,
childAspectRatio: 1,
),
itemCount: list.length,
itemBuilder: (BuildContext context, int index) {
final select = controller.selectIndex == index;
return GestureDetector(
onTap: () => controller.selectAavatar(index),
child: Stack(
children: [
NetworkImageLoader(
imageUrl: list[index],
width: double.infinity,
height: double.infinity,
borderRadius: 100,
),
if (select)
Align(
alignment: Alignment.bottomRight,
child: Image.asset(
'red_checked.png'.mineImgPath,
width: 22,
height: 22,
),
)
],
),
);
},
),
),
12.sizeBoxH,
],
);
}(),
),
);
}
}
@@ -0,0 +1,136 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/tools_base/global_store/store.dart';
import 'mine_setting_profile_logic.dart';
class SettingNickName extends StatefulWidget {
const SettingNickName({super.key});
@override
State<SettingNickName> createState() => _SettingNickNameState();
}
class _SettingNickNameState extends State<SettingNickName> {
late final controller = Get.find<MineSettingProfilePage>();
@override
void initState() {
super.initState();
controller.nickNameTfCtr.text = globalStore.meInfo?.name ?? '';
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
60.sizeBoxH,
Row(
children: [
Expanded(
child: TextField(
controller: controller.nickNameTfCtr,
maxLength: 20,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.9), fontSize: 18),
decoration: InputDecoration(
border: InputBorder.none,
hintText: '输入新的昵称',
hintStyle: TextStyle(
color: Colors.white.withValues(alpha: 0.35),
fontSize: 18),
counterText: '',
isCollapsed: true,
contentPadding: EdgeInsets.zero),
),
),
InkWell(
enableFeedback: false,
onTap: () => controller.nickNameTfCtr.clear(),
child: Container(
padding: EdgeInsets.all(2),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(90)),
child: Image.asset(
'close_button.png'.commonImgPath,
width: 24,
)),
)
],
),
12.sizeBoxH,
0.5.line,
12.sizeBoxH,
Text(
'注意*诱导性昵称会被投诉封号',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.35), fontSize: 12),
)
],
),
);
}
}
class SettingSlogan extends StatefulWidget {
const SettingSlogan({super.key});
@override
State<SettingSlogan> createState() => _SettingSloganState();
}
class _SettingSloganState extends State<SettingSlogan> {
late final controller = Get.find<MineSettingProfilePage>();
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(left: 16, top: 12, right: 16),
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: .04),
borderRadius: BorderRadius.circular(8)),
constraints: BoxConstraints(
minHeight: 111,
),
child: Stack(
children: [
TextField(
maxLines: 10,
style: const TextStyle(color: Color(0xff333333), fontSize: 12),
maxLength: 150,
controller: controller.sloganTfCtr,
decoration: InputDecoration(
border: InputBorder.none,
hintText: '有趣的介绍能让你的逼格提高N个档次!...',
hintStyle: TextStyle(color: Color(0xff999999), fontSize: 12),
counterText: '',
contentPadding: EdgeInsets.zero,
isDense: true),
),
Positioned(
bottom: 0,
right: 0,
// 字数计数:只监听不接管所有权(ChangeNotifierProvider(create:) 会把 ctr 一起 dispose
// 而它归 MineSettingProfilePage.onClose 释放 → 二次释放)
child: ValueListenableBuilder(
valueListenable: controller.sloganTfCtr,
builder: (_, value, __) => Text(
'${value.text.length}/150',
style: TextStyle(
color: Color(0xff666666),
fontSize: 12,
),
),
),
)
],
),
);
}
}