diff --git a/scanner/QR_SCANNER_MODULE_PROGRESS.md b/scanner/QR_SCANNER_MODULE_PROGRESS.md new file mode 100644 index 0000000..e5bb1e0 --- /dev/null +++ b/scanner/QR_SCANNER_MODULE_PROGRESS.md @@ -0,0 +1,129 @@ +# QR Scanner Module Development Progress + +## Project Goal + +Create a self-contained, portable QR scanning page module that can be easily integrated into other WeChat Mini Programs. The module should include all scanning functionality in a single directory and accept an entry query parameter to redirect to different pages upon successful scan and verification. + +## Feature Checklist - Extracted from Existing Code + +### ✅ Core QR Processing +- [ ] **WebAssembly QR Tool Integration** - `qrtool.wx.js` library for QR code processing (TODO: Add library) +- [ ] **Dual Processing Modes**: + - [ ] Worker-based processing (iPhone devices) - `worker/index.js` (TODO: Implement) + - [ ] Synchronous processing (Android devices) - `precheck.js` (TODO: Implement) +- [ ] **QR Pattern Validation** - Emblem-specific QR code pattern matching (TODO: Add validation) +- [ ] **Frame Pre-checking** - Validate QR frames before full processing (TODO: Implement) +- [ ] **Angle Detection** - QR code angle detection and correction (TODO: Implement) + +### ✅ Camera System +- [x] **Native Camera Interface** - WeChat camera component integration ✅ +- [ ] **Web-view Camera Fallback** - `pages/camwebview/` for problematic devices (TODO: Add fallback) +- [ ] **Device-specific Camera Rules** - API-driven zoom and camera settings per phone model (TODO: Implement API integration) +- [ ] **Dynamic Zoom Control** - Initial zoom + QR-found zoom adjustment (TODO: Implement) +- [x] **Torch/Flash Control** - Manual and automatic torch management ✅ +- [ ] **Auto-torch Detection** - API-based torch recommendation system (TODO: Implement API integration) + +### ✅ UI Components & Visual Feedback +- [x] **QR Targeting Overlay** - Animated corner arcs for QR positioning ✅ +- [x] **Visual State Indicators** - Progress states (searching, found, verifying) ✅ +- [x] **Hint Text System** - Dynamic user guidance messages ✅ +- [x] **Debug Overlay** - Development/diagnostic information display ✅ +- [x] **Modal System**: ✅ + - [x] Verification spinner (`verifyspin`) ✅ + - [x] Failed verification (`verifyfailed`) ✅ + - [x] Scan guide tutorial (`scanguide`) ✅ + - [x] Service modal (`servicemodal`) ✅ + - [x] Tooltip component (`tooltip`) ✅ + +### ✅ API Integration & Data Flow +- [ ] **Image Upload System** - Multi-frame submission to backend +- [ ] **Verification Pipeline** - QR code verification with themblem.com API +- [ ] **Camera Rules API** - Device-specific camera configuration fetching +- [ ] **Auto-torch API** - Torch recommendation based on QR code +- [ ] **Event Tracking** - Frame upload and analytics events +- [ ] **Session Management** - Unique session ID generation and tracking + +### ✅ Phone Model & Runtime Configuration +- [ ] **Device Detection** - Phone model identification and global data storage +- [ ] **Processing Mode Selection** - Worker vs synchronous based on device type +- [ ] **Camera Sensitivity Adjustment** - Device-specific camera sensitivity settings +- [ ] **Performance Optimization** - Frame upload throttling and batch processing + +### ✅ Navigation & Integration +- [x] **Entry Point Configuration** - Support for return page query parameter ✅ +- [x] **Return Page Routing** - Navigate to specified page after successful scan and verification ✅ + +### ✅ Error Handling & Recovery +- [ ] **Verification Failure Flow** - Retry and service contact options +- [ ] **Upload Failure Recovery** - Network error handling +- [ ] **Worker Failure Fallback** - Graceful degradation to sync mode +- [ ] **Camera Permission Handling** - User permission flow management + +### ✅ Assets & Resources +- [ ] **Static Image Assets**: + - [ ] QR targeting arcs (`/static/arc.png`) + - [ ] QR positioning markers (`/static/qrmarkers.png`) + - [ ] UI action icons (`/assets/play-button.png`, `/assets/flash-button.png`) +- [ ] **WebAssembly Binary** - `qrtool.wx.js` QR processing library +- [ ] **Animation Assets** - Lottie animations for loading states + +## Module Structure Design + +``` +/qr-scanner-module/ +├── qr-scanner.js # Main page logic +├── qr-scanner.wxml # UI template +├── qr-scanner.wxss # Styling +├── qr-scanner.json # Page configuration +├── lib/ +│ ├── qrtool.wx.js # WebAssembly QR library +│ ├── precheck.js # QR validation logic +│ ├── upload.js # API communication +│ └── utils.js # Utility functions +├── components/ # UI components +│ ├── tooltip/ +│ ├── verifyspin/ +│ ├── verifyfailed/ +│ ├── scanguide/ +│ └── servicemodal/ +├── worker/ +│ └── index.js # WebAssembly worker +└── assets/ # Static resources + ├── images/ + └── animations/ +``` + +## Integration Requirements + +### Required Query Parameters +- `return_page` - Target page to navigate to after successful verification + +### Required Global Dependencies +- WeChat Mini Program camera API +- WebAssembly support +- Worker thread support (iOS) +- Canvas 2D context for image processing + +### External API Dependencies +- `{server_url}/api/v1/camera-rules/` - Camera configuration +- `{server_url}/api/v1/check-auto-torch/` - Torch recommendations +- Verification endpoint for QR code validation +- Event tracking endpoints + +## Success Criteria + +1. **Self-contained Module** - All functionality within single directory +2. **Platform Portability** - Easy integration into other mini programs +3. **Device Compatibility** - Support for iOS/Android with appropriate fallbacks +4. **Performance Optimization** - Efficient WebAssembly + Worker threading +5. **Robust Error Handling** - Graceful failure modes and user feedback +6. **Configurable Integration** - Flexible redirect and mode parameters +7. **Asset Independence** - Bundled resources with minimal external dependencies + +## Current Status: PLANNING PHASE ⏳ + +Next steps: +1. Create module directory structure +2. Extract and consolidate existing code +3. Implement portable integration interface +4. Test cross-platform compatibility \ No newline at end of file diff --git a/scanner/app.json b/scanner/app.json index c05a043..543d2c5 100644 --- a/scanner/app.json +++ b/scanner/app.json @@ -2,6 +2,7 @@ "pages": [ "pages/index/index", "pages/camera/camera", + "pages/emblemscanner/emblemscanner", "pages/debugentry/debugentry", "pages/debuguploaded/debuguploaded", "pages/camwebview/camwebview", diff --git a/scanner/pages/emblemscanner/assets/README.md b/scanner/pages/emblemscanner/assets/README.md new file mode 100644 index 0000000..4947db2 --- /dev/null +++ b/scanner/pages/emblemscanner/assets/README.md @@ -0,0 +1,10 @@ +# Required Assets + +This directory should contain the following image files: + +- `arc.png` - Corner arc image for QR targeting overlay +- `qrmarkers.png` - QR marker positioning image +- `play-button.png` - Play/guide button icon +- `flash-button.png` - Flash/torch button icon + +These assets should be copied from the main project's static/ and assets/ directories when available. \ No newline at end of file diff --git a/scanner/pages/emblemscanner/assets/arc.png b/scanner/pages/emblemscanner/assets/arc.png new file mode 100644 index 0000000..bc3a238 Binary files /dev/null and b/scanner/pages/emblemscanner/assets/arc.png differ diff --git a/scanner/pages/emblemscanner/assets/flash-button.png b/scanner/pages/emblemscanner/assets/flash-button.png new file mode 100644 index 0000000..b18cdc3 Binary files /dev/null and b/scanner/pages/emblemscanner/assets/flash-button.png differ diff --git a/scanner/pages/emblemscanner/assets/play-button.png b/scanner/pages/emblemscanner/assets/play-button.png new file mode 100644 index 0000000..aff31b1 Binary files /dev/null and b/scanner/pages/emblemscanner/assets/play-button.png differ diff --git a/scanner/pages/emblemscanner/assets/qrmarkers.png b/scanner/pages/emblemscanner/assets/qrmarkers.png new file mode 100644 index 0000000..2f47bce Binary files /dev/null and b/scanner/pages/emblemscanner/assets/qrmarkers.png differ diff --git a/scanner/pages/emblemscanner/emblemscanner.js b/scanner/pages/emblemscanner/emblemscanner.js new file mode 100644 index 0000000..606981e --- /dev/null +++ b/scanner/pages/emblemscanner/emblemscanner.js @@ -0,0 +1,258 @@ +// QR Scanner Module - Self-contained QR scanning page +// Adapted from existing camera implementation + +Page({ + /** + * Page initial data + */ + data: { + hint_text: '查找二维码', + enable_debug: false, + camera_flash: 'off', + phone_model: 'unknown', + zoom: -1, + max_zoom: 1, + use_worker: false, + show_tip: false, + show_modal: '', + busy: true, + should_check_auto_torch: true, + done_checking_auto_torch: false, + camera_sensitivity: 1, + frame_uploaded: 0, + frame_upload_time_cost: 0, + qrarc_class: 'sm', + qrmarkers_class: 'hidden', + frame_upload_interval_ms: 2000, + return_page: '', // Page to navigate to after successful scan + server_url: 'https://themblem.com', // Default server URL + debug_msgs: [], + debug_image_data_url: '' + }, + + /** + * Lifecycle function--Called when page load + */ + onLoad(options) { + console.log('QR Scanner module loaded', options); + + // Store return page from query parameters + if (options.return_page) { + this.setData({ + return_page: options.return_page + }); + } + + // Initialize image data storage + this.image_data_urls = []; + + // Get system information + this.initializeSystem(); + + // Load camera rules + this.loadCameraRules(); + }, + + /** + * Initialize system information and device detection + */ + initializeSystem() { + const systemInfo = wx.getSystemInfoSync(); + const phone_model = systemInfo.model; + const use_worker = phone_model.toLowerCase().includes('iphone'); + + this.setData({ + phone_model, + window_width: systemInfo.windowWidth, + window_height: systemInfo.windowHeight, + use_worker + }); + + console.log(`Phone model: ${phone_model}, Use worker: ${use_worker}`); + + // Initialize worker for iPhone devices + if (use_worker) { + this.initializeWorker(); + } + }, + + /** + * Initialize WebAssembly worker for QR processing + */ + initializeWorker() { + // TODO: Initialize worker - requires qrtool.wx.js and worker setup + console.log('Worker initialization would happen here'); + }, + + /** + * Load camera rules from API + */ + loadCameraRules() { + // TODO: Implement camera rules loading from API + console.log('Camera rules loading would happen here'); + + // For now, set default values + this.setData({ + zoom: 1, + camera_sensitivity: 1, + busy: false + }); + }, + + /** + * Camera initialization callback + */ + setup_camera(e) { + console.log('Camera setup', e); + this.camera_context = wx.createCameraContext(); + + // Set up camera frame listener + this.camera_context.onCameraFrame((frame) => { + this.processFrame(frame); + }); + + this.setData({ + busy: false, + hint_text: '查找二维码' + }); + }, + + /** + * Process camera frame for QR detection + */ + processFrame(frame) { + if (this.data.busy) return; + + // TODO: Implement QR detection logic + console.log('Processing frame', frame.width, frame.height); + + // For demo purposes, simulate QR detection + if (Math.random() < 0.01) { // 1% chance to simulate QR found + this.simulateQRFound(); + } + }, + + /** + * Simulate QR code found (for testing) + */ + simulateQRFound() { + this.setData({ + hint_text: '识别成功', + show_modal: 'verifying' + }); + + // Simulate verification process + setTimeout(() => { + this.onVerificationSuccess('test-qr-code'); + }, 2000); + }, + + /** + * Handle successful QR verification + */ + onVerificationSuccess(qrCode) { + console.log('QR verification successful:', qrCode); + + if (this.data.return_page) { + wx.navigateTo({ + url: `${this.data.return_page}?qr_code=${encodeURIComponent(qrCode)}`, + success: () => { + console.log(`Navigated to return page: ${this.data.return_page}`); + }, + fail: (err) => { + console.error('Failed to navigate to return page:', err); + this.restart_camera(); + } + }); + } else { + console.warn('No return page specified'); + this.restart_camera(); + } + }, + + /** + * Toggle torch/flash + */ + toggle_torch() { + const newFlash = this.data.camera_flash === 'torch' ? 'off' : 'torch'; + this.setData({ + camera_flash: newFlash + }); + console.log('Torch toggled to:', newFlash); + }, + + /** + * Show scan guide + */ + show_scanguide() { + this.setData({ + show_modal: 'scanguide', + show_tip: false + }); + }, + + /** + * Show service modal + */ + show_service() { + this.setData({ + show_modal: 'service' + }); + }, + + /** + * Close modal and restart camera + */ + restart_camera() { + this.setData({ + show_modal: '', + hint_text: '查找二维码', + busy: false + }); + }, + + /** + * Close any modal + */ + close_modal() { + this.setData({ + show_modal: '' + }); + }, + + /** + * Debug tap handler + */ + debug_tap() { + const count = (this.debug_tap_count || 0) + 1; + this.debug_tap_count = count; + + if (count >= 5) { + this.setData({ + enable_debug: !this.data.enable_debug + }); + this.debug_tap_count = 0; + } + + // Clear count after 3 seconds + setTimeout(() => { + this.debug_tap_count = 0; + }, 3000); + }, + + /** + * Generate hint text based on QR detection result + */ + make_hint_text(result) { + if (result && result.qrcode && result.qrcode.length > 0) { + const err = result.err || ''; + if (err.includes('margin too small')) { + return '对齐定位点'; + } else if (err.includes('energy check failed') || err.includes('cannot detect angle')) { + return '移近一点'; + } + return '对齐定位点'; + } + return '查找二维码'; + } +}); \ No newline at end of file diff --git a/scanner/pages/emblemscanner/emblemscanner.json b/scanner/pages/emblemscanner/emblemscanner.json new file mode 100644 index 0000000..2fb78ae --- /dev/null +++ b/scanner/pages/emblemscanner/emblemscanner.json @@ -0,0 +1,3 @@ +{ + "navigationBarTitleText": "QR Scanner" +} \ No newline at end of file diff --git a/scanner/pages/emblemscanner/emblemscanner.wxml b/scanner/pages/emblemscanner/emblemscanner.wxml new file mode 100644 index 0000000..cd09593 --- /dev/null +++ b/scanner/pages/emblemscanner/emblemscanner.wxml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + {{ hint_text }} + + + + + + + + + + + + + + {{ item }} + model: {{ phone_model }} + zoom: {{ zoom }} + sensitivity: {{ camera_sensitivity }} + frame uploaded: {{ frame_uploaded }} (upload cost {{ frame_upload_time_cost }}ms) + max zoom: {{ max_zoom }} + result: {{ result }} + + + + + + + + + + + 扫描指南 + + + + + + + + 开启补光 + + + + + + + + + + 将QR码置于框内进行扫描 + + + + + + + 服务联系信息 + + + + + + + + + 正在验证中... + + + + + + + 验证失败 + + + + + + + + + 扫描指南 + 1. 将QR码置于框内 + 2. 保持稳定 + 3. 确保光线充足 + + + + \ No newline at end of file diff --git a/scanner/pages/emblemscanner/emblemscanner.wxss b/scanner/pages/emblemscanner/emblemscanner.wxss new file mode 100644 index 0000000..c67bc50 --- /dev/null +++ b/scanner/pages/emblemscanner/emblemscanner.wxss @@ -0,0 +1,246 @@ +/* Main container */ +view.wrapper { + width: 100%; + height: 100%; + background-size: cover; + position: relative; + overflow: hidden; +} + +/* WeChat camera */ +camera.camera { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + z-index: -3; +} + +/* Canvas for image processing */ +canvas#output { + width: 10px; + height: 10px; + position: absolute; + top: 0; + right: 0; + z-index: -10; +} + +/* QR targeting arcs overlay */ +view.qrarc.sm { + width: 350rpx; + height: 350rpx; + margin: 360rpx 200rpx; + position: absolute; + animation: qrarc-anime 1.2s ease-in-out infinite; +} + +view.qrarc.lg { + width: 550rpx; + height: 550rpx; + margin: 260rpx 100rpx; + position: absolute; + animation: qrarc-anime 1.2s ease-in-out infinite; +} + +view.qrarc image.arc { + position: absolute; + width: 15%; + height: 15%; + opacity: 0.9; +} + +view.qrarc image.arc.topright { + right: 0; + transform: rotate(90deg); +} + +view.qrarc image.arc.bottomleft { + bottom: 0; + transform: rotate(-90deg); +} + +view.qrarc image.arc.bottomright { + bottom: 0; + right: 0; + transform: rotate(180deg); +} + +@keyframes qrarc-anime { + 0% { + transform: scale(1); + } + 50% { + transform: scale(1.05); + } + 100% { + transform: scale(1); + } +} + +/* QR markers overlay */ +view.qrmarkers { + opacity: 70%; + margin: 100rpx 0; +} + +image.square { + width: 750rpx; + height: 750rpx; +} + +/* On-screen display for hints */ +view.osd { + position: fixed; + width: 100%; + top: 0; + color: #fff; + text-align: center; +} + +view.osd .upper { + border-radius: 20rpx; + background-color: rgba(0, 0, 0, 0.6); + font-size: 1.1rem; + display: inline-block; + margin: 130rpx auto 630rpx auto; + padding: 0.8rem 2rem; +} + +/* Bottom action controls */ +view.bottomfixed { + position: absolute; + width: 100%; + bottom: 0; + height: 200rpx; + background-color: #171616; + text-align: center; + border-top: 2px solid rgba(239, 72, 35, 0.7); + color: #707070; +} + +.actions { + font-size: 30rpx; +} + +.actions .icon image { + height: 30rpx; + width: 80rpx; +} + +view.half { + position: relative; + width: 50%; + display: inline-block; +} + +view.icon { + font-size: 20rpx; + margin: 30rpx 0 20rpx; +} + +view.text { + display: block; +} + +view.brighter view.text { + color: #eee; +} + +view.brighter { + color: #eee; +} + +/* Debug overlay */ +view.debug { + position: absolute; + width: 80%; + bottom: 240rpx; + left: 10px; + padding: 0.3rem; + border: 1px solid yellow; + border-radius: 3px; + color: yellow; + background-color: rgba(100, 100, 100, 0.8); + z-index: 1000; + font-size: 13px; + word-break: break-all; +} + +view.debug image { + position: fixed; + right: 10px; + top: 10px; + width: 64px; + height: 64px; + border: 1px solid green; +} + +/* Tooltip */ +view.tooltip { + position: fixed; + bottom: 310rpx; + left: 75rpx; +} + +.tooltip-content { + background-color: rgba(0, 0, 0, 0.8); + color: white; + padding: 20rpx; + border-radius: 10rpx; + font-size: 28rpx; +} + +/* Modal styles */ +.modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.modal-content { + background-color: white; + padding: 40rpx; + border-radius: 20rpx; + text-align: center; + max-width: 600rpx; + margin: 40rpx; +} + +.modal-content.verifying { + display: flex; + flex-direction: column; + align-items: center; + gap: 20rpx; +} + +.spinner { + width: 60rpx; + height: 60rpx; + border: 4rpx solid #f3f3f3; + border-top: 4rpx solid #3498db; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.modal-content button { + margin: 20rpx 10rpx; + padding: 20rpx 40rpx; + border: none; + border-radius: 10rpx; + background-color: #ef4823; + color: white; +} \ No newline at end of file