diff --git a/scanner/.claude/settings.local.json b/scanner/.claude/settings.local.json new file mode 100644 index 0000000..5557eae --- /dev/null +++ b/scanner/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(git add:*)" + ], + "deny": [], + "ask": [] + } +} \ No newline at end of file diff --git a/scanner/CLAUDE.md b/scanner/CLAUDE.md new file mode 100644 index 0000000..b7f17c8 --- /dev/null +++ b/scanner/CLAUDE.md @@ -0,0 +1,62 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a WeChat Mini Program called "徵象" (emblem-scanner) for QR code scanning and processing. The app connects to the Themblem service for QR code verification and includes camera functionality with device-specific optimizations. + +## Architecture + +**Main App Structure:** +- `app.js` - Main application entry point with global data and worker initialization +- `utils.js` - Utility functions for camera rules, device detection, and QR code extraction +- `pages/` - WeChat Mini Program pages (index, camera, debug, webview, etc.) +- `components/` - Reusable UI components (scanguide, tooltips, modals, etc.) +- `worker/` - WebAssembly worker thread for QR code processing using qrtool library +- `static/` - Static assets and resources + +**Key Components:** +- Camera system with device-specific zoom rules fetched from API +- WebAssembly-based QR processing in worker thread for performance +- Dual camera modes: native camera (`pages/camera/`) and web view (`pages/camwebview/`) +- Upload and verification system connecting to themblem.com API + +**Global Data:** +- `server_url`: 'https://themblem.com' - main API endpoint +- `session_id`: Generated unique session identifier +- `real_ip`: User's IP address fetched from external service +- `worker`: WebAssembly worker for QR processing + +## Development Commands + +This is a WeChat Mini Program project. Development is done through WeChat Developer Tools IDE. + +**Linting:** +```bash +# ESLint configuration available but no npm scripts defined +# Lint manually using WeChat Developer Tools or external ESLint +``` + +**No test framework or build scripts are configured in package.json** + +## Key Files + +- `project.config.json` - WeChat Mini Program configuration +- `app.json` - App pages, window settings, and worker configuration +- `utils.js` - Core utility functions for camera and QR code handling +- `worker/index.js` - WebAssembly QR code processing worker +- `precheck.js` - QR code frame validation logic +- `upload.js` - Image upload and verification functions + +## Dependencies + +- `lottie-miniprogram` - Lottie animations support +- WebAssembly qrtool library (`qrtool.wx.js`) - QR code processing + +## External APIs + +- `https://themblem.com/api/v1/camera-rules/` - Device-specific camera settings +- `https://themblem.com/api/v1/check-auto-torch/` - Auto torch functionality +- `https://whatsmyip.hondcloud.com` - IP address detection +- `https://research.themblem.com/event/` - Event tracking endpoint \ No newline at end of file diff --git a/scanner/app.json b/scanner/app.json index 543d2c5..20fc997 100644 --- a/scanner/app.json +++ b/scanner/app.json @@ -3,11 +3,11 @@ "pages/index/index", "pages/camera/camera", "pages/emblemscanner/emblemscanner", + "pages/test_result_page/test_result_page", "pages/debugentry/debugentry", "pages/debuguploaded/debuguploaded", "pages/camwebview/camwebview", "pages/camentry/camentry", - "pages/productinfo/productinfo", "pages/test/test", "pages/article/article", "pages/nav/nav", diff --git a/scanner/pages/emblemscanner/emblemscanner.js b/scanner/pages/emblemscanner/emblemscanner.js index 520b900..66a59d8 100644 --- a/scanner/pages/emblemscanner/emblemscanner.js +++ b/scanner/pages/emblemscanner/emblemscanner.js @@ -1,84 +1,16 @@ // QR Scanner Module - Self-contained QR scanning page // Adapted from existing camera implementation -// Utility functions (copied from utils.js for self-contained module) -var camera_rules = null; - -function get_system_info() { - return wx.getSystemInfoSync(); -} - -function get_phone_model() { - var ret = get_system_info().model; - console.log("phone model", ret); - return ret; -} - -function match_camera_rules(model, rules, default_zoom) { - console.log(model, "apply zoom rules:", rules); - var best_match = null; - for (var rule of rules) { - if (model.toLowerCase().startsWith(rule.model.toLowerCase())) { - if (!best_match || rule.model.length > best_match.model.length) { - best_match = rule; - } - } - } - if (best_match) { - console.log("found best match", best_match); - return best_match; - } - var ret = { - zoom: default_zoom, - web_view: false - }; - console.log("using default", ret); - return ret; -} - -function get_camera_rule(max_zoom, cb) { - var default_zoom = 4; - if (max_zoom && max_zoom >= 60) { - /* - * 2024.06.01: in some Huawei/Honor models, the scale is different, use 40 - * in this case so we don't need to set up rules for each specific model - */ - console.log(`max zoom is ${max_zoom}, default zoom will be 40`); - default_zoom = 40; - } - if (camera_rules) { - let rule = match_camera_rules(get_phone_model(), camera_rules, default_zoom); - cb(rule); - } else { - var url = 'https://themblem.com/api/v1/camera-rules/'; - wx.request({ - url, - complete: (res) => { - var rules = res.data; - camera_rules = rules; - let rule = match_camera_rules(get_phone_model(), rules, default_zoom); - cb(rule); - } - }); - } -} - -function make_query(zoom, return_page) { - var gd = getApp().globalData; - var ret = "zoom=" + zoom; - var ui = wx.getStorageSync('userinfo') || {}; - ret += "&phonemodel=" + encodeURIComponent(get_phone_model()); - ret += "&realip=" + (gd.real_ip || ""); - ret += "&emblem_id=" + (ui.emblem_id || ""); - ret += "&nick_name=" + encodeURIComponent(ui.nickName || ""); - ret += "&tenant=" + (gd.tenant_id || ""); - ret += "&tk=" + Date.now(); - if (return_page) { - ret += "&return_page=" + encodeURIComponent(return_page); - } - console.log("Web-view query:", ret); - return ret; -} +// Import utility functions from library +const { + get_system_info, + get_phone_model, + get_camera_rule, + make_query, + fetch_real_ip, + get_tenant_id, + is_emblem_qr_pattern +} = require('./libemblemscanner.js'); Page({ /** @@ -91,7 +23,6 @@ Page({ phone_model: 'unknown', zoom: -1, max_zoom: 1, - use_worker: false, show_tip: false, show_modal: '', busy: true, @@ -105,12 +36,18 @@ Page({ frame_upload_interval_ms: 2000, return_page: '', // Page to navigate to after successful scan server_url: 'https://themblem.com', // Default server URL + real_ip: '', // User's real IP address + tenant_id: '', // Tenant identifier debug_msgs: [], debug_image_data_url: '', rule_zoom: -1, camera_rule: null, use_web_view: false, - emblem_camera_url: null + emblem_camera_url: null, + // State machine: loading -> scanning -> verifying -> result + app_state: 'loading', // 'loading', 'scanning_camera', 'scanning_webview', 'verifying', 'result' + scan_mode: 'unknown', // 'camera', 'webview' + force_camera: false // Override web-view rule, force native camera }, /** @@ -119,63 +56,62 @@ Page({ 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 - }); - } + // Store query parameters + const force_camera = options.force_camera === '1' || options.force_camera === 'true'; + + this.setData({ + return_page: options.return_page || '', + force_camera: force_camera + }); // Initialize image data storage this.image_data_urls = []; - // Handle debug mode + // Handle debug mode locally options = options || {}; - if (options.debug || options.scene == 'debug') { - getApp().globalData.debug = true; - } - const enable_debug = getApp().globalData.debug || false; + const enable_debug = options.debug || options.scene == 'debug' || false; // Get system information this.initializeSystem(enable_debug); - // Load camera rules + // Fetch IP address and tenant info, then load camera rules + this.fetchRealIP(); + this.fetchTenantID(); this.loadCameraRules(); }, + /** + * Fetch real IP address + */ + fetchRealIP() { + fetch_real_ip((err, ip) => { + this.setData({ real_ip: ip }); + }); + }, + + /** + * Fetch tenant ID (can be customized based on app logic) + */ + fetchTenantID() { + const tenant_id = get_tenant_id(); + this.setData({ tenant_id }); + }, + /** * Initialize system information and device detection */ initializeSystem(enable_debug) { const systemInfo = get_system_info(); const phone_model = systemInfo.model; - const use_worker = phone_model.toLowerCase().includes('iphone'); this.setData({ enable_debug, phone_model, window_width: systemInfo.windowWidth, - window_height: systemInfo.windowHeight, - use_worker + window_height: systemInfo.windowHeight }); - console.log(`Phone model: ${phone_model}, Use worker: ${use_worker}`); - - // Store phone model in global data - getApp().globalData.phone_model = phone_model; - - // 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'); + console.log(`Phone model: ${phone_model}, Using native camera mode`); }, /** @@ -185,15 +121,20 @@ Page({ get_camera_rule(null, (rule) => { console.log('Camera rule loaded:', rule); - const use_web_view = rule.web_view || false; + // Check for force_camera override + const should_use_webview = rule.web_view && !this.data.force_camera; let emblem_camera_url = null; // Set up web-view URL if needed - if (use_web_view) { - emblem_camera_url = "https://themblem.com/camera-5.0/?" + make_query(rule.zoom, this.data.return_page); + if (should_use_webview) { + emblem_camera_url = "https://themblem.com/camera-5.0/?" + make_query(rule.zoom, this.data.return_page, this.data.real_ip, this.data.tenant_id); this.addDebugMessage(`Using web-view camera: ${emblem_camera_url}`); } else { - this.addDebugMessage('Using native WeChat camera'); + if (this.data.force_camera && rule.web_view) { + this.addDebugMessage('Forcing native camera (override web-view rule)'); + } else { + this.addDebugMessage('Using native WeChat camera'); + } } this.setData({ @@ -201,66 +142,45 @@ Page({ zoom: rule.zoom, rule_zoom: rule.zoom, camera_sensitivity: rule.sensitivity || 1, - use_web_view: use_web_view, - emblem_camera_url: emblem_camera_url, - busy: false + use_web_view: should_use_webview, + emblem_camera_url: emblem_camera_url }); // Add rule info to debug messages - this.addDebugMessage(`Camera rule: zoom=${rule.zoom}, web_view=${rule.web_view}`); + this.addDebugMessage(`Camera rule: zoom=${rule.zoom}, web_view=${rule.web_view}${this.data.force_camera ? ' (FORCED_CAMERA)' : ''}`); + + // Transition to appropriate scanning state + if (should_use_webview) { + this.startWebviewScanning(); + } else { + this.startCameraScanning(); + } }); }, /** - * Camera initialization callback + * Camera ready callback */ - setup_camera(e) { - console.log('Camera setup', e); + onCameraReady(e) { + console.log('Camera ready', e); this.camera_context = wx.createCameraContext(); - - // Set up camera frame listener - this.camera_context.onCameraFrame((frame) => { - this.processFrame(frame); - }); + this.addDebugMessage('Camera initialized in scanCode mode'); + // State transition is handled in loadCameraRules + }, + /** + * Camera error callback + */ + onCameraError(e) { + console.error('Camera error', e); + this.addDebugMessage(`Camera error: ${JSON.stringify(e.detail)}`); this.setData({ - busy: false, - hint_text: '查找二维码' + show_modal: 'verifyfailed', + 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); - }, - /** - * 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 @@ -293,14 +213,10 @@ Page({ }, /** - * Close modal and restart camera + * Close modal and restart camera (legacy method name for WXML compatibility) */ restart_camera() { - this.setData({ - show_modal: '', - hint_text: '查找二维码', - busy: false - }); + this.restartScanning(); }, /** @@ -332,20 +248,92 @@ Page({ }, 3000); }, + /** - * Generate hint text based on QR detection result + * State Machine: Transition to new state */ - 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 '对齐定位点'; + transitionToState(newState, mode = null) { + const oldState = this.data.app_state; + this.addDebugMessage(`State: ${oldState} -> ${newState}${mode ? ` (${mode})` : ''}`); + + const stateData = { app_state: newState }; + if (mode) stateData.scan_mode = mode; + + this.setData(stateData); + }, + + /** + * State: Loading -> Scanning (Camera) + */ + startCameraScanning() { + this.transitionToState('scanning_camera', 'camera'); + this.setData({ + hint_text: '查找二维码', + busy: false + }); + }, + + /** + * State: Loading -> Scanning (Web-view) + */ + startWebviewScanning() { + this.transitionToState('scanning_webview', 'webview'); + this.setData({ + hint_text: '查找二维码', + busy: false + }); + }, + + /** + * State: Scanning -> Verifying (only for camera mode) + */ + startVerifying() { + this.transitionToState('verifying'); + this.setData({ + hint_text: '识别成功', + show_modal: 'verifying' + }); + }, + + /** + * State: Any -> Result (jump to return page) + */ + goToResult(qrCode) { + this.transitionToState('result'); + + if (this.data.return_page) { + wx.navigateTo({ + url: `${this.data.return_page}?qr_code=${encodeURIComponent(qrCode)}`, + success: () => { + this.addDebugMessage(`Navigated to: ${this.data.return_page}`); + }, + fail: (err) => { + this.addDebugMessage(`Navigation failed: ${err.errMsg}`); + this.restartScanning(); + } + }); + } else { + this.addDebugMessage('No return page specified'); + this.restartScanning(); } - return '查找二维码'; + }, + + /** + * State: Any -> Loading (restart) + */ + restartScanning() { + this.transitionToState('loading'); + this.setData({ + show_modal: '', + hint_text: '初始化相机...', + busy: true, + qr_position: null, + qr_sharpness: 0, + qr_size: 0 + }); + + // Reload camera rules to restart the flow + this.loadCameraRules(); }, /** @@ -381,14 +369,23 @@ Page({ if (e.detail && e.detail.data && e.detail.data.length > 0) { const messageData = e.detail.data[0]; if (messageData.qr_code) { - this.onVerificationSuccess(messageData.qr_code); + // Web-view results go directly to result (no verification step) + this.goToResult(messageData.qr_code); } else if (messageData.error) { this.addDebugMessage(`Web-view error: ${messageData.error}`); this.setData({ - show_modal: 'verifyfailed', hint_text: '识别失败' }); } } - } + }, + + /** + * Check if QR code matches Emblem pattern + */ + isEmblemQRPattern(qrCode) { + return is_emblem_qr_pattern(qrCode); + }, + + }); \ No newline at end of file diff --git a/scanner/pages/emblemscanner/emblemscanner.wxml b/scanner/pages/emblemscanner/emblemscanner.wxml index 88a0c0d..2f38668 100644 --- a/scanner/pages/emblemscanner/emblemscanner.wxml +++ b/scanner/pages/emblemscanner/emblemscanner.wxml @@ -1,65 +1,65 @@ - - - - - - - - - - - - - - - - - {{ hint_text }} - - - - - + + 初始化相机... - - - + + + + + + + + + + + + + + + + + + + {{ hint_text }} + + + + + + + + + + + + + + - - - - - - - + {{ item }} + state: {{ app_state }} ({{ scan_mode }}) model: {{ phone_model }} zoom: {{ zoom }} (rule: {{ rule_zoom }}) camera rule: {{ camera_rule.model || 'default' }} (web_view: {{ use_web_view }}) + FORCE_CAMERA: enabled sensitivity: {{ camera_sensitivity }} - frame uploaded: {{ frame_uploaded }} (upload cost {{ frame_upload_time_cost }}ms) max zoom: {{ max_zoom }} - worker: {{ use_worker ? 'yes' : 'no' }} result: {{ result }} - - + + diff --git a/scanner/pages/emblemscanner/emblemscanner.wxss b/scanner/pages/emblemscanner/emblemscanner.wxss index b54a928..ed6278c 100644 --- a/scanner/pages/emblemscanner/emblemscanner.wxss +++ b/scanner/pages/emblemscanner/emblemscanner.wxss @@ -187,16 +187,19 @@ view.brighter { view.debug { position: absolute; width: 80%; + max-height: 30vh; bottom: 240rpx; left: 10px; padding: 0.3rem; border: 1px solid yellow; border-radius: 3px; color: yellow; - background-color: rgba(100, 100, 100, 0.8); + background-color: rgba(100, 100, 100, 0.5); z-index: 1000; font-size: 13px; word-break: break-all; + overflow-y: auto; + opacity: 0.5; } view.debug image { diff --git a/scanner/pages/emblemscanner/libemblemscanner.js b/scanner/pages/emblemscanner/libemblemscanner.js new file mode 100644 index 0000000..e22083d --- /dev/null +++ b/scanner/pages/emblemscanner/libemblemscanner.js @@ -0,0 +1,142 @@ +// Emblem Scanner Library - Utility functions for QR scanning +// Self-contained utility functions for camera rules, device detection, and query building + +var camera_rules = null; + +/** + * Get system information + */ +function get_system_info() { + return wx.getSystemInfoSync(); +} + +/** + * Get phone model from system info + */ +function get_phone_model() { + var ret = get_system_info().model; + console.log("phone model", ret); + return ret; +} + +/** + * Match camera rules based on phone model + */ +function match_camera_rules(model, rules, default_zoom) { + console.log(model, "apply zoom rules:", rules); + var best_match = null; + for (var rule of rules) { + if (model.toLowerCase().startsWith(rule.model.toLowerCase())) { + if (!best_match || rule.model.length > best_match.model.length) { + best_match = rule; + } + } + } + if (best_match) { + console.log("found best match", best_match); + return best_match; + } + var ret = { + zoom: default_zoom, + web_view: false + }; + console.log("using default", ret); + return ret; +} + +/** + * Get camera rule from API or cache + */ +function get_camera_rule(max_zoom, cb) { + var default_zoom = 4; + if (max_zoom && max_zoom >= 60) { + /* + * 2024.06.01: in some Huawei/Honor models, the scale is different, use 40 + * in this case so we don't need to set up rules for each specific model + */ + console.log(`max zoom is ${max_zoom}, default zoom will be 40`); + default_zoom = 40; + } + if (camera_rules) { + let rule = match_camera_rules(get_phone_model(), camera_rules, default_zoom); + cb(rule); + } else { + var url = 'https://themblem.com/api/v1/camera-rules/'; + wx.request({ + url, + complete: (res) => { + var rules = res.data; + camera_rules = rules; + let rule = match_camera_rules(get_phone_model(), rules, default_zoom); + cb(rule); + } + }); + } +} + +/** + * Build query string for web-view camera + */ +function make_query(zoom, return_page, real_ip, tenant_id) { + var ret = "zoom=" + zoom; + var ui = wx.getStorageSync('userinfo') || {}; + ret += "&phonemodel=" + encodeURIComponent(get_phone_model()); + ret += "&realip=" + (real_ip || ""); + ret += "&emblem_id=" + (ui.emblem_id || ""); + ret += "&nick_name=" + encodeURIComponent(ui.nickName || ""); + ret += "&tenant=" + (tenant_id || ""); + ret += "&tk=" + Date.now(); + if (return_page) { + ret += "&return_page=" + encodeURIComponent(return_page); + } + console.log("Web-view query:", ret); + return ret; +} + +/** + * Fetch real IP address + */ +function fetch_real_ip(callback) { + wx.request({ + url: 'https://whatsmyip.hondcloud.com', + success: (res) => { + const ip = res.data || ''; + console.log('Real IP fetched:', ip); + callback(null, ip); + }, + fail: (err) => { + console.error('Failed to fetch real IP:', err); + callback(err, ''); + } + }); +} + +/** + * Get tenant ID from storage + */ +function get_tenant_id() { + const tenant_id = wx.getStorageSync('tenant_id') || ''; + console.log('Tenant ID:', tenant_id); + return tenant_id; +} + +/** + * Check if QR code matches Emblem pattern + */ +function is_emblem_qr_pattern(p) { + if (p.search(/code=[0-9a-zA-Z]+/) >= 0) return true; + if (p.search(/id=[0-9a-zA-Z]+/) >= 0) return true; + if (p.search(/c=[0-9a-zA-Z]+/) >= 0) return true; + if (p.search(/https:\/\/xy.ltd\/v\/[0-9a-zA-Z]+/) >= 0) return true; + return false; +} + +module.exports = { + get_system_info, + get_phone_model, + get_camera_rule, + make_query, + fetch_real_ip, + get_tenant_id, + is_emblem_qr_pattern +}; \ No newline at end of file diff --git a/scanner/pages/productinfo/productinfo.js b/scanner/pages/productinfo/productinfo.js deleted file mode 100644 index c74b1d2..0000000 --- a/scanner/pages/productinfo/productinfo.js +++ /dev/null @@ -1,71 +0,0 @@ -// pages/productinfo/productinfo.js -Page({ - - /** - * Page initial data - */ - data: { - url: null, - }, - - /** - * Lifecycle function--Called when page load - */ - onLoad(options) { - var base_url = getApp().globalData.server_url + '/api/product-info/'; - var url = base_url + options.serial_code + '/'; - console.log(url); - this.setData({ - url: url, - }) - }, - - /** - * Lifecycle function--Called when page is initially rendered - */ - onReady() { - - }, - - /** - * Lifecycle function--Called when page show - */ - onShow() { - - }, - - /** - * Lifecycle function--Called when page hide - */ - onHide() { - - }, - - /** - * Lifecycle function--Called when page unload - */ - onUnload() { - - }, - - /** - * Page event handler function--Called when user drop down - */ - onPullDownRefresh() { - - }, - - /** - * Called when page reach bottom - */ - onReachBottom() { - - }, - - /** - * Called when user click on the top right corner to share - */ - onShareAppMessage() { - - } -}) diff --git a/scanner/pages/productinfo/productinfo.json b/scanner/pages/productinfo/productinfo.json deleted file mode 100644 index 3928faa..0000000 --- a/scanner/pages/productinfo/productinfo.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "usingComponents": {} -} \ No newline at end of file diff --git a/scanner/pages/productinfo/productinfo.wxml b/scanner/pages/productinfo/productinfo.wxml deleted file mode 100644 index 790e32e..0000000 --- a/scanner/pages/productinfo/productinfo.wxml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/scanner/pages/productinfo/productinfo.wxss b/scanner/pages/productinfo/productinfo.wxss deleted file mode 100644 index b72229b..0000000 --- a/scanner/pages/productinfo/productinfo.wxss +++ /dev/null @@ -1 +0,0 @@ -/* pages/productinfo/productinfo.wxss */ \ No newline at end of file diff --git a/scanner/pages/test_result_page/test_result_page.js b/scanner/pages/test_result_page/test_result_page.js new file mode 100644 index 0000000..347dbd6 --- /dev/null +++ b/scanner/pages/test_result_page/test_result_page.js @@ -0,0 +1,121 @@ +// Test Result Page - Display QR scan results for testing +Page({ + /** + * Page initial data + */ + data: { + qr_code: '', + scan_timestamp: '', + scan_mode: 'unknown', + source_page: 'unknown', + qr_position: null, + qr_sharpness: 0, + qr_size: 0, + raw_query_string: '' + }, + + /** + * Lifecycle function--Called when page load + */ + onLoad(options) { + console.log('Test result page loaded with options:', options); + + // Parse all query parameters + const timestamp = new Date().toLocaleString(); + const qr_code = options.qr_code || 'No QR code provided'; + + // Parse additional data if provided + let qr_position = null; + let qr_sharpness = 0; + let qr_size = 0; + + try { + if (options.qr_position) { + qr_position = JSON.parse(decodeURIComponent(options.qr_position)); + } + if (options.qr_sharpness) { + qr_sharpness = parseFloat(options.qr_sharpness); + } + if (options.qr_size) { + qr_size = parseInt(options.qr_size); + } + } catch (error) { + console.warn('Error parsing additional QR data:', error); + } + + // Generate raw query string for debugging + const raw_query_string = Object.keys(options) + .map(key => `${key}=${options[key]}`) + .join('\n'); + + this.setData({ + qr_code: decodeURIComponent(qr_code), + scan_timestamp: timestamp, + scan_mode: options.scan_mode || 'unknown', + source_page: options.source_page || 'unknown', + qr_position: qr_position, + qr_sharpness: qr_sharpness, + qr_size: qr_size, + raw_query_string: raw_query_string + }); + }, + + /** + * Scan again - go back to emblemscanner + */ + scanAgain() { + wx.redirectTo({ + url: '/pages/emblemscanner/emblemscanner?debug=1', + fail: (err) => { + console.error('Failed to navigate to scanner:', err); + wx.showToast({ + title: 'Navigation failed', + icon: 'error' + }); + } + }); + }, + + /** + * Copy QR code to clipboard + */ + copyQRCode() { + wx.setClipboardData({ + data: this.data.qr_code, + success: () => { + wx.showToast({ + title: 'QR code copied', + icon: 'success' + }); + }, + fail: () => { + wx.showToast({ + title: 'Copy failed', + icon: 'error' + }); + } + }); + }, + + /** + * Go back to previous page + */ + goBack() { + wx.navigateBack({ + fail: () => { + // If can't go back, go to scanner + this.scanAgain(); + } + }); + }, + + /** + * Share this page (for testing) + */ + onShareAppMessage() { + return { + title: 'QR Scan Result', + path: `/pages/test_result_page/test_result_page?qr_code=${encodeURIComponent(this.data.qr_code)}` + }; + } +}); \ No newline at end of file diff --git a/scanner/pages/test_result_page/test_result_page.json b/scanner/pages/test_result_page/test_result_page.json new file mode 100644 index 0000000..bc77963 --- /dev/null +++ b/scanner/pages/test_result_page/test_result_page.json @@ -0,0 +1,3 @@ +{ + "navigationBarTitleText": "QR Scan Result" +} \ No newline at end of file diff --git a/scanner/pages/test_result_page/test_result_page.wxml b/scanner/pages/test_result_page/test_result_page.wxml new file mode 100644 index 0000000..325893b --- /dev/null +++ b/scanner/pages/test_result_page/test_result_page.wxml @@ -0,0 +1,56 @@ + + + QR Code Scan Result + {{ scan_timestamp }} + + + + QR Code Content + + {{ qr_code }} + + + + + Scan Details + + + Scan Mode: + {{ scan_mode }} + + + Source Page: + {{ source_page }} + + + Position: + ({{ qr_position.x }}, {{ qr_position.y }}) + + + Centered: + {{ qr_position.centered ? 'Yes' : 'No' }} + + + Sharpness: + {{ qr_sharpness.toFixed(3) }} + + + QR Size: + {{ qr_size }}px + + + + + + Raw Query Data + + {{ raw_query_string }} + + + + + + + + + \ No newline at end of file diff --git a/scanner/pages/test_result_page/test_result_page.wxss b/scanner/pages/test_result_page/test_result_page.wxss new file mode 100644 index 0000000..dec3072 --- /dev/null +++ b/scanner/pages/test_result_page/test_result_page.wxss @@ -0,0 +1,129 @@ +.container { + padding: 40rpx; + background-color: #f8f8f8; + min-height: 100vh; +} + +.header { + text-align: center; + margin-bottom: 60rpx; +} + +.title { + display: block; + font-size: 48rpx; + font-weight: bold; + color: #333; + margin-bottom: 20rpx; +} + +.timestamp { + font-size: 28rpx; + color: #666; +} + +.section { + background-color: white; + border-radius: 20rpx; + padding: 40rpx; + margin-bottom: 40rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1); +} + +.section-title { + display: block; + font-size: 32rpx; + font-weight: bold; + color: #333; + margin-bottom: 30rpx; + border-bottom: 2rpx solid #eee; + padding-bottom: 20rpx; +} + +.content-box { + background-color: #f5f5f5; + border-radius: 10rpx; + padding: 30rpx; + border-left: 8rpx solid #007aff; +} + +.qr-content { + font-size: 30rpx; + color: #333; + line-height: 1.6; + word-break: break-all; + font-family: monospace; +} + +.raw-data { + font-size: 24rpx; + color: #666; + line-height: 1.5; + word-break: break-all; + font-family: monospace; +} + +.details-grid { + display: flex; + flex-direction: column; + gap: 20rpx; +} + +.detail-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20rpx 0; + border-bottom: 1rpx solid #eee; +} + +.detail-item:last-child { + border-bottom: none; +} + +.label { + font-size: 28rpx; + color: #666; + font-weight: 500; +} + +.value { + font-size: 28rpx; + color: #333; + font-weight: normal; + font-family: monospace; +} + +.actions { + display: flex; + flex-direction: column; + gap: 30rpx; + margin-top: 60rpx; +} + +.action-btn { + height: 90rpx; + border-radius: 45rpx; + font-size: 32rpx; + font-weight: bold; + border: none; +} + +.action-btn.primary { + background-color: #007aff; + color: white; +} + +.action-btn.secondary { + background-color: white; + color: #007aff; + border: 2rpx solid #007aff; +} + +.action-btn.primary:active { + background-color: #005bb5; +} + +.action-btn.secondary:active { + background-color: #f0f8ff; +} \ No newline at end of file diff --git a/scanner/project.private.config.json b/scanner/project.private.config.json index 7e50b56..ad8353f 100644 --- a/scanner/project.private.config.json +++ b/scanner/project.private.config.json @@ -2,12 +2,26 @@ "condition": { "miniprogram": { "list": [ + { + "name": "emblemscanner", + "pathName": "pages/emblemscanner/emblemscanner", + "query": "debug=1&return_page=/pages/test_result_page/test_result_page", + "scene": null, + "launchMode": "default" + }, + { + "name": "emblemscanner force camera", + "pathName": "pages/emblemscanner/emblemscanner", + "query": "debug=1&force_camera=1", + "launchMode": "default", + "scene": null + }, { "name": "index-with-q-code-1279885739283", "pathName": "pages/index/index", "query": "q=https%3A%2F%2Fthemblem.com%2Fapi%2Fmini-prog-entry%2F%3Fcode%3D1279885739283%0A", - "scene": null, - "launchMode": "default" + "launchMode": "default", + "scene": null }, { "name": "pages/camera/camera",