Merge branch 'feat/camcard' into dev/1.0
This commit is contained in:
19
pages.json
19
pages.json
@@ -1220,11 +1220,20 @@
|
||||
"diy-text": "view",
|
||||
"diy-video": "view",
|
||||
"diy-video-list": "view"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "index/diy",
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "webview/webview",
|
||||
"style": {
|
||||
// #ifdef APP-PLUS
|
||||
"navigationStyle": "custom",
|
||||
// #endif
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "index/diy",
|
||||
"style": {
|
||||
// #ifdef APP-PLUS
|
||||
"navigationStyle": "custom",
|
||||
|
||||
82
pages_tool/webview/webview.vue
Normal file
82
pages_tool/webview/webview.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<view :style="themeColor">
|
||||
<!-- #ifndef MP-HARMONY -->
|
||||
<view class="iconfont iconshang navigate-back" @click="navigateBack"></view>
|
||||
<!-- #endif -->
|
||||
<web-view :src="src" @titleUpdate="updateNavbarTitle"></web-view>
|
||||
|
||||
<!-- #ifdef MP-HARMONY -->
|
||||
<cover-view class="iconfont iconshang navigate-back" @click="navigateBack"></cover-view>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- 隐私弹窗 -->
|
||||
<privacy-popup ref="privacyPopup"></privacy-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { decodeEntity } from '@/common/js/util.js';
|
||||
|
||||
|
||||
/**
|
||||
* 必须真机调试
|
||||
* 在微信模拟器中,行为和真机不一样
|
||||
* 1. 模拟器中,设置标题不生效,但是,在真机是可以的。
|
||||
*/
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
src: '',
|
||||
title: '', // 标题栏标题
|
||||
enableSetTitle: false, // 是否强制设置标题,用来替换Webview中的默认标题
|
||||
};
|
||||
},
|
||||
onLoad(option) {
|
||||
this.title = decodeURIComponent(option?.title ?? '');
|
||||
this.enableSetTitle = decodeURIComponent(option?.enable_set_title) == 1;
|
||||
|
||||
// #ifdef H5
|
||||
this.src = decodeEntity(decodeURIComponent(option.src));
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
this.src = decodeEntity(decodeURIComponent(`${option.src}&from=mp-webview`));
|
||||
// #endif
|
||||
},
|
||||
onShow() {
|
||||
this.updateNavbarTitle();
|
||||
},
|
||||
watch: {
|
||||
title(newVal) {
|
||||
this.updateNavbarTitle(newVal);
|
||||
},
|
||||
enableSetTitle() {
|
||||
this.updateNavbarTitle();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
navigateBack() {
|
||||
uni.navigateBack({
|
||||
delta: 1
|
||||
});
|
||||
},
|
||||
updateNavbarTitle(newTitle) {
|
||||
const title = newTitle ?? this.title;
|
||||
if (this.enableSetTitle) {
|
||||
uni.setNavigationBarTitle({title});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.navigate-back {
|
||||
position: absolute;
|
||||
top: 34rpx;
|
||||
left: 34rpx;
|
||||
z-index: 5;
|
||||
font-size: $font-size-toolbar;
|
||||
}
|
||||
</style>
|
||||
127
tests/card-mini-code-scene.test.js
Normal file
127
tests/card-mini-code-scene.test.js
Normal file
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* 名片小程序码 · scene 解析契约测试(2.21)
|
||||
*
|
||||
* 背景:
|
||||
* 微信 getwxacodeunlimit 的 scene 只有 32 可见字符, 装不下业务 card_id
|
||||
* (形如 c_1_shop_20261013111612_9588EA, 30 字符; 加前缀更长),
|
||||
* 故 scene 里放的是名片**主键 id**(数字): "id=448"。
|
||||
*
|
||||
* 扫码进入小程序后, 微信把 scene 放在 options.scene(整体 encodeURIComponent
|
||||
* 过), 小程序端需:
|
||||
* ① decodeURIComponent(options.scene)
|
||||
* ② 解析出主键 id
|
||||
* ③ 用 id 向后端换业务 card_id(后端 /card/api/diy/card 支持 id 寻址)
|
||||
*
|
||||
* 覆盖:
|
||||
* 1. scene 解析: "id=448" / 裸 "448" / URL 编码形态
|
||||
* 2. 常规入口(query 带 card_id)不受影响
|
||||
* 3. 解析不出时走「缺少名片编号」提示(不得白屏)
|
||||
* 4. 换 card_id 走公开读接口且带 id 参数
|
||||
* 5. 反例: 不得把 scene 当 card_id 直接用(会查不到名片)
|
||||
*
|
||||
* 跑法: node --test tests/card-mini-code-scene.test.js
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const PAGE = path.resolve(__dirname, '../pages_tool/card/detail.vue');
|
||||
const src = fs.readFileSync(PAGE, 'utf8');
|
||||
// 先剥注释再断言 —— 注释里写「绝不能写 X」会被自判违规
|
||||
const strip = s => s.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
||||
|
||||
const body = strip(src);
|
||||
|
||||
// ================================================================
|
||||
// 1. scene 解析
|
||||
// ================================================================
|
||||
test('scene 解析: 从 options.scene 取名片主键 id', () => {
|
||||
assert.match(body, /pickSceneRelId\s*\(\s*options\s*\)/);
|
||||
// 必须先 decodeURIComponent(微信对 scene 整体做过编码)
|
||||
assert.match(body, /decodeURIComponent\(opt\.scene\)/);
|
||||
// 解析 "id=xxx" 形态
|
||||
assert.match(body, /kv\[0\]\s*===\s*'id'/);
|
||||
});
|
||||
|
||||
test('scene 解析: 兼容裸 id(无前缀)', () => {
|
||||
// 无 "=" 时整体当 id
|
||||
assert.match(body, /let val = scene;/);
|
||||
assert.match(body, /String\(val\)\.replace\(\/\[\^0-9\]\/g, ''\)/);
|
||||
});
|
||||
|
||||
test('scene 解析: 只取数字(防脏值/防注入)', () => {
|
||||
assert.match(body, /replace\(\/\[\^0-9\]\/g, ''\)/);
|
||||
assert.match(body, /parseInt\(val, 10\)/);
|
||||
});
|
||||
|
||||
test('scene 解析: 取不到时返回 0', () => {
|
||||
assert.match(body, /return val \? parseInt\(val, 10\) : 0;/);
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// 2. 常规入口不受影响
|
||||
// ================================================================
|
||||
test('常规入口: query 带 card_id 时直接使用', () => {
|
||||
assert.match(body, /pickDirectCardId\s*\(\s*options\s*\)/);
|
||||
assert.match(body, /if \(opt\.card_id\)/);
|
||||
assert.match(body, /decodeURIComponent\(opt\.card_id\)/);
|
||||
});
|
||||
|
||||
test('onLoad: 常规入口直连, scene 入口走换卡流程', () => {
|
||||
assert.match(body, /const directId = this\.pickDirectCardId\(options\);/);
|
||||
assert.match(body, /const sceneId = this\.pickSceneRelId\(options\);/);
|
||||
assert.match(body, /if \(directId\)\s*\{[\s\S]*?this\.cardId = directId;/);
|
||||
assert.match(body, /else if \(sceneId\)\s*\{\s*this\.resolveCardIdByRelId\(sceneId\);/);
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// 3. 换 card_id
|
||||
// ================================================================
|
||||
test('换 card_id: 走公开读接口且带 id 参数', () => {
|
||||
assert.match(body, /resolveCardIdByRelId\s*\(\s*relId\s*\)\s*\{/);
|
||||
assert.match(body, /url:\s*'\/card\/api\/diy\/card'/);
|
||||
assert.match(body, /data:\s*\{\s*id:\s*relId\s*\}/);
|
||||
});
|
||||
|
||||
test('换 card_id: 从返回里取业务 card_id', () => {
|
||||
assert.match(body, /res\.data && res\.data\.card/);
|
||||
assert.match(body, /card\.card_id/);
|
||||
assert.match(body, /this\.cardId = bizId;/);
|
||||
});
|
||||
|
||||
test('换 card_id: 失败时给明确提示(不白屏)', () => {
|
||||
assert.match(body, /errorMsg = '名片不存在或已被删除'/);
|
||||
assert.match(body, /errorMsg = '网络异常,请稍后重试'/);
|
||||
});
|
||||
|
||||
test('换 card_id: 成功后继续加载详情', () => {
|
||||
assert.match(body, /this\.cardId = bizId;[\s\S]*?this\.loadDetail\(\);/);
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// 4. 缺少名片编号的兜底
|
||||
// ================================================================
|
||||
test('既无 card_id 也无 scene: 提示「缺少名片编号」', () => {
|
||||
assert.match(body, /this\.errorMsg = '缺少名片编号';/);
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// 5. 反例(防恒真 / 防走偏)
|
||||
// ================================================================
|
||||
test('反例: 不得把 scene 整体当 card_id 直接用', () => {
|
||||
// 若把 scene 当 card_id 传, 后端按业务 card_id 查不到 → 必然查不到名片
|
||||
assert.doesNotMatch(body, /this\.cardId = decodeURIComponent\(opt\.scene\)/);
|
||||
assert.doesNotMatch(body, /card_id:\s*decodeURIComponent\(opt\.scene\)/);
|
||||
});
|
||||
|
||||
test('反例: 不得用 card_id= 前缀解析 scene(scene 用 id= 前缀)', () => {
|
||||
assert.doesNotMatch(body, /kv\[0\]\s*===\s*'card_id'/);
|
||||
});
|
||||
|
||||
test('反例: 不得硬编码名片 id', () => {
|
||||
assert.doesNotMatch(body, /card_id:\s*['"]\d+['"]/);
|
||||
});
|
||||
Reference in New Issue
Block a user