From 0a5794d2604ab8c0cb16cd715429f9400f02e9fa Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Tue, 27 Oct 2026 11:57:11 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E4=BA=8C=E7=BB=B4=E7=A0=81=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E5=8F=8A=E5=85=B6=E4=BC=98=E5=85=88=E7=BA=A7=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/js/card-api.js | 4 +- components-diy/diy-card-mini.vue | 31 +++++++++- components-diy/diy-card-self.vue | 2 + components-diy/diy-card-show.vue | 2 + pages_tool/card/edit.vue | 103 ++++++++++++++++++++++++++++++- 5 files changed, 138 insertions(+), 4 deletions(-) diff --git a/common/js/card-api.js b/common/js/card-api.js index 958e138..130e4b3 100644 --- a/common/js/card-api.js +++ b/common/js/card-api.js @@ -147,7 +147,9 @@ export const CARD_FIELDS = [ 'name', 'company', 'job_title', 'avatar', 'mobile', 'phone', 'email', 'website', 'wechat', 'country', 'province', 'city', 'address', 'postal_code', - 'bio' + 'bio', + // 2.20 名片级自定义二维码图片(空=用系统生成码) + 'qr_image' ]; /** 人脉可更新白名单:与后端 Contact::EDITABLE_FIELDS 一致 */ diff --git a/components-diy/diy-card-mini.vue b/components-diy/diy-card-mini.vue index b246c26..b25184e 100644 --- a/components-diy/diy-card-mini.vue +++ b/components-diy/diy-card-mini.vue @@ -319,9 +319,38 @@ export default { showQrBase() { return flagOn(this.c.showQr, false) && !!this.qrSrc; }, - /** 二维码码源(2.18): 用户自定义图片(媒体库)优先, 空=名片自动生成的码 */ + /** + * 二维码来源优先级模式(2.20.1, 由用户在 DIY 组件面板显式选择): + * card 名片级 > 组件级 > 系统码(默认) + * component 组件级 > 名片级 > 系统码 + * system 仅系统生成码(忽略全部自定义图) + * 非法/缺失一律回落 'card' —— 与后端 CardQrSource::normalizePriority 同口径。 + */ + qrPriority() { + const m = String(this.c.qrPriority || '').trim(); + return ['card', 'component', 'system'].includes(m) ? m : 'card'; + }, + /** + * 二维码码源(2.20 三层来源 + 2.20.1 可选优先级, 与后端 CardQrSource::resolve 同口径): + * - 名片级 card.qr_image 每张名片各自一张(员工/企业都适用) + * - 组件级 c.qrImage 装修期统一指定 + * - 系统码 card.qr_url 原有行为, 兜底 + */ qrSrc() { + const mode = this.qrPriority; + if (mode === 'system') { + return (this.card && this.card.qr_url) || ''; + } + const own = String((this.card && this.card.qr_image) || '').trim(); const custom = String(this.c.qrImage || '').trim(); + if (mode === 'component') { + // 组件级优先: 组件图 → 名片图 → 系统码 + if (custom) return custom; + if (own) return own; + return (this.card && this.card.qr_url) || ''; + } + // card(默认): 名片级 → 组件级 → 系统码 + if (own) return own; if (custom) return custom; return (this.card && this.card.qr_url) || ''; }, diff --git a/components-diy/diy-card-self.vue b/components-diy/diy-card-self.vue index 204ec94..6bed593 100644 --- a/components-diy/diy-card-self.vue +++ b/components-diy/diy-card-self.vue @@ -119,6 +119,8 @@ export default { actsBlockPad: v.actsBlockPad, actIcons: v.actIcons, qrImage: v.qrImage, + // 2.20.1 二维码来源优先级(card/component/system; 缺省 card) + qrPriority: v.qrPriority, // 2.19: 默认动作显隐/文案 + 自定义动作(基件 acts computed 合成) actVis: v.actVis, actDeleted: v.actDeleted, diff --git a/components-diy/diy-card-show.vue b/components-diy/diy-card-show.vue index e130a15..0a3229d 100644 --- a/components-diy/diy-card-show.vue +++ b/components-diy/diy-card-show.vue @@ -85,6 +85,8 @@ export default { actsBlockPad: v.actsBlockPad, actIcons: v.actIcons, qrImage: v.qrImage, + // 2.20.1 二维码来源优先级(card/component/system; 缺省 card) + qrPriority: v.qrPriority, // 2.19: 默认动作显隐/文案 + 自定义动作(基件 acts computed 合成) actVis: v.actVis, actDeleted: v.actDeleted, diff --git a/pages_tool/card/edit.vue b/pages_tool/card/edit.vue index 56eb1e2..9d889a9 100644 --- a/pages_tool/card/edit.vue +++ b/pages_tool/card/edit.vue @@ -9,8 +9,21 @@ + - 点击从相册选择 + 点击从相册选择 + + + + 二维码 + + + + + + 点击上传自定义二维码 + {{ form.qr_image ? '已自定义,将替换默认二维码' : '未设置时使用名片链接自动生成的码' }} + 移除自定义二维码 + + *姓名 String(v || '')); @@ -284,6 +305,41 @@ export default { }); }, + /* ---------- 自定义二维码图片(2.20): 相册选择 → card 专用上传 → 回填路径 ---------- */ + chooseQrImage() { + uni.chooseImage({ + count: 1, + sizeType: ['compressed'], + sourceType: ['album', 'camera'], + success: res => { + const filePath = res.tempFilePaths && res.tempFilePaths[0]; + if (!filePath) return; + uni.showLoading({ title: '上传中…', mask: true }); + this.$api.upload({ + url: '/card/api/card/uploadQrImage', + filePath: filePath, + fileType: 'image', + success: r => { + uni.hideLoading(); + if (r && r.code == 0 && r.data && r.data.pic_path) { + this.form.qr_image = r.data.pic_path; + } else { + uni.showToast({ title: (r && r.message) || '上传失败', icon: 'none' }); + } + }, + fail: () => { + uni.hideLoading(); + uni.showToast({ title: '上传失败', icon: 'none' }); + } + }); + } + }); + }, + /* 移除自定义二维码: 置空 = 提交时回落系统自动生成的码 */ + clearQrImage() { + this.form.qr_image = ''; + }, + /* ---------- 附加手机号 ---------- */ onMobileInput(i, e) { this.$set(this.mobiles, i, e.detail.value); @@ -499,6 +555,49 @@ export default { color: #999; } +/* 自定义二维码行(2.20) */ +.qr-box { + width: 108rpx; + height: 108rpx; + border-radius: 12rpx; + background-color: #f2f3f5; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + flex-shrink: 0; + + .qr-img { + width: 100%; + height: 100%; + } + + .qr-plus { + font-size: 44rpx; + color: #c0c4cc; + } +} + +.qr-side { + flex: 1; + margin-left: 20rpx; + min-width: 0; +} + +.qr-tip { + font-size: 22rpx; + color: #b0b4bc; + margin-top: 4rpx; + line-height: 1.5; +} + +.qr-clear { + display: inline-block; + margin-top: 6rpx; + font-size: 24rpx; + color: var(--main-color, #0054a5); +} + /* 动态行(附加手机号/社交媒体/自定义字段) */ .dyn-row { .row-label { From 523473250c0bbf0c10aba1d7e0865334cff207e8 Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Tue, 27 Oct 2026 14:52:30 +0800 Subject: [PATCH 2/7] =?UTF-8?q?test(card):=20=E5=90=8D=E7=89=87=E7=BA=A7?= =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89=E4=BA=8C=E7=BB=B4=E7=A0=81=E5=A5=91?= =?UTF-8?q?=E7=BA=A6=E6=B5=8B=E8=AF=95=20+=20=E4=BF=AE=E6=AD=A3=E9=99=88?= =?UTF-8?q?=E6=97=A7=E6=96=AD=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 tests/diy-card-qr-image-source.test.js(24 项): mini 基件 qrSrc 按「名片级 card.qr_image > 组件级 c.qrImage > 系统码 card.qr_url」三层优先级取值(源码契约 + 真实求值双证); 组件级 qrImage 仍完整保留(未被名片级顶掉); 反例防恒真 - diy-card-qr-style.test.js 补 2.20 盒模型/色带/逐向间距字段契约, 并修正一处陈旧断言: cornerHeadStyle 的 paddingRight 已叠加 2.20 逐向微调右位移(customRight), 旧正则未同步 --- tests/diy-card-qr-image-source.test.js | 299 +++++++++++++++++++++++++ tests/diy-card-qr-style.test.js | 101 ++++++++- 2 files changed, 391 insertions(+), 9 deletions(-) create mode 100644 tests/diy-card-qr-image-source.test.js diff --git a/tests/diy-card-qr-image-source.test.js b/tests/diy-card-qr-image-source.test.js new file mode 100644 index 0000000..94141e4 --- /dev/null +++ b/tests/diy-card-qr-image-source.test.js @@ -0,0 +1,299 @@ +/** + * 名片 DIY 组件 · 名片级自定义二维码(card.qr_image)契约测试 + * + * 背景(2.20): + * 此前二维码只有两个来源 —— 组件级自定义图(c.qrImage)与系统生成码(card.qr_url)。 + * 2.20 起每张名片(员工名片/企业名片)可各自持有一张自定义二维码图片, + * DIY 组件按三层优先级取用: + * 1) 名片级 card.qr_image ← 每张名片各自一张(「我的名片」登录后即见本人那张) + * 2) 组件级 c.qrImage ← 装修期统一指定(名片自己没设图时生效) + * 3) 系统码 card.qr_url ← 兜底(原有行为) + * + * 覆盖: + * 1. mini 基件 qrSrc 按上述优先级取值(源码契约 + 真实求值双证) + * 2. 组件级 qrImage 仍完整保留(未被名片级顶掉) + * 3. 反例: 虚构字段不得出现(防恒真) + * + * 跑法: node --test tests/diy-card-qr-image-source.test.js + */ + +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); + +const DIY_DIR = path.resolve(__dirname, '../components-diy'); +const read = f => fs.readFileSync(path.join(DIY_DIR, f), 'utf8'); +// 先剥注释再断言 —— 注释里写「绝不能写 X」会被自判违规 +const strip = s => s + .replace(//g, '') + .replace(/\/\*[\s\S]*?\*\//g, '') + .replace(/(^|[^:])\/\/.*$/gm, '$1'); + +const miniSrc = strip(read('diy-card-mini.vue')); + +/** + * 从 mini.vue 抽出 qrSrc 的真实函数体并求值 —— 不靠正则猜逻辑, + * 避免「正则匹配到注释/字符串就假绿」。 + * + * 函数体内部用的是 this.c / this.card, 故以 ctx 为 this 求值。 + */ +function extractQrSrc(src) { + const m = src.match(/qrSrc\(\)\s*\{([\s\S]*?)\n\t\t\}/); + assert.ok(m, 'mini.vue 找不到 qrSrc 计算属性'); + return m[1]; +} + +/** 用构造器求值 qrSrc 函数体(只依赖 card/c 两个入参) */ +function evalQrSrc(card, config) { + const body = extractQrSrc(miniSrc); + // eslint-disable-next-line no-new-func + const fn = new Function('card', 'c', body); + // ctx 需提供 qrPriority 计算属性(Vue 里 qrSrc 依赖 this.qrPriority) + const ctx = { + card: card, + c: config, + get qrPriority() { return evalQrPriority(config); }, + }; + return fn.call(ctx, card, config); +} + +/** 抽 qrPriority 计算属性体并求值(模式白名单化) */ +function evalQrPriority(config) { + const m = miniSrc.match(/qrPriority\(\)\s*\{([\s\S]*?)\n\t\t\}/); + assert.ok(m, 'mini.vue 找不到 qrPriority 计算属性'); + // eslint-disable-next-line no-new-func + const fn = new Function('c', m[1]); + const ctx = { c: config }; + return fn.call(ctx, config); +} + +test('mini: qrSrc 源码按「名片级 > 组件级 > 系统码」取用', () => { + const body = extractQrSrc(miniSrc); + // 名片级: card.qr_image + assert.ok(/card\.qr_image/.test(body), 'qrSrc 未消费名片级 card.qr_image'); + // 组件级: c.qrImage + assert.ok(/c\.qrImage/.test(body), 'qrSrc 未保留组件级 c.qrImage'); + // 系统码兜底: card.qr_url + assert.ok(/card\.qr_url/.test(body), 'qrSrc 未保留系统码兜底 card.qr_url'); +}); + +test('mini: qrSrc 真实求值 —— 名片级优先于组件级', () => { + const got = evalQrSrc( + { qr_image: '/upload/own.png', qr_url: 'http://x/q' }, + { qrImage: '/upload/component.png' } + ); + assert.equal(got, '/upload/own.png'); +}); + +test('mini: qrSrc 真实求值 —— 名片级为空时回落组件级', () => { + const got = evalQrSrc( + { qr_image: '', qr_url: 'http://x/q' }, + { qrImage: '/upload/component.png' } + ); + assert.equal(got, '/upload/component.png'); +}); + +test('mini: qrSrc 真实求值 —— 两级皆空回落系统生成码(原行为)', () => { + const got = evalQrSrc( + { qr_image: '', qr_url: 'http://x/q' }, + { qrImage: '' } + ); + assert.equal(got, 'http://x/q'); +}); + +test('mini: qrSrc 真实求值 —— 名片级优先(组件级也配了图时)', () => { + // 「我的名片」组件: 员工登录后应看到属于该员工自己名片的那张图, + // 即使装修时组件级也配了图 —— 名片级更贴近实体, 优先。 + const got = evalQrSrc( + { qr_image: '/upload/employee-own.png', qr_url: 'http://x/q' }, + { qrImage: '/upload/shop-level.png' } + ); + assert.equal(got, '/upload/employee-own.png'); +}); + +test('mini: qrSrc 真实求值 —— card 为 null 不抛异常(未登录空态)', () => { + const got = evalQrSrc(null, { qrImage: '/upload/component.png' }); + assert.equal(got, '/upload/component.png'); +}); + +// ============================================================= +// 2.20.1 优先级模式(用户在 DIY 组件面板显式选择) +// ============================================================= + +test('mini: qrPriority 白名单化 —— 缺失/非法值回落 card', () => { + assert.equal(evalQrPriority({}), 'card'); + assert.equal(evalQrPriority({ qrPriority: '' }), 'card'); + assert.equal(evalQrPriority({ qrPriority: 'evil' }), 'card'); + assert.equal(evalQrPriority({ qrPriority: 'component' }), 'component'); + assert.equal(evalQrPriority({ qrPriority: 'system' }), 'system'); +}); + +test('mini: mode=card 名片级优先(默认; 员工登录看到自己那张)', () => { + const got = evalQrSrc( + { qr_image: '/upload/own.png', qr_url: 'http://x/q' }, + { qrImage: '/upload/component.png', qrPriority: 'card' } + ); + assert.equal(got, '/upload/own.png'); +}); + +test('mini: mode=card 名片级为空则用组件级', () => { + const got = evalQrSrc( + { qr_image: '', qr_url: 'http://x/q' }, + { qrImage: '/upload/component.png', qrPriority: 'card' } + ); + assert.equal(got, '/upload/component.png'); +}); + +test('mini: mode=component 组件级优先于名片级', () => { + const got = evalQrSrc( + { qr_image: '/upload/own.png', qr_url: 'http://x/q' }, + { qrImage: '/upload/component.png', qrPriority: 'component' } + ); + assert.equal(got, '/upload/component.png'); +}); + +test('mini: mode=component 组件级为空则回落名片级', () => { + const got = evalQrSrc( + { qr_image: '/upload/own.png', qr_url: 'http://x/q' }, + { qrImage: '', qrPriority: 'component' } + ); + assert.equal(got, '/upload/own.png'); +}); + +test('mini: mode=system 只用系统生成码(忽略全部自定义图)', () => { + const got = evalQrSrc( + { qr_image: '/upload/own.png', qr_url: 'http://x/q' }, + { qrImage: '/upload/component.png', qrPriority: 'system' } + ); + assert.equal(got, 'http://x/q'); +}); + +test('mini: mode=system 名片级有图也忽略', () => { + const got = evalQrSrc( + { qr_image: '/upload/own.png', qr_url: 'http://x/q' }, + { qrImage: '', qrPriority: 'system' } + ); + assert.equal(got, 'http://x/q'); +}); + +test('mini: 未设置 qrPriority(存量实例)等价于 card', () => { + const withMode = evalQrSrc( + { qr_image: '/upload/own.png', qr_url: 'http://x/q' }, + { qrImage: '/upload/component.png', qrPriority: 'card' } + ); + const withoutMode = evalQrSrc( + { qr_image: '/upload/own.png', qr_url: 'http://x/q' }, + { qrImage: '/upload/component.png' } + ); + assert.equal(withoutMode, withMode); +}); + +// ============================================================= +// 面板契约(两个组件的 design.html 都要有来源优先级控件) +// ============================================================= + +const PANEL_DIR = path.resolve(__dirname, '../../backend/src/addon/card/component/view'); +const hasPanels = fs.existsSync(path.join(PANEL_DIR, 'diy-card-self/design.html')); + +if (hasPanels) { + const selfPanel = strip(fs.readFileSync(path.join(PANEL_DIR, 'diy-card-self/design.html'), 'utf8')); + const showPanel = strip(fs.readFileSync(path.join(PANEL_DIR, 'diy-card-show/design.html'), 'utf8')); + + for (const [name, panel] of [['self', selfPanel], ['show', showPanel]]) { + test('面板(' + name + '): 二维码来源优先级三档控件均存在', () => { + for (const v of ['card', 'component', 'system']) { + assert.ok( + panel.includes("nc.qrPriority = '" + v + "'"), + name + ' 面板缺优先级选项 ' + v + ); + } + }); + + test('面板(' + name + '): 来源控件在「显示二维码」打开时才显示', () => { + // 控件所在容器必须带 v-show="nc.showQr" + const idx = panel.indexOf("nc.qrPriority = 'card'"); + assert.ok(idx > 0, name + ' 面板找不到来源控件'); + const head = panel.slice(Math.max(0, idx - 1200), idx); + assert.ok(head.includes('v-show="nc.showQr"'), name + ' 面板来源控件未随显示二维码开关联动'); + }); + } + + test('面板: 优先级默认值与 config 注册表一致(card)', () => { + // config/diy_view.php 的 util 段默认值 + const cfgPath = path.resolve(__dirname, '../../backend/src/addon/card/config/diy_view.php'); + const cfg = fs.readFileSync(cfgPath, 'utf8'); + assert.ok(cfg.includes('"qrPriority":"card"'), 'config/diy_view.php 缺 qrPriority 默认值'); + }); + + test('面板: 预览真实消费 qrPriority(4 处二维码挂点走分层 v-if)', () => { + for (const [name, panel] of [['self', selfPanel], ['show', showPanel]]) { + // 4 个挂点(head/corner/rows/bottom) 各一组分支: + // system 占位 + component 组件图 + component 名片图 + card 名片图 + card 组件图 + 兜底占位 + const sys = (panel.match(/v-if="nc\.qrPriority === 'system'"/g) || []).length; + const comp = (panel.match(/v-else-if="nc\.qrPriority === 'component'/g) || []).length; + assert.equal(sys, 4, name + ' 面板 system 档分支数不对: ' + sys); + // component 档每挂点 2 支(组件图 + 名片图回落) = 8 + assert.equal(comp, 8, name + ' 面板 component 档分支数不对: ' + comp); + // 名片级图分支(每挂点 2 支: component 回落 + card 优先; + // 每支出现 2 次: v-else-if 条件 + style 内取值) = 16 + const cardImg = (panel.match(/nc\.tempData\.previewCard && nc\.tempData\.previewCard\.qr_image/g) || []).length; + assert.equal(cardImg, 16, name + ' 面板名片级图分支数不对: ' + cardImg); + } + }); + + test('面板: 预览不得调用 window 全局函数(Vue2 模板作用域不可见)', () => { + for (const [name, panel] of [['self', selfPanel], ['show', showPanel]]) { + assert.ok( + !/cardQrPreviewSrc\s*\(/.test(panel), + name + ' 面板仍调用 window 全局函数 —— Vue2 模板会报「not defined on the instance」' + ); + } + }); + + // 2026-10-27: 「快捷操作管理」图层行的 color input 空值报错 + // 图层行 v-for 数组曾只有 {k,ch}(无 bg/fg), 而同行 :value 引用 a.fg/a.bg + // → undefined → Vue 写 elm.value='' → Chrome CSS color 格式告警 + test('面板: 图层行 v-for 数组含 bg/fg(避免 color input 空值)', () => { + for (const [name, panel] of [['self', selfPanel], ['show', showPanel]]) { + const m = panel.match(/v-for="a in (\[\{k:'tel'[\s\S]*?\}\])"/); + assert.ok(m, name + ' 面板找不到图层行 v-for 数组'); + const arr = m[1]; + const items = arr.match(/\{k:'[a-z]+'/g) || []; + assert.equal(items.length, 5, name + ' 面板图层行数组项数不对: ' + items.length); + const bgCount = (arr.match(/bg:'#[0-9A-Fa-f]{6}'/g) || []).length; + const fgCount = (arr.match(/fg:'#[0-9A-Fa-f]{6}'/g) || []).length; + assert.equal(bgCount, 5, name + ' 面板图层行数组缺 bg: ' + bgCount); + assert.equal(fgCount, 5, name + ' 面板图层行数组缺 fg: ' + fgCount); + } + }); + + test('面板: 图层行 color input 的 :value 不含未定义引用', () => { + for (const [name, panel] of [['self', selfPanel], ['show', showPanel]]) { + const colorBind = panel.match(/data-field="color"[^>]*:value="([^"]*)"/); + const bgBind = panel.match(/data-field="bg"[^>]*:value="([^"]*)"/); + assert.ok(colorBind, name + ' 面板找不到图层行 color 绑定'); + assert.ok(bgBind, name + ' 面板找不到图层行 bg 绑定'); + for (const [label, expr] of [['color', colorBind[1]], ['bg', bgBind[1]]]) { + const refs = (expr.match(/\ba\.([a-z]+)/g) || []).map(x => x.slice(2)); + for (const r of Array.from(new Set(refs))) { + assert.ok( + ['k', 'ch', 'lb', 'bg', 'fg'].includes(r), + name + ' 面板图层行 ' + label + ' 绑定引用了数组里不存在的 a.' + r + ); + } + } + } + }); +} + +// ============================================================= +// 反例(防恒真) +// ============================================================= + +test('反例: 虚构字段绝不出现在基件', () => { + assert.ok(!miniSrc.includes('c.qrMagic')); + assert.ok(!miniSrc.includes('card.qrMagic')); +}); diff --git a/tests/diy-card-qr-style.test.js b/tests/diy-card-qr-style.test.js index ae75419..b9177c4 100644 --- a/tests/diy-card-qr-style.test.js +++ b/tests/diy-card-qr-style.test.js @@ -40,6 +40,13 @@ const miniSrc = strip(read('diy-card-mini.vue')); const hasPanels = fs.existsSync(path.join(PANEL_DIR, 'diy-card-self/design.html')); const showPanel = hasPanels ? strip(fs.readFileSync(path.join(PANEL_DIR, 'diy-card-show/design.html'), 'utf8')) : ''; const selfPanel = hasPanels ? strip(fs.readFileSync(path.join(PANEL_DIR, 'diy-card-self/design.html'), 'utf8')) : ''; +const BOX_STYLE_FIELDS = [ + 'bandShow', 'bandBgMode', 'bandBg', + 'wrapMarginTop', 'wrapMarginRight', 'wrapMarginBottom', 'wrapMarginLeft', + 'headBgMode', 'headBg', 'headRadiusMode', 'headRadius', 'headSpacingCustom', + 'headMarginTop', 'headMarginRight', 'headMarginBottom', 'headMarginLeft', + 'headPaddingTop', 'headPaddingRight', 'headPaddingBottom', 'headPaddingLeft', +]; /** * 面板控件的字段绑定有三种形态(2026-10-21 面板改版: 下拉框 → 可视化选项组, @@ -172,20 +179,76 @@ test('common: qrTransform 位移(任一轴非零才输出; WXSS 支持 translate // 2. mini.vue 基件契约 // ============================================================= -test('mini: 蓝色顶带版式(2026-10-23 与精选名片 show 同款: 自适应白卡高度 + 弧线距底 88rpx)', () => { - // 模板: 卡片包进 wrap, 顶带在最前 +test('mini: 顶部色带默认隐藏,背景支持同步名片设计或独立颜色', () => { assert.match(miniSrc, /class="mini-card-wrap"/); - assert.match(miniSrc, /class="mini-band" :style="bandStyle"/); + assert.match(miniSrc, /<\/view>/); + assert.match(miniSrc, /bandShow\(\)\s*\{\s*return flagOn\(this\.c\.bandShow, false\)/); + assert.match(miniSrc, /bandStyle\(\)\s*\{[\s\S]*?this\.c\.bandBgMode === 'custom'[\s\S]*?themeGradient\(this\.card\)/); assert.match(miniSrc, /class="mini-card fade-up" :style="wrapStyle"/); - // 计算属性: 顶带底色可自定义(bandBg)或回落主题渐变; 高度可配(bandHeight) - assert.match(miniSrc, /bandStyle\(\)\s*\{[\s\S]*?bandBg[\s\S]*?themeGradient\(this\.card\)/); - // CSS: wrap 撑露沿 + band 拉伸(top:-40rpx / bottom:88rpx) + 圆角(顶直底弧) + // 保留原色带造型,显隐和背景只改内联消费,不改几何结构。 assert.match(miniSrc, /\.mini-card-wrap \{\s*position: relative;\s*margin-top: 40rpx;/); assert.match(miniSrc, /\.mini-band \{\s*position: absolute;[\s\S]*?top: -40rpx;[\s\S]*?bottom: 88rpx;[\s\S]*?border-radius: 0 0 28rpx 28rpx;/); - // mini-block 方案不得回归 assert.ok(!miniSrc.includes('mini-block')); }); +test('mini: 卡片四向外边距与头部盒模型配置完整消费', () => { + for (const side of ['Top', 'Right', 'Bottom', 'Left']) { + assert.match(miniSrc, new RegExp('margin' + side + ': px2rpx\\(num\\(c\\.wrapMargin' + side + ', 0\\)\\)')); + assert.match(miniSrc, new RegExp('c\\.headMargin' + side)); + assert.match(miniSrc, new RegExp('c\\.headPadding' + side)); + } + assert.match(miniSrc, /headBoxStyle\(\)\s*\{[\s\S]*?c\.headBgMode === 'custom'[\s\S]*?this\.headTheme \? themeGradient\(this\.card\) : 'transparent'/); + assert.match(miniSrc, /headThemeStyle\(\)\s*\{\s*return this\.headBoxStyle/); + assert.match(miniSrc, /plainHeadFinalStyle\(\)\s*\{[\s\S]*?this\.cornerHeadStyle[\s\S]*?return \[this\.headBoxStyle, corner\]/); +}); + +test('mini: C端盒模型计算值——历史默认兼容且自定义值逐项生效', async () => { + const m = await import('../components-diy/diy-card-common.js'); + const script = miniSrc.match(/