394 lines
16 KiB
Dart
394 lines
16 KiB
Dart
import 'dart:io';
|
||
import 'dart:typed_data';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:image_picker/image_picker.dart';
|
||
import 'package:provider/provider.dart';
|
||
// 新增导入
|
||
import 'package:zxing2/qrcode.dart';
|
||
import 'package:image/image.dart' as img;
|
||
|
||
import '../../providers/user_provider.dart';
|
||
|
||
class BindAccountPage extends StatefulWidget {
|
||
const BindAccountPage({super.key});
|
||
|
||
@override
|
||
State<BindAccountPage> createState() => _BindAccountPageState();
|
||
}
|
||
|
||
class _BindAccountPageState extends State<BindAccountPage> {
|
||
final TextEditingController _qrContentController = TextEditingController();
|
||
final ImagePicker _picker = ImagePicker();
|
||
|
||
bool _isProcessing = false;
|
||
String? _statusMessage;
|
||
File? _selectedImageFile; // 用户选择的原始二维码截图
|
||
|
||
@override
|
||
void dispose() {
|
||
_qrContentController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
// 1. 选择图片并识别二维码 (使用 zxing2替代 ML Kit)
|
||
Future<void> _pickImageAndScan() async {
|
||
try {
|
||
final XFile? pickedFile = await _picker.pickImage(source: ImageSource.gallery);
|
||
if (pickedFile == null) return;
|
||
|
||
setState(() {
|
||
_isProcessing = true;
|
||
_statusMessage = "正在加载图片...";
|
||
_selectedImageFile = File(pickedFile.path);
|
||
});
|
||
|
||
// 读取图片字节
|
||
final bytes = await pickedFile.readAsBytes();
|
||
|
||
// 使用 image 库解码图片为 RGB 数据
|
||
final img.Image? decodedImage = img.decodeImage(bytes);
|
||
|
||
if (decodedImage == null) {
|
||
throw Exception("无法解析图片格式");
|
||
}
|
||
|
||
setState(() {
|
||
_statusMessage = "正在解析二维码...";
|
||
});
|
||
|
||
final reader = QRCodeReader();
|
||
|
||
try {
|
||
// 【修复点开始】
|
||
// 1. 获取 Uint8List (RGBA 格式)
|
||
final Uint8List rgbaBytes = decodedImage.getBytes(order: img.ChannelOrder.rgba);
|
||
|
||
// 2. 将 Uint8List 转换为 Int32List
|
||
// 注意:这需要底层字节序匹配。大多数现代移动设备是 Little Endian。
|
||
// Uint8List 的长度必须是 4 的倍数(RGBA 每个像素4字节),这通常成立。
|
||
final Int32List pixels = Int32List.view(rgbaBytes.buffer);
|
||
|
||
// 3. 创建 LuminanceSource
|
||
// ZXing 的 RGBLuminanceSource 期望 Int32List,其中每个 int 代表一个像素 (0xAARRGGBB 或类似格式)
|
||
// 由于 image 库给出的是 RGBA (0xRR, 0xGG, 0xBB, 0xAA),直接转换可能导致颜色通道错位,
|
||
// 但 ZXing 主要关注亮度(灰度),通常 RGBA 直接转 Int32 也能被正确二值化识别,
|
||
// 如果识别失败,可能需要手动重排字节为 ARGB。
|
||
|
||
final source = RGBLuminanceSource(
|
||
decodedImage.width,
|
||
decodedImage.height,
|
||
pixels
|
||
);
|
||
// 【修复点结束】
|
||
|
||
final binarizer = HybridBinarizer(source);
|
||
final bitmap = BinaryBitmap(binarizer);
|
||
|
||
final result = reader.decode(bitmap);
|
||
|
||
if (result != null && result.text != null) {
|
||
setState(() {
|
||
_qrContentController.text = result.text!;
|
||
_statusMessage = "识别成功";
|
||
});
|
||
} else {
|
||
setState(() {
|
||
_statusMessage = "未识别到二维码,请手动输入";
|
||
});
|
||
}
|
||
} catch (e) {
|
||
// 捕获 zxing 内部的 NotFoundException 等
|
||
print("ZXing Error: $e"); // 调试用
|
||
setState(() {
|
||
_statusMessage = "未在图中找到有效二维码";
|
||
});
|
||
}
|
||
|
||
} catch (e) {
|
||
setState(() {
|
||
_statusMessage = "识别出错: $e";
|
||
});
|
||
} finally {
|
||
setState(() {
|
||
_isProcessing = false;
|
||
});
|
||
}
|
||
}
|
||
|
||
// 2. 执行隐写并上传 (触发后端绑定/登录逻辑)
|
||
Future<void> _handleBind() async {
|
||
final qrContent = _qrContentController.text.trim();
|
||
|
||
if (qrContent.isEmpty) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(content: Text("请输入或扫描二维码内容"), backgroundColor: Colors.orange),
|
||
);
|
||
return;
|
||
}
|
||
|
||
Uint8List? imageBytes;
|
||
|
||
setState(() {
|
||
_isProcessing = true;
|
||
_statusMessage = "正在处理图片...";
|
||
});
|
||
|
||
try {
|
||
// 下载背景图用于隐写
|
||
final httpClient = HttpClient();
|
||
// 建议在实际生产环境中添加 timeout
|
||
final request = await httpClient.getUrl(Uri.parse('https://union.godserver.cn/assets/jpeg/20180621142015_5FmGZ-wYXkyL4y.jpeg'));
|
||
final response = await request.close();
|
||
final bytes = <int>[];
|
||
await for (final chunk in response) {
|
||
bytes.addAll(chunk);
|
||
}
|
||
imageBytes = Uint8List.fromList(bytes);
|
||
|
||
// 关闭 client 以释放资源
|
||
httpClient.close();
|
||
|
||
if (imageBytes == null || imageBytes.isEmpty) throw Exception("图片数据无效");
|
||
|
||
setState(() {
|
||
_statusMessage = "正在提交绑定请求...";
|
||
});
|
||
|
||
// 调用 Provider
|
||
final userProvider = context.read<UserProvider>();
|
||
final result = await userProvider.uploadStegImage(imageBytes, segaId: qrContent);
|
||
|
||
setState(() {
|
||
_isProcessing = false;
|
||
});
|
||
|
||
// 处理后端返回
|
||
if (mounted) {
|
||
if (result['code'] == 200) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(content: Text("✅ ${result['msg']}"), backgroundColor: Colors.green, duration: const Duration(seconds: 5)),
|
||
);
|
||
// 绑定成功后,刷新用户信息
|
||
await userProvider.initUser();
|
||
|
||
// 可选:清空输入或跳转
|
||
// _qrContentController.clear();
|
||
// Navigator.of(context).pop();
|
||
} else {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(content: Text("❌ ${result['msg']}"), backgroundColor: Colors.red, duration: const Duration(seconds: 5)),
|
||
);
|
||
}
|
||
}
|
||
|
||
} catch (e) {
|
||
setState(() {
|
||
_isProcessing = false;
|
||
_statusMessage = "网络或处理错误: $e";
|
||
});
|
||
if (mounted) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(content: Text("错误: $e"), backgroundColor: Colors.red),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
appBar: AppBar(
|
||
title: const Text("绑定 / 刷新 Sega 账号"),
|
||
backgroundColor: Colors.black.withOpacity(0.5),
|
||
elevation: 0,
|
||
),
|
||
body: Stack(
|
||
fit: StackFit.expand,
|
||
children: [
|
||
// 背景图
|
||
Image.network(
|
||
'https://union.godserver.cn/assets/jpeg/20180621142015_5FmGZ-wYXkyL4y.jpeg',
|
||
fit: BoxFit.cover,
|
||
loadingBuilder: (context, child, loadingProgress) {
|
||
if (loadingProgress == null) return child;
|
||
return Container(color: Colors.black);
|
||
},
|
||
errorBuilder: (context, error, stackTrace) => Container(color: Colors.grey[900]),
|
||
),
|
||
|
||
// 遮罩
|
||
Container(color: Colors.black.withOpacity(0.75)),
|
||
|
||
// 内容
|
||
SafeArea(
|
||
child: Center(
|
||
child: SingleChildScrollView(
|
||
padding: const EdgeInsets.all(24.0),
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
const Icon(Icons.qr_code_scanner, size: 60, color: Colors.white70),
|
||
const SizedBox(height: 16),
|
||
const Text(
|
||
"请提供 Sega 登录二维码",
|
||
style: TextStyle(fontSize: 18, color: Colors.white, fontWeight: FontWeight.bold),
|
||
),
|
||
const SizedBox(height: 8),
|
||
const Text(
|
||
"支持识别截图或手动输入二维码内容\n用于绑定账号或刷新登录状态",
|
||
style: TextStyle(fontSize: 14, color: Colors.white60),
|
||
textAlign: TextAlign.center,
|
||
),
|
||
const SizedBox(height: 30),
|
||
|
||
// 输入卡片
|
||
Container(
|
||
padding: const EdgeInsets.all(20),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white.withOpacity(0.1),
|
||
borderRadius: BorderRadius.circular(16),
|
||
border: Border.all(color: Colors.white.withOpacity(0.2)),
|
||
),
|
||
child: Column(
|
||
children: [
|
||
TextField(
|
||
controller: _qrContentController,
|
||
style: const TextStyle(color: Colors.white),
|
||
maxLines: 10,
|
||
decoration: InputDecoration(
|
||
labelText: "二维码内容 (QR Data)",
|
||
labelStyle: const TextStyle(color: Colors.white70),
|
||
hintText: "例如: SGWCMAID...",
|
||
hintStyle: const TextStyle(color: Colors.white38),
|
||
enabledBorder: OutlineInputBorder(
|
||
borderSide: const BorderSide(color: Colors.white38),
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
focusedBorder: OutlineInputBorder(
|
||
borderSide: const BorderSide(color: Colors.white),
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
),
|
||
),
|
||
|
||
const SizedBox(height: 20),
|
||
|
||
// 预览图
|
||
if (_selectedImageFile != null)
|
||
ClipRRect(
|
||
borderRadius: BorderRadius.circular(8),
|
||
child: Image.file(_selectedImageFile!, height: 120, fit: BoxFit.contain),
|
||
),
|
||
|
||
if (_selectedImageFile != null) const SizedBox(height: 15),
|
||
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: Container(
|
||
decoration: BoxDecoration(
|
||
gradient: const LinearGradient(
|
||
colors: [Colors.purpleAccent, Colors.deepOrange],
|
||
begin: Alignment.topLeft, // 改成你要的
|
||
end: Alignment.bottomRight, // 改成你要的
|
||
),
|
||
borderRadius: BorderRadius.circular(14),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.pink.withOpacity(0.4),
|
||
blurRadius: 8,
|
||
offset: const Offset(0, 4),
|
||
),
|
||
],
|
||
),
|
||
child: ElevatedButton.icon(
|
||
onPressed: _isProcessing ? null : _pickImageAndScan,
|
||
icon: const Icon(Icons.image_search, size: 20),
|
||
label: const Text("识别截图", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500)),
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: Colors.transparent,
|
||
foregroundColor: Colors.white,
|
||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||
elevation: 0,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(14),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 12),
|
||
Expanded(
|
||
child: Container(
|
||
decoration: BoxDecoration(
|
||
// 只改这里:渐变方向 → 参考你的写法
|
||
gradient: const LinearGradient(
|
||
colors: [Colors.white10, Colors.black],
|
||
begin: Alignment.topLeft, // 改成你要的
|
||
end: Alignment.bottomRight, // 改成你要的
|
||
),
|
||
borderRadius: BorderRadius.circular(14),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.white.withOpacity(0.4),
|
||
blurRadius: 8,
|
||
offset: const Offset(0, 4),
|
||
),
|
||
],
|
||
),
|
||
child: ElevatedButton.icon(
|
||
onPressed: _isProcessing ? null : _handleBind,
|
||
icon: _isProcessing
|
||
? const SizedBox(
|
||
width: 18,
|
||
height: 18,
|
||
child: CircularProgressIndicator(
|
||
strokeWidth: 2,
|
||
color: Colors.white,
|
||
),
|
||
)
|
||
: const Icon(Icons.login, size: 20),
|
||
label: Text(
|
||
_isProcessing ? "处理中" : "绑定/刷新",
|
||
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
|
||
),
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: Colors.transparent,
|
||
foregroundColor: Colors.white,
|
||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||
elevation: 0,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(14),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
// 状态消息
|
||
if (_statusMessage != null)
|
||
Padding(
|
||
padding: const EdgeInsets.only(top: 20),
|
||
child: Text(
|
||
_statusMessage!,
|
||
style: TextStyle(
|
||
color: _statusMessage!.contains("失败") || _statusMessage!.contains("错误") || _statusMessage!.contains("未识别") ? Colors.redAccent : Colors.white70,
|
||
),
|
||
textAlign: TextAlign.center,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
} |