emblemscanner wip

This commit is contained in:
Fam Zheng 2025-09-13 10:10:26 +01:00
parent 0aacad31f4
commit 2df1973773
16 changed files with 765 additions and 305 deletions

View File

@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"Bash(git add:*)"
],
"deny": [],
"ask": []
}
}

62
scanner/CLAUDE.md Normal file
View File

@ -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

View File

@ -3,11 +3,11 @@
"pages/index/index", "pages/index/index",
"pages/camera/camera", "pages/camera/camera",
"pages/emblemscanner/emblemscanner", "pages/emblemscanner/emblemscanner",
"pages/test_result_page/test_result_page",
"pages/debugentry/debugentry", "pages/debugentry/debugentry",
"pages/debuguploaded/debuguploaded", "pages/debuguploaded/debuguploaded",
"pages/camwebview/camwebview", "pages/camwebview/camwebview",
"pages/camentry/camentry", "pages/camentry/camentry",
"pages/productinfo/productinfo",
"pages/test/test", "pages/test/test",
"pages/article/article", "pages/article/article",
"pages/nav/nav", "pages/nav/nav",

View File

@ -1,84 +1,16 @@
// QR Scanner Module - Self-contained QR scanning page // QR Scanner Module - Self-contained QR scanning page
// Adapted from existing camera implementation // Adapted from existing camera implementation
// Utility functions (copied from utils.js for self-contained module) // Import utility functions from library
var camera_rules = null; const {
get_system_info,
function get_system_info() { get_phone_model,
return wx.getSystemInfoSync(); get_camera_rule,
} make_query,
fetch_real_ip,
function get_phone_model() { get_tenant_id,
var ret = get_system_info().model; is_emblem_qr_pattern
console.log("phone model", ret); } = require('./libemblemscanner.js');
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;
}
Page({ Page({
/** /**
@ -91,7 +23,6 @@ Page({
phone_model: 'unknown', phone_model: 'unknown',
zoom: -1, zoom: -1,
max_zoom: 1, max_zoom: 1,
use_worker: false,
show_tip: false, show_tip: false,
show_modal: '', show_modal: '',
busy: true, busy: true,
@ -105,12 +36,18 @@ Page({
frame_upload_interval_ms: 2000, frame_upload_interval_ms: 2000,
return_page: '', // Page to navigate to after successful scan return_page: '', // Page to navigate to after successful scan
server_url: 'https://themblem.com', // Default server URL server_url: 'https://themblem.com', // Default server URL
real_ip: '', // User's real IP address
tenant_id: '', // Tenant identifier
debug_msgs: [], debug_msgs: [],
debug_image_data_url: '', debug_image_data_url: '',
rule_zoom: -1, rule_zoom: -1,
camera_rule: null, camera_rule: null,
use_web_view: false, 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) { onLoad(options) {
console.log('QR Scanner module loaded', options); console.log('QR Scanner module loaded', options);
// Store return page from query parameters // Store query parameters
if (options.return_page) { const force_camera = options.force_camera === '1' || options.force_camera === 'true';
this.setData({
return_page: options.return_page this.setData({
}); return_page: options.return_page || '',
} force_camera: force_camera
});
// Initialize image data storage // Initialize image data storage
this.image_data_urls = []; this.image_data_urls = [];
// Handle debug mode // Handle debug mode locally
options = options || {}; options = options || {};
if (options.debug || options.scene == 'debug') { const enable_debug = options.debug || options.scene == 'debug' || false;
getApp().globalData.debug = true;
}
const enable_debug = getApp().globalData.debug || false;
// Get system information // Get system information
this.initializeSystem(enable_debug); this.initializeSystem(enable_debug);
// Load camera rules // Fetch IP address and tenant info, then load camera rules
this.fetchRealIP();
this.fetchTenantID();
this.loadCameraRules(); 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 * Initialize system information and device detection
*/ */
initializeSystem(enable_debug) { initializeSystem(enable_debug) {
const systemInfo = get_system_info(); const systemInfo = get_system_info();
const phone_model = systemInfo.model; const phone_model = systemInfo.model;
const use_worker = phone_model.toLowerCase().includes('iphone');
this.setData({ this.setData({
enable_debug, enable_debug,
phone_model, phone_model,
window_width: systemInfo.windowWidth, window_width: systemInfo.windowWidth,
window_height: systemInfo.windowHeight, window_height: systemInfo.windowHeight
use_worker
}); });
console.log(`Phone model: ${phone_model}, Use worker: ${use_worker}`); console.log(`Phone model: ${phone_model}, Using native camera mode`);
// 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');
}, },
/** /**
@ -185,15 +121,20 @@ Page({
get_camera_rule(null, (rule) => { get_camera_rule(null, (rule) => {
console.log('Camera rule loaded:', 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; let emblem_camera_url = null;
// Set up web-view URL if needed // Set up web-view URL if needed
if (use_web_view) { if (should_use_webview) {
emblem_camera_url = "https://themblem.com/camera-5.0/?" + make_query(rule.zoom, this.data.return_page); 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}`); this.addDebugMessage(`Using web-view camera: ${emblem_camera_url}`);
} else { } 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({ this.setData({
@ -201,66 +142,45 @@ Page({
zoom: rule.zoom, zoom: rule.zoom,
rule_zoom: rule.zoom, rule_zoom: rule.zoom,
camera_sensitivity: rule.sensitivity || 1, camera_sensitivity: rule.sensitivity || 1,
use_web_view: use_web_view, use_web_view: should_use_webview,
emblem_camera_url: emblem_camera_url, emblem_camera_url: emblem_camera_url
busy: false
}); });
// Add rule info to debug messages // 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) { onCameraReady(e) {
console.log('Camera setup', e); console.log('Camera ready', e);
this.camera_context = wx.createCameraContext(); this.camera_context = wx.createCameraContext();
this.addDebugMessage('Camera initialized in scanCode mode');
// Set up camera frame listener // State transition is handled in loadCameraRules
this.camera_context.onCameraFrame((frame) => { },
this.processFrame(frame);
});
/**
* Camera error callback
*/
onCameraError(e) {
console.error('Camera error', e);
this.addDebugMessage(`Camera error: ${JSON.stringify(e.detail)}`);
this.setData({ this.setData({
busy: false, show_modal: 'verifyfailed',
hint_text: '查找二维码' 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 * 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() { restart_camera() {
this.setData({ this.restartScanning();
show_modal: '',
hint_text: '查找二维码',
busy: false
});
}, },
/** /**
@ -332,20 +248,92 @@ Page({
}, 3000); }, 3000);
}, },
/** /**
* Generate hint text based on QR detection result * State Machine: Transition to new state
*/ */
make_hint_text(result) { transitionToState(newState, mode = null) {
if (result && result.qrcode && result.qrcode.length > 0) { const oldState = this.data.app_state;
const err = result.err || ''; this.addDebugMessage(`State: ${oldState} -> ${newState}${mode ? ` (${mode})` : ''}`);
if (err.includes('margin too small')) {
return '对齐定位点'; const stateData = { app_state: newState };
} else if (err.includes('energy check failed') || err.includes('cannot detect angle')) { if (mode) stateData.scan_mode = mode;
return '移近一点';
} this.setData(stateData);
return '对齐定位点'; },
/**
* 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) { if (e.detail && e.detail.data && e.detail.data.length > 0) {
const messageData = e.detail.data[0]; const messageData = e.detail.data[0];
if (messageData.qr_code) { 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) { } else if (messageData.error) {
this.addDebugMessage(`Web-view error: ${messageData.error}`); this.addDebugMessage(`Web-view error: ${messageData.error}`);
this.setData({ this.setData({
show_modal: 'verifyfailed',
hint_text: '识别失败' hint_text: '识别失败'
}); });
} }
} }
} },
/**
* Check if QR code matches Emblem pattern
*/
isEmblemQRPattern(qrCode) {
return is_emblem_qr_pattern(qrCode);
},
}); });

View File

@ -1,65 +1,65 @@
<view class="wrapper"> <view class="wrapper">
<!-- QR targeting arcs overlay --> <!-- STATE: LOADING -->
<view wx:if="{{ camera_rule != null }}" class="qrarc {{ qrarc_class }}"> <view wx:if="{{ app_state == 'loading' }}" class="loading-spinner">
<image class="topleft arc" src="./assets/arc.png"></image>
<image class="topright arc" src="./assets/arc.png"></image>
<image class="bottomleft arc" src="./assets/arc.png"></image>
<image class="bottomright arc" src="./assets/arc.png"></image>
</view>
<!-- QR markers overlay -->
<view wx:if="{{ camera_rule != null }}" class="qrmarkers {{ qrmarkers_class }}">
<image class="square" src="./assets/qrmarkers.png"></image>
</view>
<!-- On-screen display for hints -->
<view wx:if="{{ camera_rule != null }}" class="osd">
<view class="upper" bindtap="debug_tap">
{{ hint_text }}
</view>
</view>
<!-- Loading spinner while camera rules are loading -->
<view wx:if="{{ show_modal == '' && camera_rule == null }}" class="loading-spinner">
<view class="spinner"></view> <view class="spinner"></view>
<text>初始化相机...</text> <text>初始化相机...</text>
</view> </view>
<!-- WeChat native camera --> <!-- STATE: SCANNING_CAMERA -->
<camera wx:if="{{ show_modal == '' && !use_web_view && camera_rule != null }}" <block wx:if="{{ app_state == 'scanning_camera' }}">
class="camera" <!-- QR targeting arcs overlay -->
flash="{{ camera_flash }}" <view class="qrarc {{ qrarc_class }}">
mode="normal" <image class="topleft arc" src="./assets/arc.png"></image>
frame-size="large" <image class="topright arc" src="./assets/arc.png"></image>
resolution="high" <image class="bottomleft arc" src="./assets/arc.png"></image>
bindinitdone="setup_camera"> <image class="bottomright arc" src="./assets/arc.png"></image>
</camera> </view>
<!-- QR markers overlay -->
<view class="qrmarkers {{ qrmarkers_class }}">
<image class="square" src="./assets/qrmarkers.png"></image>
</view>
<!-- On-screen display for hints -->
<view class="osd">
<view class="upper" bindtap="debug_tap">
{{ hint_text }}
</view>
</view>
<!-- WeChat native camera -->
<camera class="camera"
flash="{{ camera_flash }}"
bindready="onCameraReady"
binderror="onCameraError">
</camera>
</block>
<!-- STATE: SCANNING_WEBVIEW -->
<block wx:if="{{ app_state == 'scanning_webview' }}">
<!-- Web-view camera (no overlays) -->
<web-view src="{{ emblem_camera_url }}"
bindmessage="on_webview_message">
</web-view>
</block>
<!-- Web-view camera fallback for problematic devices -->
<web-view wx:if="{{ show_modal == '' && use_web_view && emblem_camera_url && camera_rule != null }}"
src="{{ emblem_camera_url }}"
bindmessage="on_webview_message">
</web-view>
<!-- Canvas for image processing --> <!-- Debug overlay (available in all states) -->
<canvas id="output" type="2d"></canvas>
<!-- Debug overlay -->
<view class="debug" wx:if="{{ enable_debug }}"> <view class="debug" wx:if="{{ enable_debug }}">
<view><image src="{{ debug_image_data_url }}"></image></view> <view><image src="{{ debug_image_data_url }}"></image></view>
<view wx:for="{{ debug_msgs }}">{{ item }}</view> <view wx:for="{{ debug_msgs }}">{{ item }}</view>
<view>state: {{ app_state }} ({{ scan_mode }})</view>
<view>model: {{ phone_model }}</view> <view>model: {{ phone_model }}</view>
<view>zoom: {{ zoom }} (rule: {{ rule_zoom }})</view> <view>zoom: {{ zoom }} (rule: {{ rule_zoom }})</view>
<view>camera rule: {{ camera_rule.model || 'default' }} (web_view: {{ use_web_view }})</view> <view>camera rule: {{ camera_rule.model || 'default' }} (web_view: {{ use_web_view }})</view>
<view wx:if="{{ force_camera }}">FORCE_CAMERA: enabled</view>
<view>sensitivity: {{ camera_sensitivity }}</view> <view>sensitivity: {{ camera_sensitivity }}</view>
<view>frame uploaded: {{ frame_uploaded }} (upload cost {{ frame_upload_time_cost }}ms)</view>
<view>max zoom: {{ max_zoom }}</view> <view>max zoom: {{ max_zoom }}</view>
<view>worker: {{ use_worker ? 'yes' : 'no' }}</view>
<view>result: {{ result }}</view> <view>result: {{ result }}</view>
</view> </view>
<!-- Bottom action controls --> <!-- Bottom action controls (only for camera scanning) -->
<view class="bottomfixed"> <view wx:if="{{ app_state == 'scanning_camera' }}" class="bottomfixed">
<view class="actions"> <view class="actions">
<view class="half {{ show_tip ? 'brighter' : '' }}" bindtap="show_scanguide"> <view class="half {{ show_tip ? 'brighter' : '' }}" bindtap="show_scanguide">
<view class="icon"> <view class="icon">

View File

@ -187,16 +187,19 @@ view.brighter {
view.debug { view.debug {
position: absolute; position: absolute;
width: 80%; width: 80%;
max-height: 30vh;
bottom: 240rpx; bottom: 240rpx;
left: 10px; left: 10px;
padding: 0.3rem; padding: 0.3rem;
border: 1px solid yellow; border: 1px solid yellow;
border-radius: 3px; border-radius: 3px;
color: yellow; color: yellow;
background-color: rgba(100, 100, 100, 0.8); background-color: rgba(100, 100, 100, 0.5);
z-index: 1000; z-index: 1000;
font-size: 13px; font-size: 13px;
word-break: break-all; word-break: break-all;
overflow-y: auto;
opacity: 0.5;
} }
view.debug image { view.debug image {

View File

@ -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
};

View File

@ -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() {
}
})

View File

@ -1,3 +0,0 @@
{
"usingComponents": {}
}

View File

@ -1 +0,0 @@
<web-view wx:if="{{ url }}" src="{{url}}"></web-view>

View File

@ -1 +0,0 @@
/* pages/productinfo/productinfo.wxss */

View File

@ -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)}`
};
}
});

View File

@ -0,0 +1,3 @@
{
"navigationBarTitleText": "QR Scan Result"
}

View File

@ -0,0 +1,56 @@
<view class="container">
<view class="header">
<text class="title">QR Code Scan Result</text>
<text class="timestamp">{{ scan_timestamp }}</text>
</view>
<view class="section">
<text class="section-title">QR Code Content</text>
<view class="content-box">
<text class="qr-content" selectable="true">{{ qr_code }}</text>
</view>
</view>
<view class="section">
<text class="section-title">Scan Details</text>
<view class="details-grid">
<view class="detail-item">
<text class="label">Scan Mode:</text>
<text class="value">{{ scan_mode }}</text>
</view>
<view class="detail-item">
<text class="label">Source Page:</text>
<text class="value">{{ source_page }}</text>
</view>
<view class="detail-item" wx:if="{{ qr_position }}">
<text class="label">Position:</text>
<text class="value">({{ qr_position.x }}, {{ qr_position.y }})</text>
</view>
<view class="detail-item" wx:if="{{ qr_position }}">
<text class="label">Centered:</text>
<text class="value">{{ qr_position.centered ? 'Yes' : 'No' }}</text>
</view>
<view class="detail-item" wx:if="{{ qr_sharpness > 0 }}">
<text class="label">Sharpness:</text>
<text class="value">{{ qr_sharpness.toFixed(3) }}</text>
</view>
<view class="detail-item" wx:if="{{ qr_size > 0 }}">
<text class="label">QR Size:</text>
<text class="value">{{ qr_size }}px</text>
</view>
</view>
</view>
<view class="section">
<text class="section-title">Raw Query Data</text>
<view class="content-box">
<text class="raw-data" selectable="true">{{ raw_query_string }}</text>
</view>
</view>
<view class="actions">
<button class="action-btn primary" bindtap="scanAgain">Scan Again</button>
<button class="action-btn secondary" bindtap="copyQRCode">Copy QR Code</button>
<button class="action-btn secondary" bindtap="goBack">Go Back</button>
</view>
</view>

View File

@ -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;
}

View File

@ -2,12 +2,26 @@
"condition": { "condition": {
"miniprogram": { "miniprogram": {
"list": [ "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", "name": "index-with-q-code-1279885739283",
"pathName": "pages/index/index", "pathName": "pages/index/index",
"query": "q=https%3A%2F%2Fthemblem.com%2Fapi%2Fmini-prog-entry%2F%3Fcode%3D1279885739283%0A", "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", "name": "pages/camera/camera",