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 块可见性未纳入有效期行'); +});