Files
UnionApp/lib/pages/score/updateScorePage.dart
spasolreisa 603772bc81 ver1.00.00
update2
2026-04-21 01:28:53 +08:00

342 lines
13 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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: ElevatedButton.icon(
onPressed: _isProcessing ? null : _pickImageAndScan,
icon: const Icon(Icons.image_search),
label: const Text("识别截图"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueAccent,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: _isProcessing ? null : _handleBind,
icon: _isProcessing
? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
: const Icon(Icons.login),
label: Text(_isProcessing ? "处理中" : "绑定/刷新"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
],
),
],
),
),
// 状态消息
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,
),
),
],
),
),
),
),
],
),
);
}
}