From d789dd71423a366ffa70b1b5b63b6aa0b6d096f4 Mon Sep 17 00:00:00 2001
From: ZF sun <34314687@qq.com>
Date: Wed, 28 Oct 2026 17:41:16 +0800
Subject: [PATCH] =?UTF-8?q?feat(card):=20=E5=A2=9E=E5=8A=A0=E6=9C=89?=
=?UTF-8?q?=E6=95=88=E6=9C=9F=E5=B1=95=E7=A4=BA=E5=8A=9F=E8=83=BD=EF=BC=8C?=
=?UTF-8?q?=E6=94=AF=E6=8C=81=E7=8B=AC=E7=AB=8B=E6=88=90=E5=9D=97=E6=A0=B7?=
=?UTF-8?q?=E5=BC=8F=E4=B8=8E=E7=AE=80=E4=BB=8B=E6=A0=B7=E5=BC=8F=E9=85=8D?=
=?UTF-8?q?=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
components-diy/diy-card-mini.vue | 20 +++++-
tests/card-share-card-id.test.js | 103 +++++++++++++++++++++++++++++-
tests/diy-card-bio-social.test.js | 33 ++++++++++
3 files changed, 153 insertions(+), 3 deletions(-)
diff --git a/components-diy/diy-card-mini.vue b/components-diy/diy-card-mini.vue
index 3617ed8..912d02a 100644
--- a/components-diy/diy-card-mini.vue
+++ b/components-diy/diy-card-mini.vue
@@ -177,12 +177,13 @@
{{ qrTipEm }}{{ qrTipLine1 }}{{ qrTipSub }}{{ qrUrlText }}
-
-
+
+
{{ t }}
{{ sourceName }}
+ 有效期至 {{ expireDateText }}
@@ -1163,6 +1164,21 @@ export default {
showSourceRow() {
return flagOn(this.c.showSource, false) && !!this.sourceName;
},
+ // ---- 2.24 有效期(自动展示: 名片设了 expire_at 就显示, 无独立开关) ----
+ showExpireRow() {
+ return !!(this.card && num(this.card.expire_at, 0) > 0);
+ },
+ expireDateText() {
+ const ts = num(this.card && this.card.expire_at, 0);
+ if (ts <= 0) return '';
+ const d = new Date(ts * 1000);
+ const p = (n) => (n < 10 ? '0' + n : '' + n);
+ return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
+ },
+ // ---- 2.24 标签与来源(meta)区块: 可独立成块 + 逐向间距(与 bio/social 同款) ----
+ metaFinalStyle() {
+ return blockFinalStyle(this.c, 'meta');
+ },
/**
* 二维码是否「独立成块」: 后台「二维码独立样式」开关(与资料区同套逻辑)。
* 跟随卡片 → false(与上方资料同底连为一体, 无白卡/描边/阴影);
diff --git a/tests/card-share-card-id.test.js b/tests/card-share-card-id.test.js
index 1b599cc..e9efff9 100644
--- a/tests/card-share-card-id.test.js
+++ b/tests/card-share-card-id.test.js
@@ -115,7 +115,34 @@ test('A6: pickShareCardId 无 dataset 时回落 storage(朋友圈通道)', () =>
assert.equal(pickShareCardId({ from: 'menu' }), 'C_FROM_STORAGE');
});
-test('A7: pickShareCardId 两路都没有 → 空', () => {
+test('A6b: 第三级兜底 — storage 空且落地 query 带 card_id → 续传(右上角转发 S5-2)', () => {
+ // 场景: 访客经分享落地(query 带 card_id), 未点过任何分享按钮(storage 空),
+ // 直接右上角「···」转发 → 应自动续传落地页的源头卡
+ const uni = makeUni();
+ const { pickShareCardId } = loadCardShare(uni, () => ([{
+ route: 'pages_tool/index/diy',
+ options: { id: '3', card_id: 'C_LANDING' },
+ }]));
+ assert.equal(pickShareCardId({ from: 'menu' }), 'C_LANDING');
+ assert.equal(pickShareCardId(null), 'C_LANDING');
+});
+
+test('A6c: 三级优先级 dataset > storage > 落地 query', () => {
+ const uni = makeUni();
+ const { rememberShareCardId, pickShareCardId } = loadCardShare(uni, () => ([{
+ route: 'pages_tool/index/diy',
+ options: { card_id: 'C_LANDING' },
+ }]));
+ // query 在, 但 dataset 最准 → dataset 胜
+ assert.equal(pickShareCardId({ target: { dataset: { cardId: 'C_DATASET' } } }), 'C_DATASET');
+ // storage 在(刚点过按钮), dataset 无 → storage 胜(按钮意图更近)
+ rememberShareCardId('C_FROM_BTN');
+ assert.equal(pickShareCardId(null), 'C_FROM_BTN');
+ // 都没有 → 落地 query
+ assert.equal(pickShareCardId({ from: 'menu' }), 'C_LANDING');
+});
+
+test('A7: pickShareCardId 三路都没有 → 空', () => {
const uni = makeUni();
const { pickShareCardId } = loadCardShare(uni, () => []);
assert.equal(pickShareCardId({ from: 'menu' }), '');
@@ -280,6 +307,80 @@ test('B9: 行为级 — 朋友圈无 card_id 时原样回落', () => {
assert.equal(hooks.onShareTimeline.call(ctx), ctx.mpShareData.timeLine);
});
+// =============================================================
+// B+. H5 公众号分享链接补 card_id(setPublicShare, S10-1)
+// =============================================================
+
+/** 把 diy.js 的 setPublicShare 抠出来真跑(同 loadDiyShareHooks 思路) */
+function loadDiySetPublicShare(share) {
+ const src = read('common/js/diy.js');
+ const start = src.indexOf('setPublicShare() {');
+ const end = src.indexOf('onPageScroll(e) {');
+ assert.ok(start > 0 && end > start, '未能定位 diy.js 的 setPublicShare');
+ // 剥注释后, 丢掉切片尾部的外层 methods 闭括号(setPublicShare 自己的 '}' 是倒数第二个)
+ let body = strip(src.slice(start, end));
+ body = body.slice(0, body.lastIndexOf('}')).trim();
+ const factory = new Function('routeQueryValue', 'appendPathParam', 'return {' + body + '};');
+ // 工厂返回 { setPublicShare: fn } 对象, 这里直接交出方法本体
+ return factory(share.routeQueryValue, share.appendPathParam).setPublicShare;
+}
+
+test('B10: diy.js 引入 routeQueryValue 且 setPublicShare 补 card_id', () => {
+ const src = read('common/js/diy.js');
+ assert.match(src, /import\s*\{[^}]*routeQueryValue[^}]*\}\s*from\s*['"]@\/common\/js\/card-share\.js['"]/,
+ '未引入 card-share.js 的 routeQueryValue');
+ const stripped = strip(src);
+ const start = stripped.indexOf('setPublicShare()');
+ const end = stripped.indexOf('onPageScroll');
+ assert.ok(start > 0 && end > start, '未定位到 setPublicShare');
+ const seg = stripped.slice(start, end);
+ assert.match(seg, /routeQueryValue\('card_id'\)/, 'setPublicShare 未从落地 query 取 card_id');
+ assert.match(seg, /appendPathParam\(shareUrl, 'card_id', shareCardId\)/, '分享链接未追加 card_id');
+});
+
+test('B11: 行为级 — H5 落地带 card_id 时公众号分享链接续传', () => {
+ const uni = makeUni();
+ const share = loadCardShare(uni, () => ([{
+ route: 'pages_tool/index/diy',
+ options: { id: '3', card_id: 'C_1_shop_x' },
+ }]));
+ const setPublicShare = loadDiySetPublicShare(share);
+ let captured = null;
+ const ctx = {
+ $config: { h5Domain: 'https://h5.example.com' },
+ diyRoute: '/pages_tool/index/diy',
+ id: 3,
+ name: '',
+ $util: { setPublicShare: d => { captured = d; }, img: s => s || '' },
+ $langConfig: { getField: () => '标题' },
+ diyData: { global: {} },
+ siteInfo: null,
+ };
+ setPublicShare.call(ctx);
+ assert.equal(captured.link, 'https://h5.example.com/pages_tool/index/diy?id=3&card_id=C_1_shop_x',
+ 'H5 分享链接未续传落地 card_id');
+});
+
+test('B12: 行为级 — 裸 Tab(无 card_id)时 H5 分享链接行为零变化', () => {
+ const uni = makeUni();
+ const share = loadCardShare(uni, () => ([]));
+ const setPublicShare = loadDiySetPublicShare(share);
+ let captured = null;
+ const ctx = {
+ $config: { h5Domain: 'https://h5.example.com' },
+ diyRoute: '/pages_tool/index/diy',
+ id: 3,
+ name: '',
+ $util: { setPublicShare: d => { captured = d; }, img: s => s || '' },
+ $langConfig: { getField: () => '标题' },
+ diyData: { global: {} },
+ siteInfo: null,
+ };
+ setPublicShare.call(ctx);
+ assert.equal(captured.link, 'https://h5.example.com/pages_tool/index/diy?id=3',
+ '无 card_id 时链接应保持原样(不得追加空参数)');
+});
+
// =============================================================
// C. 名片基件的分享按钮(diy-card-mini.vue)
// =============================================================
diff --git a/tests/diy-card-bio-social.test.js b/tests/diy-card-bio-social.test.js
index 4575a5d..1c1017c 100644
--- a/tests/diy-card-bio-social.test.js
+++ b/tests/diy-card-bio-social.test.js
@@ -104,3 +104,36 @@ test('多语言: 请求带 lang 参数(后端按 _en 优先下发)', () => {
f + ' 请求未带 lang 参数');
}
});
+
+// ---- 2.24: 简介文字样式可配 + meta 区块独立成块 + 有效期展示 ----
+
+test('2.24 简介样式: 消费 bioSize/bioColor/bioLineHeight(后端补 3 键后可配)', () => {
+ assert.match(miniSrc, /bioTextStyle\(\)\s*\{[\s\S]*?num\(c\.bioSize, 13\)/,
+ '简介字号未读 bioSize(回退 13)');
+ assert.match(miniSrc, /bioTextStyle\(\)\s*\{[\s\S]*?c\.bioColor \|\| c\.infoColor/,
+ '简介颜色未读 bioColor(应回退 infoColor)');
+ assert.match(miniSrc, /bioTextStyle\(\)\s*\{[\s\S]*?num\(c\.bioLineHeight, 22\)/,
+ '简介行高未读 bioLineHeight(回退 22)');
+});
+
+test('2.24 meta 区块: 与 bio/social 同款, 可独立成块 + 逐向间距', () => {
+ assert.match(miniSrc, /metaFinalStyle\(\)\s*\{[\s\S]*?blockFinalStyle\(this\.c, 'meta'\)/,
+ 'meta 块未走通用独立成块样式(blockFinalStyle(this.c, "meta"))');
+ // 模板挂上 metaFinalStyle
+ assert.match(miniSrc, /class="mini-meta"[\s\S]*?:style="metaFinalStyle"/,
+ 'meta 块模板未挂 metaFinalStyle');
+ // 区块流 token 含 meta(可参与排序)
+ assert.match(miniSrc, /BLOCK_TOKENS = \['bio', 'rows', 'acts', 'social', 'qr', 'meta'\]/,
+ 'BLOCK_TOKENS 应含 meta token');
+});
+
+test('2.24 有效期: expire_at 自动展示「有效期至」(无独立开关)', () => {
+ assert.match(miniSrc, /showExpireRow\(\)\s*\{[\s\S]*?this\.card && num\(this\.card\.expire_at, 0\) > 0/,
+ '有效期行未按 card.expire_at>0 判定');
+ assert.match(miniSrc, /expireDateText\(\)\s*\{[\s\S]*?this\.card && this\.card\.expire_at/,
+ '缺少有效期日期格式化');
+ // 模板渲染「有效期至」且计入 meta 块可见性
+ assert.match(miniSrc, /有效期至 \{\{ expireDateText \}\}/, '模板未渲染「有效期至」');
+ assert.match(miniSrc, /showTagsRow \|\| showSourceRow \|\| showExpireRow/,
+ 'meta 块可见性未纳入有效期行');
+});