feat(payment-password): 添加支付密码功能(全栈)
后端(identity-service):
- Prisma schema 新增 paymentPasswordHash 字段及手动迁移脚本
- user-application.service 添加 4 个方法:isPaymentPasswordSet / setPaymentPassword /
changePaymentPassword / verifyPaymentPassword(纯新增,不修改已有逻辑)
- user-account.controller 新增 4 个端点:
GET /user/payment-password-status
POST /user/set-payment-password
POST /user/change-payment-password
POST /user/verify-payment-password → { valid: bool }
前端(mobile-app):
- account_service 新增 4 个方法对应后端 4 个接口
- 新建 change_payment_password_page.dart:6 位数字支付密码设置/修改页面
- 路由:RoutePaths / RouteNames / AppRouter 注册 /security/payment-password
- profile_page:'修改登录密码' 改为 '修改密码',下方新增 '支付密码' 入口
- PasswordVerifyDialog:新增 title / subtitle / hint 可选参数,支持登录/支付双模式
- planting_location_page:认种确认改为验证支付密码(verifyPaymentPassword)
- pre_planting_purchase_page:预种确认后追加支付密码验证步骤
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d91ff7b83a
commit
cad7ebe832
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable: 新增支付密码哈希字段
|
||||||
|
ALTER TABLE "user_accounts" ADD COLUMN "payment_password_hash" VARCHAR(100);
|
||||||
|
|
@ -17,7 +17,8 @@ model UserAccount {
|
||||||
email String? @unique @db.VarChar(100) // 绑定的邮箱地址
|
email String? @unique @db.VarChar(100) // 绑定的邮箱地址
|
||||||
emailVerified Boolean @default(false) @map("email_verified") // 邮箱是否验证
|
emailVerified Boolean @default(false) @map("email_verified") // 邮箱是否验证
|
||||||
emailVerifiedAt DateTime? @map("email_verified_at") // 邮箱验证时间
|
emailVerifiedAt DateTime? @map("email_verified_at") // 邮箱验证时间
|
||||||
passwordHash String? @map("password_hash") @db.VarChar(100) // bcrypt 哈希密码
|
passwordHash String? @map("password_hash") @db.VarChar(100) // bcrypt 哈希登录密码
|
||||||
|
paymentPasswordHash String? @map("payment_password_hash") @db.VarChar(100) // bcrypt 哈希支付密码
|
||||||
nickname String @db.VarChar(100)
|
nickname String @db.VarChar(100)
|
||||||
avatarUrl String? @map("avatar_url") @db.Text
|
avatarUrl String? @map("avatar_url") @db.Text
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -756,6 +756,77 @@ export class UserAccountController {
|
||||||
return { valid };
|
return { valid };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ 支付密码相关 ============
|
||||||
|
|
||||||
|
@Get('payment-password-status')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '查询支付密码状态',
|
||||||
|
description: '查询当前用户是否已设置支付密码',
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, description: '{ isSet: boolean }' })
|
||||||
|
async getPaymentPasswordStatus(@CurrentUser() user: CurrentUserData) {
|
||||||
|
const isSet = await this.userService.isPaymentPasswordSet(user.userId);
|
||||||
|
return { isSet };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('set-payment-password')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '设置支付密码',
|
||||||
|
description: '首次设置支付密码(无需旧密码)',
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, description: '设置成功' })
|
||||||
|
@ApiResponse({ status: 400, description: '支付密码已设置' })
|
||||||
|
async setPaymentPassword(
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
@Body() body: { password: string },
|
||||||
|
) {
|
||||||
|
await this.userService.setPaymentPassword({
|
||||||
|
userId: user.userId,
|
||||||
|
password: body.password,
|
||||||
|
});
|
||||||
|
return { message: '支付密码设置成功' };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('change-payment-password')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '修改支付密码',
|
||||||
|
description: '验证旧支付密码后修改为新支付密码',
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, description: '修改成功' })
|
||||||
|
@ApiResponse({ status: 400, description: '旧支付密码错误' })
|
||||||
|
async changePaymentPassword(
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
@Body() body: { oldPassword: string; newPassword: string },
|
||||||
|
) {
|
||||||
|
await this.userService.changePaymentPassword({
|
||||||
|
userId: user.userId,
|
||||||
|
oldPassword: body.oldPassword,
|
||||||
|
newPassword: body.newPassword,
|
||||||
|
});
|
||||||
|
return { message: '支付密码修改成功' };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('verify-payment-password')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '验证支付密码',
|
||||||
|
description: '验证用户的支付密码,用于认种/预种等支付操作二次验证',
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, description: '{ valid: boolean }' })
|
||||||
|
async verifyPaymentPassword(
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
@Body() body: { password: string },
|
||||||
|
) {
|
||||||
|
const valid = await this.userService.verifyPaymentPassword(
|
||||||
|
user.userId,
|
||||||
|
body.password,
|
||||||
|
);
|
||||||
|
return { valid };
|
||||||
|
}
|
||||||
|
|
||||||
@Get('users/resolve-address/:accountSequence')
|
@Get('users/resolve-address/:accountSequence')
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
|
|
|
||||||
|
|
@ -2856,6 +2856,106 @@ export class UserApplicationService {
|
||||||
return isValid;
|
return isValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ 支付密码相关 ============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询是否已设置支付密码
|
||||||
|
*/
|
||||||
|
async isPaymentPasswordSet(userId: string): Promise<boolean> {
|
||||||
|
const user = await this.prisma.userAccount.findUnique({
|
||||||
|
where: { userId: BigInt(userId) },
|
||||||
|
select: { paymentPasswordHash: true },
|
||||||
|
});
|
||||||
|
return !!user?.paymentPasswordHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置支付密码(首次设置,无需旧密码)
|
||||||
|
*/
|
||||||
|
async setPaymentPassword(command: {
|
||||||
|
userId: string;
|
||||||
|
password: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
this.logger.log(`Setting payment password for user: ${command.userId}`);
|
||||||
|
|
||||||
|
const user = await this.prisma.userAccount.findUnique({
|
||||||
|
where: { userId: BigInt(command.userId) },
|
||||||
|
select: { paymentPasswordHash: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user?.paymentPasswordHash) {
|
||||||
|
throw new ApplicationError('支付密码已设置,请使用修改支付密码功能');
|
||||||
|
}
|
||||||
|
|
||||||
|
const bcrypt = await import('bcrypt');
|
||||||
|
const paymentPasswordHash = await bcrypt.hash(command.password, 10);
|
||||||
|
|
||||||
|
await this.prisma.userAccount.update({
|
||||||
|
where: { userId: BigInt(command.userId) },
|
||||||
|
data: { paymentPasswordHash },
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`Payment password set for user: ${command.userId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改支付密码(需验证旧密码)
|
||||||
|
*/
|
||||||
|
async changePaymentPassword(command: {
|
||||||
|
userId: string;
|
||||||
|
oldPassword: string;
|
||||||
|
newPassword: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
this.logger.log(`Changing payment password for user: ${command.userId}`);
|
||||||
|
|
||||||
|
const user = await this.prisma.userAccount.findUnique({
|
||||||
|
where: { userId: BigInt(command.userId) },
|
||||||
|
select: { paymentPasswordHash: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user?.paymentPasswordHash) {
|
||||||
|
throw new ApplicationError('尚未设置支付密码,请先设置');
|
||||||
|
}
|
||||||
|
|
||||||
|
const bcrypt = await import('bcrypt');
|
||||||
|
const isOldValid = await bcrypt.compare(command.oldPassword, user.paymentPasswordHash);
|
||||||
|
if (!isOldValid) {
|
||||||
|
throw new ApplicationError('旧支付密码错误');
|
||||||
|
}
|
||||||
|
|
||||||
|
const paymentPasswordHash = await bcrypt.hash(command.newPassword, 10);
|
||||||
|
await this.prisma.userAccount.update({
|
||||||
|
where: { userId: BigInt(command.userId) },
|
||||||
|
data: { paymentPasswordHash },
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`Payment password changed for user: ${command.userId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证支付密码
|
||||||
|
*
|
||||||
|
* @returns 密码是否正确
|
||||||
|
*/
|
||||||
|
async verifyPaymentPassword(userId: string, password: string): Promise<boolean> {
|
||||||
|
this.logger.log(`Verifying payment password for user: ${userId}`);
|
||||||
|
|
||||||
|
const user = await this.prisma.userAccount.findUnique({
|
||||||
|
where: { userId: BigInt(userId) },
|
||||||
|
select: { paymentPasswordHash: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user?.paymentPasswordHash) {
|
||||||
|
throw new ApplicationError('请先设置支付密码');
|
||||||
|
}
|
||||||
|
|
||||||
|
const bcrypt = await import('bcrypt');
|
||||||
|
const isValid = await bcrypt.compare(password, user.paymentPasswordHash);
|
||||||
|
|
||||||
|
this.logger.log(`Payment password verification result for user ${userId}: ${isValid}`);
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 遮蔽手机号
|
* 遮蔽手机号
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -2124,6 +2124,93 @@ class AccountService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ 支付密码相关 ============
|
||||||
|
|
||||||
|
/// 查询是否已设置支付密码 (GET /user/payment-password-status)
|
||||||
|
///
|
||||||
|
/// 返回 true 表示已设置,false 表示未设置
|
||||||
|
Future<bool> isPaymentPasswordSet() async {
|
||||||
|
debugPrint('$_tag isPaymentPasswordSet() - 查询支付密码状态');
|
||||||
|
try {
|
||||||
|
final response = await _apiClient.get('/user/payment-password-status');
|
||||||
|
final isSet = response.data['isSet'] == true;
|
||||||
|
debugPrint('$_tag isPaymentPasswordSet() - 结果: $isSet');
|
||||||
|
return isSet;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('$_tag isPaymentPasswordSet() - 异常: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置支付密码(首次设置)(POST /user/set-payment-password)
|
||||||
|
///
|
||||||
|
/// [password] 支付密码明文(建议6位数字)
|
||||||
|
Future<void> setPaymentPassword(String password) async {
|
||||||
|
debugPrint('$_tag setPaymentPassword() - 开始设置支付密码');
|
||||||
|
try {
|
||||||
|
await _apiClient.post(
|
||||||
|
'/user/set-payment-password',
|
||||||
|
data: {'password': password},
|
||||||
|
);
|
||||||
|
debugPrint('$_tag setPaymentPassword() - 支付密码设置成功');
|
||||||
|
} on ApiException catch (e) {
|
||||||
|
debugPrint('$_tag setPaymentPassword() - API 异常: $e');
|
||||||
|
rethrow;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('$_tag setPaymentPassword() - 未知异常: $e');
|
||||||
|
throw ApiException('设置支付密码失败: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 修改支付密码 (POST /user/change-payment-password)
|
||||||
|
///
|
||||||
|
/// [oldPassword] 当前支付密码
|
||||||
|
/// [newPassword] 新支付密码
|
||||||
|
Future<void> changePaymentPassword({
|
||||||
|
required String oldPassword,
|
||||||
|
required String newPassword,
|
||||||
|
}) async {
|
||||||
|
debugPrint('$_tag changePaymentPassword() - 开始修改支付密码');
|
||||||
|
try {
|
||||||
|
await _apiClient.post(
|
||||||
|
'/user/change-payment-password',
|
||||||
|
data: {'oldPassword': oldPassword, 'newPassword': newPassword},
|
||||||
|
);
|
||||||
|
debugPrint('$_tag changePaymentPassword() - 支付密码修改成功');
|
||||||
|
} on ApiException catch (e) {
|
||||||
|
debugPrint('$_tag changePaymentPassword() - API 异常: $e');
|
||||||
|
rethrow;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('$_tag changePaymentPassword() - 未知异常: $e');
|
||||||
|
throw ApiException('修改支付密码失败: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 验证支付密码 (POST /user/verify-payment-password)
|
||||||
|
///
|
||||||
|
/// 后端响应格式:{ "valid": true/false }
|
||||||
|
/// - 返回 true:密码正确
|
||||||
|
/// - 返回 false:密码错误
|
||||||
|
/// - 抛出异常:未设置支付密码或网络错误
|
||||||
|
Future<bool> verifyPaymentPassword(String password) async {
|
||||||
|
debugPrint('$_tag verifyPaymentPassword() - 开始验证支付密码');
|
||||||
|
try {
|
||||||
|
final response = await _apiClient.post(
|
||||||
|
'/user/verify-payment-password',
|
||||||
|
data: {'password': password},
|
||||||
|
);
|
||||||
|
final valid = response.data['valid'] == true;
|
||||||
|
debugPrint('$_tag verifyPaymentPassword() - 验证结果: $valid');
|
||||||
|
return valid;
|
||||||
|
} on ApiException catch (e) {
|
||||||
|
debugPrint('$_tag verifyPaymentPassword() - 验证异常: $e');
|
||||||
|
rethrow;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('$_tag verifyPaymentPassword() - 未知异常: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============ 邮箱绑定相关 ============
|
// ============ 邮箱绑定相关 ============
|
||||||
|
|
||||||
/// 获取邮箱绑定状态
|
/// 获取邮箱绑定状态
|
||||||
|
|
|
||||||
|
|
@ -243,17 +243,20 @@ class _PlantingLocationPageState extends ConsumerState<PlantingLocationPage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 验证登录密码后再提交认种
|
/// 验证支付密码后再提交认种 [2026-03-05]
|
||||||
///
|
///
|
||||||
/// 插入在 [PlantingConfirmDialog] 确认与 [_submitPlanting] 之间:
|
/// 插入在 [PlantingConfirmDialog] 确认与 [_submitPlanting] 之间:
|
||||||
/// 弹出 [PasswordVerifyDialog] → 密码正确才调用 [_submitPlanting],
|
/// 弹出 [PasswordVerifyDialog](支付密码模式)→ 密码正确才调用 [_submitPlanting],
|
||||||
/// 取消或验证失败则停留在当前页,不触发任何提交逻辑。
|
/// 取消或验证失败则停留在当前页,不触发任何提交逻辑。
|
||||||
Future<void> _verifyPasswordThenSubmit() async {
|
Future<void> _verifyPasswordThenSubmit() async {
|
||||||
final verified = await PasswordVerifyDialog.show(
|
final verified = await PasswordVerifyDialog.show(
|
||||||
context: context,
|
context: context,
|
||||||
|
title: '验证支付密码',
|
||||||
|
subtitle: '为保障资金安全,请输入您的6位支付密码以确认认种',
|
||||||
|
hint: '请输入支付密码',
|
||||||
onVerify: (password) async {
|
onVerify: (password) async {
|
||||||
final accountService = ref.read(accountServiceProvider);
|
final accountService = ref.read(accountServiceProvider);
|
||||||
return await accountService.verifyLoginPassword(password);
|
return await accountService.verifyPaymentPassword(password);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (verified == true) {
|
if (verified == true) {
|
||||||
|
|
|
||||||
|
|
@ -7,28 +7,45 @@ import 'package:flutter/material.dart';
|
||||||
/// 抛出异常表示网络/系统错误(由弹窗捕获并展示提示)
|
/// 抛出异常表示网络/系统错误(由弹窗捕获并展示提示)
|
||||||
typedef PasswordVerifyCallback = Future<bool> Function(String password);
|
typedef PasswordVerifyCallback = Future<bool> Function(String password);
|
||||||
|
|
||||||
/// 认种密码校验弹窗
|
/// 密码校验弹窗(通用)
|
||||||
///
|
///
|
||||||
/// 在 [PlantingConfirmDialog] 用户点击"确认认种"之后、实际提交之前弹出,
|
/// 支持两种模式:
|
||||||
/// 要求用户输入登录密码进行二次身份验证,防止误操作或他人操作。
|
/// - 登录密码验证(认种/预种前身份核实)
|
||||||
|
/// - 支付密码验证(认种/预种支付二次确认)
|
||||||
///
|
///
|
||||||
/// 调用方式:
|
/// 调用方式:
|
||||||
/// ```dart
|
/// ```dart
|
||||||
|
/// // 验证支付密码
|
||||||
/// final verified = await PasswordVerifyDialog.show(
|
/// final verified = await PasswordVerifyDialog.show(
|
||||||
/// context: context,
|
/// context: context,
|
||||||
/// onVerify: (password) => accountService.verifyLoginPassword(password),
|
/// title: '验证支付密码',
|
||||||
|
/// subtitle: '请输入6位支付密码以确认认种',
|
||||||
|
/// hint: '请输入支付密码',
|
||||||
|
/// onVerify: (password) => accountService.verifyPaymentPassword(password),
|
||||||
/// );
|
/// );
|
||||||
/// if (verified == true) _submitPlanting();
|
/// if (verified == true) _submitPlanting();
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// 后端接口:POST /user/verify-password → { valid: bool }
|
/// 后端接口:POST /user/verify-payment-password → { valid: bool }
|
||||||
class PasswordVerifyDialog extends StatefulWidget {
|
class PasswordVerifyDialog extends StatefulWidget {
|
||||||
/// 密码校验回调,由调用方注入(通常调用 accountService.verifyLoginPassword)
|
/// 密码校验回调,由调用方注入
|
||||||
final PasswordVerifyCallback onVerify;
|
final PasswordVerifyCallback onVerify;
|
||||||
|
|
||||||
|
/// 弹窗标题,默认"验证登录密码"
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
/// 说明文字,默认"为保障账户安全,请输入您的登录密码以继续"
|
||||||
|
final String subtitle;
|
||||||
|
|
||||||
|
/// 输入框 hint,默认"请输入登录密码"
|
||||||
|
final String hint;
|
||||||
|
|
||||||
const PasswordVerifyDialog({
|
const PasswordVerifyDialog({
|
||||||
super.key,
|
super.key,
|
||||||
required this.onVerify,
|
required this.onVerify,
|
||||||
|
this.title = '验证登录密码',
|
||||||
|
this.subtitle = '为保障账户安全,请输入您的登录密码以继续',
|
||||||
|
this.hint = '请输入登录密码',
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 显示密码验证弹窗
|
/// 显示密码验证弹窗
|
||||||
|
|
@ -36,12 +53,20 @@ class PasswordVerifyDialog extends StatefulWidget {
|
||||||
static Future<bool?> show({
|
static Future<bool?> show({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
required PasswordVerifyCallback onVerify,
|
required PasswordVerifyCallback onVerify,
|
||||||
|
String title = '验证登录密码',
|
||||||
|
String subtitle = '为保障账户安全,请输入您的登录密码以继续',
|
||||||
|
String hint = '请输入登录密码',
|
||||||
}) {
|
}) {
|
||||||
return showDialog<bool>(
|
return showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
barrierColor: const Color(0x80000000),
|
barrierColor: const Color(0x80000000),
|
||||||
builder: (context) => PasswordVerifyDialog(onVerify: onVerify),
|
builder: (context) => PasswordVerifyDialog(
|
||||||
|
onVerify: onVerify,
|
||||||
|
title: title,
|
||||||
|
subtitle: subtitle,
|
||||||
|
hint: hint,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,7 +98,7 @@ class _PasswordVerifyDialogState extends State<PasswordVerifyDialog> {
|
||||||
Future<void> _handleConfirm() async {
|
Future<void> _handleConfirm() async {
|
||||||
final password = _passwordController.text.trim();
|
final password = _passwordController.text.trim();
|
||||||
if (password.isEmpty) {
|
if (password.isEmpty) {
|
||||||
setState(() => _errorMessage = '请输入登录密码');
|
setState(() => _errorMessage = '请输入${widget.hint.replaceAll('请输入', '')}');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -205,9 +230,9 @@ class _PasswordVerifyDialogState extends State<PasswordVerifyDialog> {
|
||||||
|
|
||||||
/// 构建标题
|
/// 构建标题
|
||||||
Widget _buildTitle() {
|
Widget _buildTitle() {
|
||||||
return const Text(
|
return Text(
|
||||||
'验证登录密码',
|
widget.title,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontFamily: 'Inter',
|
fontFamily: 'Inter',
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
|
|
@ -221,9 +246,9 @@ class _PasswordVerifyDialogState extends State<PasswordVerifyDialog> {
|
||||||
|
|
||||||
/// 构建说明文字
|
/// 构建说明文字
|
||||||
Widget _buildSubtitle() {
|
Widget _buildSubtitle() {
|
||||||
return const Text(
|
return Text(
|
||||||
'为保障账户安全,请输入您的登录密码以继续认种',
|
widget.subtitle,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontFamily: 'Inter',
|
fontFamily: 'Inter',
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
|
|
@ -267,7 +292,7 @@ class _PasswordVerifyDialogState extends State<PasswordVerifyDialog> {
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
hintText: '请输入登录密码',
|
hintText: widget.hint,
|
||||||
hintStyle: const TextStyle(
|
hintStyle: const TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontFamily: 'Inter',
|
fontFamily: 'Inter',
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import '../../../../core/di/injection_container.dart';
|
||||||
import '../../../../core/services/pre_planting_service.dart';
|
import '../../../../core/services/pre_planting_service.dart';
|
||||||
import '../../../../core/services/tree_pricing_service.dart';
|
import '../../../../core/services/tree_pricing_service.dart';
|
||||||
import '../../../../routes/route_paths.dart';
|
import '../../../../routes/route_paths.dart';
|
||||||
|
import '../../../planting/presentation/widgets/password_verify_dialog.dart'; // [2026-03-05] 支付密码验证
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// [2026-02-17] 预种计划购买页面
|
// [2026-02-17] 预种计划购买页面
|
||||||
|
|
@ -344,6 +345,19 @@ class _PrePlantingPurchasePageState
|
||||||
final confirmed = await _showConfirmDialog();
|
final confirmed = await _showConfirmDialog();
|
||||||
if (confirmed != true || !mounted) return;
|
if (confirmed != true || !mounted) return;
|
||||||
|
|
||||||
|
// 第三步:验证支付密码 [2026-03-05]
|
||||||
|
final verified = await PasswordVerifyDialog.show(
|
||||||
|
context: context,
|
||||||
|
title: '验证支付密码',
|
||||||
|
subtitle: '为保障资金安全,请输入您的6位支付密码以确认预种',
|
||||||
|
hint: '请输入支付密码',
|
||||||
|
onVerify: (password) async {
|
||||||
|
final accountService = ref.read(accountServiceProvider);
|
||||||
|
return await accountService.verifyPaymentPassword(password);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (verified != true || !mounted) return;
|
||||||
|
|
||||||
setState(() => _isPurchasing = true);
|
setState(() => _isPurchasing = true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1342,11 +1342,16 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
context.push(RoutePaths.googleAuth);
|
context.push(RoutePaths.googleAuth);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 修改密码
|
/// 修改登录密码
|
||||||
void _goToChangePassword() {
|
void _goToChangePassword() {
|
||||||
context.push(RoutePaths.changePassword);
|
context.push(RoutePaths.changePassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 支付密码设置/修改 [2026-03-05]
|
||||||
|
void _goToPaymentPassword() {
|
||||||
|
context.push(RoutePaths.paymentPassword);
|
||||||
|
}
|
||||||
|
|
||||||
/// 绑定邮箱
|
/// 绑定邮箱
|
||||||
void _goToBindEmail() {
|
void _goToBindEmail() {
|
||||||
context.push(RoutePaths.bindEmail);
|
context.push(RoutePaths.bindEmail);
|
||||||
|
|
@ -4206,9 +4211,14 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
// ),
|
// ),
|
||||||
_buildSettingItem(
|
_buildSettingItem(
|
||||||
icon: Icons.lock,
|
icon: Icons.lock,
|
||||||
title: '修改登录密码',
|
title: '修改密码',
|
||||||
onTap: _goToChangePassword,
|
onTap: _goToChangePassword,
|
||||||
),
|
),
|
||||||
|
_buildSettingItem(
|
||||||
|
icon: Icons.payment,
|
||||||
|
title: '支付密码',
|
||||||
|
onTap: _goToPaymentPassword,
|
||||||
|
),
|
||||||
_buildSettingItem(
|
_buildSettingItem(
|
||||||
icon: Icons.email,
|
icon: Icons.email,
|
||||||
title: '绑定邮箱',
|
title: '绑定邮箱',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,487 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import '../../../../core/di/injection_container.dart';
|
||||||
|
|
||||||
|
/// 支付密码设置/修改页面
|
||||||
|
///
|
||||||
|
/// - 未设置时:直接输入新密码(首次设置)
|
||||||
|
/// - 已设置时:需验证旧密码后修改
|
||||||
|
///
|
||||||
|
/// 支付密码格式:6位纯数字
|
||||||
|
class ChangePaymentPasswordPage extends ConsumerStatefulWidget {
|
||||||
|
const ChangePaymentPasswordPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<ChangePaymentPasswordPage> createState() =>
|
||||||
|
_ChangePaymentPasswordPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChangePaymentPasswordPageState
|
||||||
|
extends ConsumerState<ChangePaymentPasswordPage> {
|
||||||
|
final TextEditingController _oldPasswordController = TextEditingController();
|
||||||
|
final TextEditingController _newPasswordController = TextEditingController();
|
||||||
|
final TextEditingController _confirmPasswordController =
|
||||||
|
TextEditingController();
|
||||||
|
|
||||||
|
bool _showOldPassword = false;
|
||||||
|
bool _showNewPassword = false;
|
||||||
|
bool _showConfirmPassword = false;
|
||||||
|
bool _isSubmitting = false;
|
||||||
|
bool _hasPaymentPassword = false;
|
||||||
|
bool _isLoading = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_oldPasswordController.dispose();
|
||||||
|
_newPasswordController.dispose();
|
||||||
|
_confirmPasswordController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadStatus() async {
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
try {
|
||||||
|
final accountService = ref.read(accountServiceProvider);
|
||||||
|
final hasPassword = await accountService.isPaymentPasswordSet();
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_hasPaymentPassword = hasPassword;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_hasPaymentPassword = false;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 校验支付密码格式:6位纯数字
|
||||||
|
String? _validatePassword(String password) {
|
||||||
|
if (password.length != 6) return '支付密码必须为6位数字';
|
||||||
|
if (!RegExp(r'^\d{6}$').hasMatch(password)) return '支付密码只能包含数字';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _submit() async {
|
||||||
|
final oldPassword = _oldPasswordController.text;
|
||||||
|
final newPassword = _newPasswordController.text;
|
||||||
|
final confirmPassword = _confirmPasswordController.text;
|
||||||
|
|
||||||
|
if (_hasPaymentPassword && oldPassword.isEmpty) {
|
||||||
|
_showError('请输入当前支付密码');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (newPassword.isEmpty) {
|
||||||
|
_showError('请输入新支付密码');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final formatError = _validatePassword(newPassword);
|
||||||
|
if (formatError != null) {
|
||||||
|
_showError(formatError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (confirmPassword != newPassword) {
|
||||||
|
_showError('两次输入的支付密码不一致');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() => _isSubmitting = true);
|
||||||
|
try {
|
||||||
|
final accountService = ref.read(accountServiceProvider);
|
||||||
|
if (_hasPaymentPassword) {
|
||||||
|
await accountService.changePaymentPassword(
|
||||||
|
oldPassword: oldPassword,
|
||||||
|
newPassword: newPassword,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await accountService.setPaymentPassword(newPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _isSubmitting = false);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(_hasPaymentPassword ? '支付密码修改成功' : '支付密码设置成功'),
|
||||||
|
backgroundColor: const Color(0xFF4CAF50),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
context.pop(true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _isSubmitting = false);
|
||||||
|
_showError(
|
||||||
|
'操作失败: ${e.toString().replaceAll('Exception: ', '')}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showError(String message) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(message), backgroundColor: Colors.red),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
body: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [Color(0xFFFFF5E6), Color(0xFFFFE4B5)],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildAppBar(),
|
||||||
|
Expanded(
|
||||||
|
child: _isLoading
|
||||||
|
? const Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
Color(0xFFD4AF37)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildStatusCard(),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_buildPasswordRequirements(),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
if (_hasPaymentPassword) ...[
|
||||||
|
_buildPasswordInput(
|
||||||
|
controller: _oldPasswordController,
|
||||||
|
label: '当前支付密码',
|
||||||
|
hint: '请输入当前6位支付密码',
|
||||||
|
showPassword: _showOldPassword,
|
||||||
|
onToggleVisibility: () => setState(
|
||||||
|
() => _showOldPassword = !_showOldPassword),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
_buildPasswordInput(
|
||||||
|
controller: _newPasswordController,
|
||||||
|
label: '新支付密码',
|
||||||
|
hint: '请输入6位数字支付密码',
|
||||||
|
showPassword: _showNewPassword,
|
||||||
|
onToggleVisibility: () => setState(
|
||||||
|
() => _showNewPassword = !_showNewPassword),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildPasswordInput(
|
||||||
|
controller: _confirmPasswordController,
|
||||||
|
label: '确认支付密码',
|
||||||
|
hint: '请再次输入6位支付密码',
|
||||||
|
showPassword: _showConfirmPassword,
|
||||||
|
onToggleVisibility: () => setState(() =>
|
||||||
|
_showConfirmPassword = !_showConfirmPassword),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
_buildSubmitButton(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAppBar() {
|
||||||
|
return Container(
|
||||||
|
height: 64,
|
||||||
|
padding: const EdgeInsets.only(top: 16, left: 16, right: 16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => context.pop(),
|
||||||
|
child: Container(
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: const Icon(Icons.arrow_back,
|
||||||
|
size: 24, color: Color(0xFF5D4037)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_hasPaymentPassword ? '修改支付密码' : '设置支付密码',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontFamily: 'Inter',
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
height: 1.25,
|
||||||
|
letterSpacing: -0.27,
|
||||||
|
color: Color(0xFF5D4037),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 48),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatusCard() {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0x80FFFFFF),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: const Color(0x33D4AF37), width: 1),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _hasPaymentPassword
|
||||||
|
? const Color(0xFF4CAF50).withValues(alpha: 0.1)
|
||||||
|
: const Color(0xFFFF9800).withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
_hasPaymentPassword ? Icons.payment : Icons.payment_outlined,
|
||||||
|
color: _hasPaymentPassword
|
||||||
|
? const Color(0xFF4CAF50)
|
||||||
|
: const Color(0xFFFF9800),
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_hasPaymentPassword ? '已设置支付密码' : '未设置支付密码',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontFamily: 'Inter',
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: _hasPaymentPassword
|
||||||
|
? const Color(0xFF4CAF50)
|
||||||
|
: const Color(0xFFFF9800),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
_hasPaymentPassword
|
||||||
|
? '认种、预种等支付操作需要验证支付密码'
|
||||||
|
: '设置后,认种和预种时需输入支付密码确认',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: 'Inter',
|
||||||
|
color: Color(0xFF745D43),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPasswordRequirements() {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFFFF5E6),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: const Color(0x33D4AF37), width: 1),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.info_outline, size: 20, color: Color(0xFF8B5A2B)),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'支付密码要求',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontFamily: 'Inter',
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF5D4037),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildRequirementItem('必须为 6 位纯数字'),
|
||||||
|
_buildRequirementItem('与登录密码相互独立'),
|
||||||
|
_buildRequirementItem('用于认种、预种等支付场景的二次验证'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRequirementItem(String text) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.check_circle_outline,
|
||||||
|
size: 16, color: Color(0xFFD4AF37)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: 'Inter',
|
||||||
|
color: Color(0xFF745D43),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPasswordInput({
|
||||||
|
required TextEditingController controller,
|
||||||
|
required String label,
|
||||||
|
required String hint,
|
||||||
|
required bool showPassword,
|
||||||
|
required VoidCallback onToggleVisibility,
|
||||||
|
}) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontFamily: 'Inter',
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Color(0xFF5D4037),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 54,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0x80FFFFFF),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: const Color(0x80FFFFFF), width: 1),
|
||||||
|
boxShadow: const [
|
||||||
|
BoxShadow(
|
||||||
|
color: Color(0x0D000000),
|
||||||
|
blurRadius: 2,
|
||||||
|
offset: Offset(0, 1),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
controller: controller,
|
||||||
|
obscureText: !showPassword,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
maxLength: 6,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontFamily: 'Inter',
|
||||||
|
height: 1.19,
|
||||||
|
color: Color(0xFF5D4037),
|
||||||
|
),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 15),
|
||||||
|
border: InputBorder.none,
|
||||||
|
counterText: '',
|
||||||
|
hintText: hint,
|
||||||
|
hintStyle: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontFamily: 'Inter',
|
||||||
|
height: 1.19,
|
||||||
|
color: Color(0x995D4037),
|
||||||
|
),
|
||||||
|
suffixIcon: GestureDetector(
|
||||||
|
onTap: onToggleVisibility,
|
||||||
|
child: Icon(
|
||||||
|
showPassword ? Icons.visibility_off : Icons.visibility,
|
||||||
|
color: const Color(0xFF8B5A2B),
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSubmitButton() {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: _isSubmitting ? null : _submit,
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 56,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _isSubmitting
|
||||||
|
? const Color(0xFFD4AF37).withValues(alpha: 0.5)
|
||||||
|
: const Color(0xFFD4AF37),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
boxShadow: const [
|
||||||
|
BoxShadow(
|
||||||
|
color: Color(0x4DD4AF37),
|
||||||
|
blurRadius: 14,
|
||||||
|
offset: Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: _isSubmitting
|
||||||
|
? const SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
_hasPaymentPassword ? '修改支付密码' : '设置支付密码',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontFamily: 'Inter',
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
height: 1.5,
|
||||||
|
letterSpacing: 0.24,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,7 @@ import '../features/planting/presentation/pages/planting_quantity_page.dart';
|
||||||
import '../features/planting/presentation/pages/planting_location_page.dart';
|
import '../features/planting/presentation/pages/planting_location_page.dart';
|
||||||
import '../features/security/presentation/pages/google_auth_page.dart';
|
import '../features/security/presentation/pages/google_auth_page.dart';
|
||||||
import '../features/security/presentation/pages/change_password_page.dart';
|
import '../features/security/presentation/pages/change_password_page.dart';
|
||||||
|
import '../features/security/presentation/pages/change_payment_password_page.dart'; // [2026-03-05] 支付密码页
|
||||||
import '../features/security/presentation/pages/bind_email_page.dart';
|
import '../features/security/presentation/pages/bind_email_page.dart';
|
||||||
import '../features/authorization/presentation/pages/authorization_apply_page.dart';
|
import '../features/authorization/presentation/pages/authorization_apply_page.dart';
|
||||||
import '../features/withdraw/presentation/pages/withdraw_usdt_page.dart';
|
import '../features/withdraw/presentation/pages/withdraw_usdt_page.dart';
|
||||||
|
|
@ -341,6 +342,13 @@ final appRouterProvider = Provider<GoRouter>((ref) {
|
||||||
builder: (context, state) => const ChangePasswordPage(),
|
builder: (context, state) => const ChangePasswordPage(),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Payment Password Page (支付密码设置/修改) [2026-03-05]
|
||||||
|
GoRoute(
|
||||||
|
path: RoutePaths.paymentPassword,
|
||||||
|
name: RouteNames.paymentPassword,
|
||||||
|
builder: (context, state) => const ChangePaymentPasswordPage(),
|
||||||
|
),
|
||||||
|
|
||||||
// Bind Email Page (绑定邮箱)
|
// Bind Email Page (绑定邮箱)
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: RoutePaths.bindEmail,
|
path: RoutePaths.bindEmail,
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ class RouteNames {
|
||||||
static const plantingLocation = 'planting-location';
|
static const plantingLocation = 'planting-location';
|
||||||
static const googleAuth = 'google-auth';
|
static const googleAuth = 'google-auth';
|
||||||
static const changePassword = 'change-password';
|
static const changePassword = 'change-password';
|
||||||
|
static const paymentPassword = 'payment-password'; // [2026-03-05] 支付密码
|
||||||
static const bindEmail = 'bind-email';
|
static const bindEmail = 'bind-email';
|
||||||
static const authorizationApply = 'authorization-apply';
|
static const authorizationApply = 'authorization-apply';
|
||||||
static const transactionHistory = 'transaction-history';
|
static const transactionHistory = 'transaction-history';
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ class RoutePaths {
|
||||||
static const plantingLocation = '/planting/location';
|
static const plantingLocation = '/planting/location';
|
||||||
static const googleAuth = '/security/google-auth';
|
static const googleAuth = '/security/google-auth';
|
||||||
static const changePassword = '/security/password';
|
static const changePassword = '/security/password';
|
||||||
|
static const paymentPassword = '/security/payment-password'; // [2026-03-05] 支付密码设置/修改
|
||||||
static const bindEmail = '/security/email';
|
static const bindEmail = '/security/email';
|
||||||
static const authorizationApply = '/authorization/apply';
|
static const authorizationApply = '/authorization/apply';
|
||||||
static const transactionHistory = '/trading/history';
|
static const transactionHistory = '/trading/history';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue