This commit is contained in:
Developer 2025-12-01 19:19:00 -08:00
parent 097396bb93
commit 6f2ff2d9b3
5 changed files with 457 additions and 86 deletions

View File

@ -6,7 +6,28 @@
"Bash(git add:*)",
"Bash(flutter pub get:*)",
"Bash(flutter analyze:*)",
"Bash(git commit -m \"$(cat <<''EOF''\nfeat: 添加APK在线升级和遥测统计模块\n\nAPK升级模块 (lib/core/updater/):\n- 支持自建服务器和Google Play双渠道更新\n- 版本检测、APK下载、SHA-256校验、安装\n- 应用市场来源检测\n- 强制更新和普通更新对话框\n\n遥测模块 (lib/core/telemetry/):\n- 设备信息采集 (品牌、型号、系统版本、屏幕等)\n- 会话管理 (DAU日活统计)\n- 心跳服务 (实时在线人数统计)\n- 事件队列和批量上传\n- 远程配置热更新\n\nAndroid原生配置:\n- MainActivity.kt Platform Channel实现\n- FileProvider配置 (APK安装)\n- 权限配置 (INTERNET, REQUEST_INSTALL_PACKAGES)\n\n文档:\n- docs/backend_api_guide.md 后端API开发指南\n- docs/testing_guide.md 测试指南\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude <noreply@anthropic.com>\nEOF\n)\")"
"Bash(git commit -m \"$(cat <<''EOF''\nfeat: 添加APK在线升级和遥测统计模块\n\nAPK升级模块 (lib/core/updater/):\n- 支持自建服务器和Google Play双渠道更新\n- 版本检测、APK下载、SHA-256校验、安装\n- 应用市场来源检测\n- 强制更新和普通更新对话框\n\n遥测模块 (lib/core/telemetry/):\n- 设备信息采集 (品牌、型号、系统版本、屏幕等)\n- 会话管理 (DAU日活统计)\n- 心跳服务 (实时在线人数统计)\n- 事件队列和批量上传\n- 远程配置热更新\n\nAndroid原生配置:\n- MainActivity.kt Platform Channel实现\n- FileProvider配置 (APK安装)\n- 权限配置 (INTERNET, REQUEST_INSTALL_PACKAGES)\n\n文档:\n- docs/backend_api_guide.md 后端API开发指南\n- docs/testing_guide.md 测试指南\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude <noreply@anthropic.com>\nEOF\n)\")",
"Bash(mkdir:*)",
"Bash(keytool:*)",
"Bash(flutter doctor:*)",
"Bash(flutter emulators:*)",
"Bash(c:Androidemulatoremulator -list-avds)",
"Bash(flutter devices:*)",
"Bash(c:Androidcmdline-toolslatestbinsdkmanager.bat --list)",
"Bash(findstr:*)",
"Bash(cmd /c \"c:\\Android\\cmdline-tools\\latest\\bin\\sdkmanager.bat --list\")",
"Bash(cmd /c \"dir c:\\Android\\cmdline-tools\\latest\\bin\")",
"Bash(cmd /c \"dir c:\\Android\")",
"Bash(dir:*)",
"Bash(powershell -Command:*)",
"Bash(Select-String \"system-images\")",
"Bash(Select-Object -First 10)",
"Bash(powershell:*)",
"Bash(c:/Android/platform-tools/adb.exe devices:*)",
"Bash(c:/Android/platform-tools/adb.exe kill-server)",
"Bash(c:/Android/platform-tools/adb.exe start-server:*)",
"Bash(flutter run:*)",
"Bash(git commit:*)"
],
"deny": [],
"ask": []

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:go_router/go_router.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import '../../../../routes/route_paths.dart';
import '../providers/auth_provider.dart';
@ -257,77 +258,155 @@ class _WelcomePageContentState extends State<_WelcomePageContent> {
super.dispose();
}
///
Future<void> _openQrScanner() async {
final result = await Navigator.of(context).push<String>(
MaterialPageRoute(
builder: (context) => const _QrScannerPage(),
),
);
if (result != null && result.isNotEmpty) {
//
final referralCode = _extractReferralCode(result);
setState(() {
_referralCodeController.text = referralCode;
_hasReferrer = true;
});
}
}
///
/// :
/// - URL: https://app.rwadurian.com/r/ABC123 -> ABC123
/// - : https://rwa.link/ABC123 -> ABC123
/// - : ABC123 -> ABC123
String _extractReferralCode(String scannedData) {
//
String data = scannedData.trim();
// URL
if (data.startsWith('http://') || data.startsWith('https://')) {
try {
final uri = Uri.parse(data);
// ( /r/ABC123 /ABC123)
final pathSegments = uri.pathSegments;
if (pathSegments.isNotEmpty) {
//
final lastSegment = pathSegments.last;
if (lastSegment.isNotEmpty) {
return lastSegment;
}
// 'r'
if (pathSegments.length >= 2 && lastSegment == 'r') {
return pathSegments[pathSegments.length - 2];
}
}
// ( ?ref=ABC123 ?code=ABC123)
final refCode = uri.queryParameters['ref'] ??
uri.queryParameters['code'] ??
uri.queryParameters['referral'];
if (refCode != null && refCode.isNotEmpty) {
return refCode;
}
} catch (e) {
// URL
debugPrint('URL 解析失败: $e');
}
}
// URL
return data;
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Column(
children: [
// 退
Align(
alignment: Alignment.topRight,
child: Padding(
padding: EdgeInsets.only(top: 32.h),
child: GestureDetector(
onTap: () {
// 退
Navigator.of(context).maybePop();
},
child: Text(
'退出 Exit',
style: TextStyle(
fontSize: 14.sp,
color: const Color(0xFFA99F93),
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Column(
children: [
// 退
Align(
alignment: Alignment.topRight,
child: Padding(
padding: EdgeInsets.only(top: 32.h),
child: GestureDetector(
onTap: () {
// 退
Navigator.of(context).maybePop();
},
child: Text(
'退出 Exit',
style: TextStyle(
fontSize: 14.sp,
color: const Color(0xFFA99F93),
),
),
),
),
),
SizedBox(height: 80.h),
//
Text(
'欢迎加入',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w700,
height: 1.33,
color: const Color(0xFF6F6354),
),
),
SizedBox(height: 12.h),
//
Text(
'创建账号前的最后一步 · 请选择是否有推荐人',
style: TextStyle(
fontSize: 14.sp,
height: 1.43,
color: const Color(0xFFA99F93),
),
),
SizedBox(height: 48.h),
//
_buildReferrerOptions(),
const Spacer(),
//
GestureDetector(
onTap: widget.onNext,
child: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 16.h),
child: Text(
'下一步 (创建账号)',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
height: 1.5,
color: const Color(0xFFD9C8A9),
),
textAlign: TextAlign.right,
),
),
),
SizedBox(height: 48.h),
],
),
),
),
),
),
SizedBox(height: 100.h),
//
Text(
'欢迎加入',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w700,
height: 1.33,
color: const Color(0xFF6F6354),
),
),
SizedBox(height: 12.h),
//
Text(
'创建账号前的最后一步 · 请选择是否有推荐人',
style: TextStyle(
fontSize: 14.sp,
height: 1.43,
color: const Color(0xFFA99F93),
),
),
SizedBox(height: 62.h),
//
_buildReferrerOptions(),
const Spacer(),
//
GestureDetector(
onTap: widget.onNext,
child: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 16.h),
child: Text(
'下一步 (创建账号)',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
height: 1.5,
color: const Color(0xFFD9C8A9),
),
textAlign: TextAlign.right,
),
),
),
SizedBox(height: 80.h),
],
);
},
),
);
}
@ -356,11 +435,17 @@ class _WelcomePageContentState extends State<_WelcomePageContent> {
),
),
const Spacer(),
//
Icon(
Icons.qr_code_scanner,
size: 24.sp,
color: const Color(0xFFA99F93),
// -
GestureDetector(
onTap: _openQrScanner,
child: Container(
padding: EdgeInsets.all(8.w),
child: Icon(
Icons.qr_code_scanner,
size: 24.sp,
color: const Color(0xFF8E794A),
),
),
),
],
),
@ -378,22 +463,40 @@ class _WelcomePageContentState extends State<_WelcomePageContent> {
),
),
),
child: TextField(
controller: _referralCodeController,
decoration: InputDecoration(
hintText: '请输入推荐码 / 序列号',
hintStyle: TextStyle(
fontSize: 16.sp,
color: const Color(0xFFA99F93),
child: Row(
children: [
Expanded(
child: TextField(
controller: _referralCodeController,
decoration: InputDecoration(
hintText: '请输入推荐码 / 序列号',
hintStyle: TextStyle(
fontSize: 16.sp,
color: const Color(0xFFA99F93),
),
border: InputBorder.none,
isDense: true,
contentPadding: EdgeInsets.zero,
),
style: TextStyle(
fontSize: 16.sp,
color: const Color(0xFF6F6354),
),
),
),
border: InputBorder.none,
isDense: true,
contentPadding: EdgeInsets.zero,
),
style: TextStyle(
fontSize: 16.sp,
color: const Color(0xFF6F6354),
),
//
GestureDetector(
onTap: _openQrScanner,
child: Padding(
padding: EdgeInsets.only(left: 8.w),
child: Icon(
Icons.camera_alt_outlined,
size: 20.sp,
color: const Color(0xFFA99F93),
),
),
),
],
),
),
SizedBox(height: 24.h),
@ -440,3 +543,239 @@ class _WelcomePageContentState extends State<_WelcomePageContent> {
);
}
}
///
class _QrScannerPage extends StatefulWidget {
const _QrScannerPage();
@override
State<_QrScannerPage> createState() => _QrScannerPageState();
}
class _QrScannerPageState extends State<_QrScannerPage> {
MobileScannerController? _controller;
bool _hasScanned = false;
bool _torchOn = false;
@override
void initState() {
super.initState();
_controller = MobileScannerController(
detectionSpeed: DetectionSpeed.normal,
facing: CameraFacing.back,
);
}
@override
void dispose() {
_controller?.dispose();
super.dispose();
}
void _onDetect(BarcodeCapture capture) {
if (_hasScanned) return;
final List<Barcode> barcodes = capture.barcodes;
for (final barcode in barcodes) {
if (barcode.rawValue != null && barcode.rawValue!.isNotEmpty) {
_hasScanned = true;
Navigator.of(context).pop(barcode.rawValue);
return;
}
}
}
Future<void> _toggleTorch() async {
await _controller?.toggleTorch();
setState(() {
_torchOn = !_torchOn;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.black,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.of(context).pop(),
),
title: Text(
'扫描推荐码',
style: TextStyle(
fontSize: 18.sp,
color: Colors.white,
),
),
centerTitle: true,
actions: [
IconButton(
icon: Icon(
_torchOn ? Icons.flash_on : Icons.flash_off,
color: Colors.white,
),
onPressed: _toggleTorch,
),
],
),
body: Stack(
children: [
//
MobileScanner(
controller: _controller,
onDetect: _onDetect,
),
//
_buildScanOverlay(),
//
Positioned(
bottom: 100.h,
left: 0,
right: 0,
child: Text(
'将二维码放入框内,即可自动扫描',
style: TextStyle(
fontSize: 14.sp,
color: Colors.white70,
),
textAlign: TextAlign.center,
),
),
],
),
);
}
///
Widget _buildScanOverlay() {
return LayoutBuilder(
builder: (context, constraints) {
final scanAreaSize = 250.w;
final left = (constraints.maxWidth - scanAreaSize) / 2;
final top = (constraints.maxHeight - scanAreaSize) / 2 - 50.h;
return Stack(
children: [
//
ColorFiltered(
colorFilter: ColorFilter.mode(
Colors.black.withValues(alpha: 0.6),
BlendMode.srcOut,
),
child: Stack(
children: [
Container(
decoration: const BoxDecoration(
color: Colors.black,
backgroundBlendMode: BlendMode.dstOut,
),
),
Positioned(
left: left,
top: top,
child: Container(
width: scanAreaSize,
height: scanAreaSize,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
),
),
),
],
),
),
//
Positioned(
left: left,
top: top,
child: _buildCorner(true, true),
),
Positioned(
right: left,
top: top,
child: _buildCorner(false, true),
),
Positioned(
left: left,
bottom: constraints.maxHeight - top - scanAreaSize,
child: _buildCorner(true, false),
),
Positioned(
right: left,
bottom: constraints.maxHeight - top - scanAreaSize,
child: _buildCorner(false, false),
),
],
);
},
);
}
///
Widget _buildCorner(bool isLeft, bool isTop) {
return SizedBox(
width: 24.w,
height: 24.w,
child: CustomPaint(
painter: _CornerPainter(
isLeft: isLeft,
isTop: isTop,
color: const Color(0xFFD4A84B),
strokeWidth: 3.w,
),
),
);
}
}
///
class _CornerPainter extends CustomPainter {
final bool isLeft;
final bool isTop;
final Color color;
final double strokeWidth;
_CornerPainter({
required this.isLeft,
required this.isTop,
required this.color,
required this.strokeWidth,
});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
final path = Path();
if (isLeft && isTop) {
path.moveTo(0, size.height);
path.lineTo(0, 0);
path.lineTo(size.width, 0);
} else if (!isLeft && isTop) {
path.moveTo(0, 0);
path.lineTo(size.width, 0);
path.lineTo(size.width, size.height);
} else if (isLeft && !isTop) {
path.moveTo(0, 0);
path.lineTo(0, size.height);
path.lineTo(size.width, size.height);
} else {
path.moveTo(0, size.height);
path.lineTo(size.width, size.height);
path.lineTo(size.width, 0);
}
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

View File

@ -10,6 +10,7 @@ import device_info_plus
import file_selector_macos
import flutter_secure_storage_macos
import local_auth_darwin
import mobile_scanner
import package_info_plus
import path_provider_foundation
import share_plus
@ -23,6 +24,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin"))
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))

View File

@ -909,6 +909,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.6"
mobile_scanner:
dependency: "direct main"
description:
name: mobile_scanner
sha256: d234581c090526676fd8fab4ada92f35c6746e3fb4f05a399665d75a399fb760
url: "https://pub.dev"
source: hosted
version: "5.2.3"
mocktail:
dependency: "direct dev"
description:

View File

@ -41,6 +41,7 @@ dependencies:
shimmer: ^3.0.0
lottie: ^3.1.0
qr_flutter: ^4.1.0
mobile_scanner: ^5.1.1
flutter_screenutil: ^5.9.0
city_pickers: ^1.3.0