emblemscanner wip
This commit is contained in:
parent
0aacad31f4
commit
2df1973773
9
scanner/.claude/settings.local.json
Normal file
9
scanner/.claude/settings.local.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git add:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
62
scanner/CLAUDE.md
Normal file
62
scanner/CLAUDE.md
Normal 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
|
||||
@ -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",
|
||||
|
||||
@ -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);
|
||||
},
|
||||
|
||||
|
||||
});
|
||||
@ -1,65 +1,65 @@
|
||||
<view class="wrapper">
|
||||
<!-- QR targeting arcs overlay -->
|
||||
<view wx:if="{{ camera_rule != null }}" class="qrarc {{ qrarc_class }}">
|
||||
<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">
|
||||
<!-- STATE: LOADING -->
|
||||
<view wx:if="{{ app_state == 'loading' }}" class="loading-spinner">
|
||||
<view class="spinner"></view>
|
||||
<text>初始化相机...</text>
|
||||
</view>
|
||||
|
||||
<!-- WeChat native camera -->
|
||||
<camera wx:if="{{ show_modal == '' && !use_web_view && camera_rule != null }}"
|
||||
class="camera"
|
||||
flash="{{ camera_flash }}"
|
||||
mode="normal"
|
||||
frame-size="large"
|
||||
resolution="high"
|
||||
bindinitdone="setup_camera">
|
||||
</camera>
|
||||
<!-- STATE: SCANNING_CAMERA -->
|
||||
<block wx:if="{{ app_state == 'scanning_camera' }}">
|
||||
<!-- QR targeting arcs overlay -->
|
||||
<view class="qrarc {{ qrarc_class }}">
|
||||
<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 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 -->
|
||||
<canvas id="output" type="2d"></canvas>
|
||||
|
||||
<!-- Debug overlay -->
|
||||
<!-- Debug overlay (available in all states) -->
|
||||
<view class="debug" wx:if="{{ enable_debug }}">
|
||||
<view><image src="{{ debug_image_data_url }}"></image></view>
|
||||
<view wx:for="{{ debug_msgs }}">{{ item }}</view>
|
||||
<view>state: {{ app_state }} ({{ scan_mode }})</view>
|
||||
<view>model: {{ phone_model }}</view>
|
||||
<view>zoom: {{ zoom }} (rule: {{ rule_zoom }})</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>frame uploaded: {{ frame_uploaded }} (upload cost {{ frame_upload_time_cost }}ms)</view>
|
||||
<view>max zoom: {{ max_zoom }}</view>
|
||||
<view>worker: {{ use_worker ? 'yes' : 'no' }}</view>
|
||||
<view>result: {{ result }}</view>
|
||||
</view>
|
||||
|
||||
<!-- Bottom action controls -->
|
||||
<view class="bottomfixed">
|
||||
<!-- Bottom action controls (only for camera scanning) -->
|
||||
<view wx:if="{{ app_state == 'scanning_camera' }}" class="bottomfixed">
|
||||
<view class="actions">
|
||||
<view class="half {{ show_tip ? 'brighter' : '' }}" bindtap="show_scanguide">
|
||||
<view class="icon">
|
||||
|
||||
@ -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 {
|
||||
|
||||
142
scanner/pages/emblemscanner/libemblemscanner.js
Normal file
142
scanner/pages/emblemscanner/libemblemscanner.js
Normal 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
|
||||
};
|
||||
@ -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() {
|
||||
|
||||
}
|
||||
})
|
||||
@ -1,3 +0,0 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
<web-view wx:if="{{ url }}" src="{{url}}"></web-view>
|
||||
@ -1 +0,0 @@
|
||||
/* pages/productinfo/productinfo.wxss */
|
||||
121
scanner/pages/test_result_page/test_result_page.js
Normal file
121
scanner/pages/test_result_page/test_result_page.js
Normal 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)}`
|
||||
};
|
||||
}
|
||||
});
|
||||
3
scanner/pages/test_result_page/test_result_page.json
Normal file
3
scanner/pages/test_result_page/test_result_page.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "QR Scan Result"
|
||||
}
|
||||
56
scanner/pages/test_result_page/test_result_page.wxml
Normal file
56
scanner/pages/test_result_page/test_result_page.wxml
Normal 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>
|
||||
129
scanner/pages/test_result_page/test_result_page.wxss
Normal file
129
scanner/pages/test_result_page/test_result_page.wxss
Normal 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;
|
||||
}
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user