From 1de3146c3a2940a21ee1a76c8e833285817af39c Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Sat, 24 Oct 2026 17:03:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(card-diy):=202.19.10=20C=20=E7=AB=AF?= =?UTF-8?q?=E6=B6=88=E8=B4=B9=20detail=20=E5=AF=B9=E9=BD=90=20+=20?= =?UTF-8?q?=E5=8C=BA=E5=9D=97=E7=8B=AC=E7=AB=8B=E6=A0=B7=E5=BC=8F=E5=AD=97?= =?UTF-8?q?=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - mini.vue: 8 detail 字段消费(三列行/复制范围外观/QR 多行/URL 行/hero 徽章) + rowsBlockStyle/qrBlockCustomStyle computed(与操作区 actsBlockStyle 同款模式) + rowsFinalStyle/qrBlockFinalStyle 合并独立块样式 - show.vue/self.vue: miniConfig 透传 14 字段(8 detail + 6 独立样式) - 测试 ×3: 2.19.10 契约锁(配置层/透传/面板/预览消费/反例: layoutCards 已移除) 前端 7 套 123 项全绿 --- components-diy/diy-card-mini.vue | 211 +++++++++++++++++++++++++++++-- components-diy/diy-card-self.vue | 15 +++ components-diy/diy-card-show.vue | 15 +++ tests/diy-card-ab-visual.test.js | 16 ++- tests/diy-card-qr-style.test.js | 8 +- tests/diy-card-style-v2.test.js | 132 +++++++++++++++++-- 6 files changed, 367 insertions(+), 30 deletions(-) diff --git a/components-diy/diy-card-mini.vue b/components-diy/diy-card-mini.vue index 392591c..6ed762e 100644 --- a/components-diy/diy-card-mini.vue +++ b/components-diy/diy-card-mini.vue @@ -44,13 +44,17 @@ - {{ qrTip || '扫码查看完整名片' }} + {{ qrTipEm }}{{ qrTipLine1 }}{{ qrTipSub }}{{ qrUrlText }} + + + {{ badgeText }} + @@ -67,13 +71,17 @@ - {{ qrTip || '扫码查看完整名片' }} + {{ qrTipEm }}{{ qrTipLine1 }}{{ qrTipSub }}{{ qrUrlText }} - + + + {{ badgeText }} + + {{ initialOf(card.name) }} @@ -89,7 +97,7 @@ - {{ qrTip || '扫码查看完整名片' }} + {{ qrTipEm }}{{ qrTipLine1 }}{{ qrTipSub }}{{ qrUrlText }} @@ -114,14 +122,14 @@ - + {{ rowsTitle }} + :class="{ 'row-tappable': rowTapable(row), 'row-3col': showRowLabels }" :hover-class="rowTapable(row) ? 'btn-press' : 'none'" @tap="onRowTap(row)"> @@ -133,9 +141,9 @@ - {{ row.label }} + {{ row.label }} {{ row.value }} - 复制 + 复制 {{ expanded ? '收起资料' : '展开全部资料(' + hiddenCount + ')' }} @@ -143,9 +151,9 @@ - + - {{ qrTip || '扫码查看完整名片' }} + {{ qrTipEm }}{{ qrTipLine1 }}{{ qrTipSub }}{{ qrUrlText }} @@ -171,9 +179,9 @@ - + - {{ qrTip || '扫码查看完整名片' }} + {{ qrTipEm }}{{ qrTipLine1 }}{{ qrTipSub }}{{ qrUrlText }} @@ -429,6 +437,47 @@ export default { qrTip() { return this.c.qrTip || ''; }, + // ===== 2.19.10 detail 版式对齐字段 ===== + /** 资料行三列(图标+灰标签+值, detail 同款): 默认 0 = 历史图标/标签二选一 */ + showRowLabels() { + return flagOn(this.c.showRowLabels, false); + }, + /** hero 徽章(左上「数字名片」): 默认关 */ + badgeShow() { + return flagOn(this.c.badgeShow, false); + }, + badgeText() { + return String(this.c.badgeText || '数字名片'); + }, + /** 提示加粗段(detail 扫码保存名片); 空 = 整行走 qrTip(历史形态) */ + qrTipEm() { + return String(this.c.qrTipEm || '').trim(); + }, + /** 提示第一行(加粗段之后): 有 em 时前置「 · 」分隔符 */ + qrTipLine1() { + const tip = String(this.c.qrTip || '').trim(); + if (this.qrTipEm) return tip ? (' · ' + tip) : ''; + return tip || '扫码查看完整名片'; + }, + /** 提示第二行(detail「同一二维码可写入 NFC 名片贴」): 空 = 不出 */ + qrTipSub() { + return String(this.c.qrTipSub || '').trim(); + }, + /** 二维码下 URL 文本行(detail qr-url 同款): 默认关 */ + qrUrlShow() { + return flagOn(this.c.qrUrlShow, false); + }, + qrUrlText() { + return (this.card && this.card.wap_url) || ''; + }, + /** 复制按钮范围: all=全部行(历史) / mobile=仅手机微信行(detail 口径) */ + copyBtnScope() { + return String(this.c.copyBtnScope || 'all'); + }, + /** 复制按钮外观: theme=主题浅底(历史) / gray=浅灰描边胶囊(detail 同款) */ + copyBtnLook() { + return String(this.c.copyBtnLook || 'theme'); + }, detailBtnText() { return this.c.detailBtnText || ''; }, @@ -586,14 +635,52 @@ export default { fontWeight: '600', }); }, - /** 复制按钮: 主题浅底 + 主题色字(hexToRgba 动态派生, WXSS 无 color-mix) */ + /** 复制按钮: theme=主题浅底+主题色字(历史); gray=detail 同款浅灰描边胶囊 */ copyBtnStyle() { + if (this.copyBtnLook === 'gray') { + return this.$util.objToStyle({ + background: '#F7F9FC', + color: '#5A6478', + border: '2rpx solid #EEF1F6', + }); + } const theme = (this.card && this.card.theme_color) || CardTokens.color.primary; return this.$util.objToStyle({ background: hexToRgba(theme, 0.12) || '#F2F3F5', color: theme, }); }, + /** 提示加粗段样式(detail qr-tip b 同款: ink-2 深灰 + 加粗) */ + qrTipEmStyle() { + return this.$util.objToStyle({ + color: this.c.infoColor || '#5A6478', + fontWeight: '600', + }); + }, + /** 二维码 URL 行样式(detail qr-url 同款: 小字灰 + 断行) */ + qrUrlStyle() { + return this.$util.objToStyle({ + color: this.c.labelColor || CardTokens.color.textLabel, + fontSize: px2rpx(10) + 'rpx', + marginTop: px2rpx(8) + 'rpx', + wordBreak: 'break-all', + textAlign: 'center', + }); + }, + /** hero 徽章胶囊(theme 档白描边 / plain 档主题描边; 避让企业徽标 badgeStyle) */ + heroBadgeStyle() { + const theme = (this.card && this.card.theme_color) || CardTokens.color.primary; + return this.$util.objToStyle({ + border: '2rpx solid ' + (this.headTheme ? 'rgba(255,255,255,0.35)' : (hexToRgba(theme, 0.35) || theme)), + }); + }, + heroBadgeTextStyle() { + return this.$util.objToStyle({ + color: this.headTheme ? 'rgba(255,255,255,0.85)' : (this.c.labelColor || CardTokens.color.textLabel), + fontSize: px2rpx(11) + 'rpx', + letterSpacing: '2rpx', + }); + }, badgeStyle() { // 企业徽标固定红底(参考图 2026-10-22 口径), 与头像底色解耦 return this.$util.objToStyle({ background: '#FA5151' }); @@ -692,6 +779,47 @@ export default { const extra = clear.marginTop ? 'margin-top: ' + clear.marginTop + ';' : ''; return ((this.actsBlockStyle || '') + ' ' + extra).trim(); }, + /** 资料区独立块样式(与操作区同款: Bg/Radius/Pad) */ + rowsBlockStyle() { + const c = this.c; + const bg = String(c.rowsBlockBg || '').trim(); + const radius = num(c.rowsBlockRadius, 0); + const pad = num(c.rowsBlockPad, 0); + const style = {}; + if (bg) style.background = bg; + if (radius > 0) style.borderRadius = px2rpx(radius) + 'rpx'; + if (pad > 0) style.padding = px2rpx(pad) + 'rpx'; + return this.$util.objToStyle(style); + }, + /** 二维码独立块样式(与操作区同款: Bg/Radius/Pad) */ + qrBlockCustomStyle() { + const c = this.c; + const bg = String(c.qrBlockBg || '').trim(); + const radius = num(c.qrBlockRadius, 0); + const pad = num(c.qrBlockPad, 0); + const style = {}; + if (bg) style.background = bg; + if (radius > 0) style.borderRadius = px2rpx(radius) + 'rpx'; + if (pad > 0) style.padding = px2rpx(pad) + 'rpx'; + return this.$util.objToStyle(style); + }, + /** 资料区最终样式: 独立块样式 + corner 让位合并 */ + rowsFinalStyle() { + const clear = this.rowsClearStyle || {}; + const extra = clear.marginTop ? 'margin-top: ' + clear.marginTop + ';' : ''; + return ((this.rowsBlockStyle || '') + ' ' + extra).trim(); + }, + /** 二维码(rows/bottom 流内档)最终样式: 独立块样式 + 尺寸/位移合并 */ + qrBlockFinalStyle() { + const transform = qrTransform(this.c.qrOffsetX, this.c.qrOffsetY); + const parts = [this.qrBlockStyle, this.qrBlockCustomStyle]; + if (transform) parts.push('transform:' + transform + ';'); + return parts.filter(Boolean).join(' ').trim(); + }, + /** plain 头最终样式 */ + plainHeadFinalStyle() { + return this.cornerHeadStyle; + }, detailBtnStyle() { // 发送名片大按钮: 纯色主题实心(2026-10-22 参考图定稿, 不再用渐变); // 高度 104rpx(用户反馈: 比触达标准 80rpx 加高一档, 参考图观感) @@ -875,6 +1003,15 @@ export default { fontSize: px2rpx(num(this.c.actSize, 12)) + 'rpx', }); }, + /** 复制按钮可见性: 开关 + 值非空 + 范围(mobile=仅手机/微信行, detail 口径) */ + copyBtnVisible(row) { + if (!this.showCopyBtn || !row || !String(row.value || '').trim()) return false; + if (this.copyBtnScope === 'mobile') { + const f = String(row.field || '').toLowerCase(); + return f === 'mobile' || f === 'phone' || f === 'wechat' || f === 'wx'; + } + return true; + }, /** 复制按钮: 复用 row-act 的 copy 分支(rowTap 关闭时仍可独立复制) */ onCopyRow(row) { this.$emit('row-act', { type: 'copy', value: row.value }); @@ -1046,6 +1183,42 @@ export default { } } + /* 2.19.10 hero 徽章(detail hero-badge 同款: 绿点 + 描边胶囊) */ + .mini-hero-badge { + display: inline-flex; + align-items: center; + position: relative; + z-index: 2; + align-self: flex-start; + margin-bottom: 20rpx; + padding: 6rpx 18rpx; + border-radius: 999rpx; + + .hero-badge-dot { + width: 10rpx; + height: 10rpx; + border-radius: 50%; + background: #07c160; + margin-right: 10rpx; + } + } + + /* 提示多行(detail qr-tip 同款: 加粗段 + 换行) */ + .qr-tip-em { + font-weight: 600; + } + + .qr-tip-sub { + font-size: 22rpx; + margin-top: 4rpx; + } + + /* 二维码 URL 行(detail qr-url 同款) */ + .qr-url { + display: block; + width: 100%; + } + /* 复制按钮(2.17): 浅底主题色小按钮(色内联), 不挤压资料值换行 */ .row-copy { flex-shrink: 0; @@ -1213,6 +1386,18 @@ export default { font-size: 24rpx; } + /* 三列档(2.19.10): 图标+灰标签+值, detail 资料行同款列宽 */ + &.row-3col { + .row-icon, + .row-css-wrap { + width: 56rpx; + } + + .row-label { + width: 88rpx; + } + } + /* 图标列与文字标签列同宽, 保证值列左缘对齐(参考图口径) */ .row-icon { flex-shrink: 0; diff --git a/components-diy/diy-card-self.vue b/components-diy/diy-card-self.vue index 32c1cf6..8a01aeb 100644 --- a/components-diy/diy-card-self.vue +++ b/components-diy/diy-card-self.vue @@ -125,6 +125,21 @@ export default { actLabels: v.actLabels, actOrder: v.actOrder, customActs: v.customActs, + // 2.19.10 detail 版式对齐(三列行/复制范围外观/qrTip 富文本/URL 行/hero 徽章/分卡) + showRowLabels: v.showRowLabels, + copyBtnScope: v.copyBtnScope, + copyBtnLook: v.copyBtnLook, + qrTipEm: v.qrTipEm, + qrTipSub: v.qrTipSub, + qrUrlShow: v.qrUrlShow, + badgeShow: v.badgeShow, + badgeText: v.badgeText, + rowsBlockBg: v.rowsBlockBg, + rowsBlockRadius: v.rowsBlockRadius, + rowsBlockPad: v.rowsBlockPad, + qrBlockBg: v.qrBlockBg, + qrBlockRadius: v.qrBlockRadius, + qrBlockPad: v.qrBlockPad, }; }, boxStyle() { diff --git a/components-diy/diy-card-show.vue b/components-diy/diy-card-show.vue index ddb696b..451e397 100644 --- a/components-diy/diy-card-show.vue +++ b/components-diy/diy-card-show.vue @@ -90,6 +90,21 @@ export default { actLabels: v.actLabels, actOrder: v.actOrder, customActs: v.customActs, + // 2.19.10 detail 版式对齐(三列行/复制范围外观/qrTip 富文本/URL 行/hero 徽章/分卡) + showRowLabels: v.showRowLabels, + copyBtnScope: v.copyBtnScope, + copyBtnLook: v.copyBtnLook, + qrTipEm: v.qrTipEm, + qrTipSub: v.qrTipSub, + qrUrlShow: v.qrUrlShow, + badgeShow: v.badgeShow, + badgeText: v.badgeText, + rowsBlockBg: v.rowsBlockBg, + rowsBlockRadius: v.rowsBlockRadius, + rowsBlockPad: v.rowsBlockPad, + qrBlockBg: v.qrBlockBg, + qrBlockRadius: v.qrBlockRadius, + qrBlockPad: v.qrBlockPad, }; }, }, diff --git a/tests/diy-card-ab-visual.test.js b/tests/diy-card-ab-visual.test.js index f65d8b5..dab0e0c 100644 --- a/tests/diy-card-ab-visual.test.js +++ b/tests/diy-card-ab-visual.test.js @@ -103,7 +103,7 @@ test('mini: 资料行图标列 + 文字标签退化', () => { // 2.17.1: 行首新增 CSS 描边图标档(邮箱/网站), iconfont 档退为 v-else-if assert.match(miniSrc, //); assert.match(miniSrc, / { test('mini: 资料行可点(开关 + href 派生 + emit row-act)', () => { assert.match(miniSrc, /rowTapable\(row\)\s*\{\s*return flagOn\(this\.c\.rowTap,\s*true\)\s*&&\s*!!rowActOf\(row\)/); - assert.match(miniSrc, /:class="\{ 'row-tappable': rowTapable\(row\) \}"/); + assert.match(miniSrc, /:class="\{ 'row-tappable': rowTapable\(row\), 'row-3col': showRowLabels \}"/); assert.match(miniSrc, /@tap="onRowTap\(row\)"/); assert.match(miniSrc, /onRowTap\(row\)\s*\{[\s\S]*?this\.\$emit\('row-act',\s*act\)/); }); @@ -164,6 +164,18 @@ test('self: miniConfig 透传 A+B 三字段', () => { assert.match(selfSrc, /qrPosition:\s*v\.qrPosition/); }); +test('self: miniConfig 透传 2.19.10 detail 对齐 9 字段', () => { + for (const f of ['showRowLabels', 'copyBtnScope', 'copyBtnLook', 'qrTipEm', 'qrTipSub', 'qrUrlShow', 'badgeShow', 'badgeText', 'rowsBlockBg', 'rowsBlockRadius', 'rowsBlockPad', 'qrBlockBg', 'qrBlockRadius', 'qrBlockPad']) { + assert.match(selfSrc, new RegExp(f + ':\\s*v\\.' + f + ','), 'self 透传 ' + f); + } +}); + +test('show: miniConfig 透传 2.19.10 detail 对齐 9 字段', () => { + for (const f of ['showRowLabels', 'copyBtnScope', 'copyBtnLook', 'qrTipEm', 'qrTipSub', 'qrUrlShow', 'badgeShow', 'badgeText', 'rowsBlockBg', 'rowsBlockRadius', 'rowsBlockPad', 'qrBlockBg', 'qrBlockRadius', 'qrBlockPad']) { + assert.match(showSrc, new RegExp(f + ':\\s*v\\.' + f + ','), 'show 透传 ' + f); + } +}); + test('self: 模板挂 @row-act="doAct"(行点击有接收方)', () => { assert.match(selfSrc, /@row-act="doAct"/); }); diff --git a/tests/diy-card-qr-style.test.js b/tests/diy-card-qr-style.test.js index 73e779d..9f98b65 100644 --- a/tests/diy-card-qr-style.test.js +++ b/tests/diy-card-qr-style.test.js @@ -254,9 +254,11 @@ test('mini: cornerHeadStyle 头部水平让位按浮层实际宽度动态反算' assert.match(miniSrc, //); }); -test('mini: 四个二维码块统一绑 qrBlockStyle(尺寸/位移/外观单一事实来源)', () => { - const binds = miniSrc.match(/:style="qrBlockStyle"/g) || []; - assert.equal(binds.length, 5, 'qrBlockStyle 绑定(corner/rows/bottom 各 1 + head 双分支各一), 实际 ' + binds.length); +test('mini: 二维码块样式绑定——corner/head 走 qrBlockStyle, rows/bottom 走 qrBlockFinalStyle(2.19.10 分卡合并)', () => { + const direct = miniSrc.match(/:style="qrBlockStyle"/g) || []; + assert.equal(direct.length, 3, 'qrBlockStyle 直绑(corner 1 + head 双分支 2), 实际 ' + direct.length); + const final = miniSrc.match(/:style="qrBlockFinalStyle"/g) || []; + assert.equal(final.length, 2, 'qrBlockFinalStyle(rows/bottom 各 1), 实际 ' + final.length); }); // ============================================================= diff --git a/tests/diy-card-style-v2.test.js b/tests/diy-card-style-v2.test.js index e6f3847..3f775e8 100644 --- a/tests/diy-card-style-v2.test.js +++ b/tests/diy-card-style-v2.test.js @@ -2,8 +2,12 @@ * 名片 DIY 组件 · 2.17 双形态(渐变头部/宫格快捷操作/资料区标题/复制按钮)契约测试 * * 锁死三层口径: diy_view.php 默认值 ↔ design.html 面板控件与预览 ↔ C 端 vue 消费 - * 兼容铁律: 新字段默认值 = 历史形态(plain/chips/关), 存量组件行缺字段时基件 - * flagOn/normalize 回落历史默认 —— 渲染零变化, 种子只补缺绝不 UPDATE + * 兼容铁律(2026-10-24 起分两层): + * ① 基件 flagOn/normalize 缺字段回落历史形态(plain/chips/关) —— 本测试 108-241 行 + * 断言保持不变, 保护任何缺字段的存量行渲染零变化; + * ② diy_view.php config 默认值代际升级为参考图形态(theme/grid/开/above) —— + * 配套迁移(test/card_diy_instance_style_migrate.php)把注册表行补缺 + 存量页面 + * 实例按整值指纹刷新, 存量行不再依赖①的回落, 故 config 默认值断言随代际更新。 * * 同时锁死 2.17 修复: self.vue miniConfig 补齐 7 个字号字段透传 * (diy-card-show.vue 自 2026-10-21 起改为自绘, 不再经 miniConfig 复用 diy-card-mini, @@ -19,21 +23,21 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const BE = resolve(__dirname, '../../backend/src/addon/card'); const FE = resolve(__dirname, '../components-diy'); -/** 2.17 新字段 → 默认值(= 历史形态, 兼容存量行) */ +/** 2.17 新字段 → 默认值(2026-10-24 代际升级: 参考图形态, 见文件头注释) */ const V2_FIELDS = { - headStyle: 'plain', + headStyle: 'theme', headNameColor: '#FFFFFF', headSubColor: '#FFFFFF', - actsLayout: 'chips', + actsLayout: 'grid', actGridColorMode: 'preset', - showRowsTitle: 0, + showRowsTitle: 1, rowsTitle: '联系资料', - showCopyBtn: 0, + showCopyBtn: 1, }; -/** 2.18 新字段 → 默认值(= 历史形态, 兼容存量行) */ +/** 2.18 新字段 → 默认值(2026-10-24 代际升级: actsPosition above) */ const V18_FIELDS = { - actsPosition: 'below', + actsPosition: 'above', actsBlockBg: '', actsBlockRadius: 0, actsBlockPad: 0, @@ -58,7 +62,7 @@ function readCommonJs() { return readFileSync(resolve(FE, 'diy-card-common.js'), 'utf8'); } -test('diy_view.php: Show/Self value 均含 8 个 2.17 字段且默认值=历史形态', () => { +test('diy_view.php: Show/Self value 均含 8 个 2.17 字段且默认值=参考图形态(2026-10-24 代际升级)', () => { for (const comp of ['show', 'self']) { const v = readValueJson(comp); for (const [field, dft] of Object.entries(V2_FIELDS)) { @@ -68,7 +72,7 @@ test('diy_view.php: Show/Self value 均含 8 个 2.17 字段且默认值=历史 } }); -test('diy_view.php: Show/Self value 均含 2.18 字段(actIcons 五类型完整结构)', () => { +test('diy_view.php: Show/Self value 均含 2.18 字段(actIcons 五类型完整结构, actsPosition=above 代际升级)', () => { for (const comp of ['show', 'self']) { const v = readValueJson(comp); for (const [field, dft] of Object.entries(V18_FIELDS)) { @@ -380,6 +384,24 @@ test('反例自测: 新形态标记不得出现在未改造组件中', () => { const V19_FIELDS = { actVis: 'object', actDeleted: 'object', actLabels: 'object', customActs: 'array', actOrder: 'array' }; const V19_ACT_LABELS = { tel: '打电话', wx: '微信号', mail: '发邮件', web: '网站', save: '存通讯录' }; +/** 2.19.10 detail 版式对齐字段 → 默认值(零变化) */ +const V21910_FIELDS = { + showRowLabels: 0, + copyBtnScope: 'all', + copyBtnLook: 'theme', + qrTipEm: '', + qrTipSub: '', + qrUrlShow: 0, + badgeShow: 0, + badgeText: '数字名片', + rowsBlockBg: '', + rowsBlockRadius: 0, + rowsBlockPad: 0, + qrBlockBg: '', + qrBlockRadius: 0, + qrBlockPad: 0, +}; + test('diy_view.php: Show/Self value 均含 2.19 字段(默认值=全显/默认文案/空数组/历史顺序=零变化)', () => { for (const comp of ['show', 'self']) { const v = readValueJson(comp); @@ -398,6 +420,62 @@ test('diy_view.php: Show/Self value 均含 2.19 字段(默认值=全显/默认 } }); +/* ------------------------------------------------------------------ + * 2.19.10 detail 版式对齐: 三列行/复制范围外观/qrTip 富文本/URL 行/hero 徽章/分卡 + * ------------------------------------------------------------------ */ + +test('diy_view.php: Show/Self value 均含 2.19.10 字段(默认值=历史形态零变化)', () => { + for (const comp of ['show', 'self']) { + const v = readValueJson(comp); + for (const [field, dft] of Object.entries(V21910_FIELDS)) { + assert.ok(field in v, comp + '.' + field + ' 已注册'); + assert.deepEqual(v[field], dft, comp + '.' + field + ' 默认=' + JSON.stringify(dft)); + } + } +}); + +test('show/self.vue: miniConfig 均透传 9 个 2.19.10 字段', () => { + for (const file of ['diy-card-show.vue', 'diy-card-self.vue']) { + const src = readFileSync(resolve(FE, file), 'utf8'); + for (const f of Object.keys(V21910_FIELDS)) { + assert.match(src, new RegExp(f + ':\\s*v\\.' + f + ','), file + ' 透传 ' + f); + } + } +}); + +test('mini.vue: 消费 2.19.10 字段(computed/模板/样式)', () => { + const src = readFileSync(resolve(FE, 'diy-card-mini.vue'), 'utf8'); + // computed: 新字段 getter + for (const f of ['showRowLabels', 'badgeShow', 'badgeText', 'qrTipEm', 'qrTipSub', 'qrUrlShow', 'qrUrlText', 'copyBtnScope', 'copyBtnLook', 'rowsBlockStyle', 'qrBlockCustomStyle']) { + assert.match(src, new RegExp(f + '\\(\\)\\s*\\{'), 'mini.vue computed ' + f); + } + // 模板消费 + assert.match(src, /class="mini-hero-badge"/, 'hero 徽章渲染'); + assert.match(src, /row-3col.*showRowLabels|showRowLabels.*row-3col/, '三列行 class 绑定'); + assert.match(src, /copyBtnVisible\(row\)/, '复制按钮范围方法'); + assert.match(src, /qrTipEm/, 'qrTip 加粗段'); + assert.match(src, /qrTipSub/, 'qrTip 第二行'); + assert.match(src, /qrUrlShow/, 'URL 行开关'); + // 独立块样式消费(与操作区同款: Bg/Radius/Pad) + assert.match(src, /rowsBlockBg/, 'rowsBlockStyle 消费 rowsBlockBg'); + assert.match(src, /qrBlockBg/, 'qrBlockCustomStyle 消费 qrBlockBg'); + assert.match(src, /rowsFinalStyle\(\)\s*\{[\s\S]*?rowsBlockStyle/, 'rowsFinalStyle 合并 rowsBlockStyle'); + assert.match(src, /qrBlockFinalStyle\(\)\s*\{[\s\S]*?qrBlockCustomStyle/, 'qrBlockFinalStyle 合并 qrBlockCustomStyle'); + // 样式 + assert.match(src, /\.mini-hero-badge/, 'hero 徽章 CSS'); + assert.match(src, /\.qr-tip-em/, 'qrTip 加粗 CSS'); + assert.match(src, /\.qr-tip-sub/, 'qrTip 第二行 CSS'); + assert.match(src, /\.qr-url/, 'URL 行 CSS'); + assert.match(src, /row-3col/, '三列行 CSS'); + // 反例: 禁数组 style 绑定(uni-app 静默丢弃)——先剥注释防误判 + const stripped = src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, ''); + assert.doesNotMatch(stripped, /:style="\[/, '无数组 style 绑定'); + // 反例: layoutCards 已移除 + assert.doesNotMatch(stripped, /layoutCards/, 'layoutCards 已移除'); + // 徽章样式避让企业徽标 badgeStyle + assert.match(src, /heroBadgeStyle\(\)/, 'heroBadgeStyle 避让 badgeStyle'); +}); + test('素材图标库三源同步: PHP CardIconLibrary ↔ 面板 card-icon-lib.js ↔ C端 common.js', () => { // PHP 源: 解析 CardIconLibrary::ICONS(key => svg) const php = readFileSync(resolve(BE, 'service/CardIconLibrary.php'), 'utf8'); @@ -597,7 +675,8 @@ test('design.html ×2: 图层式管理面板 + 素材图标库 + 预览显隐/ // 默认行 color: wx#07C160/save#FF9500 固定 → theme#2F6BFF → a.fg; bg: theme#E6EDFF(rgba 叠白近似) → a.bg const previewColorChain = html.match(/color: \(nc\.actIcons && nc\.actIcons\[a\.k\] && nc\.actIcons\[a\.k\]\.color\) \|\| \(a\.k === 'wx' \? '#07C160'[^"]*\)/); assert.ok(previewColorChain, comp + ' 预览 color 回落链存在'); - assert.match(html, /nc\.actIcons\[a\.k\]\.color \? nc\.actIcons\[a\.k\]\.color : \(a\.k === 'wx' \? '#07C160' : a\.k === 'save' \? '#FF9500' : \(nc\.actGridColorMode === 'theme' \? '#2F6BFF' : a\.fg\)\)"/, comp + ' 默认行前景输入占位与预览同链(wx/save 固定色 + theme/preset 档)'); + // 2.19.9 起 theme 段可被所选名片设计字段覆盖(previewCard.theme_color 优先, 无则回落 #2F6BFF) + assert.match(html, /nc\.actIcons\[a\.k\]\.color \? nc\.actIcons\[a\.k\]\.color : \(a\.k === 'wx' \? '#07C160' : a\.k === 'save' \? '#FF9500' : \(nc\.actGridColorMode === 'theme' \? \(\(nc\.tempData && nc\.tempData\.previewCard && nc\.tempData\.previewCard\.theme_color\) \|\| '#2F6BFF'\) : a\.fg\)\)"/, comp + ' 默认行前景输入占位与预览同链(wx/save 固定色 + theme 段 previewCard 覆盖/preset 档)'); assert.match(html, /nc\.actIcons\[a\.k\]\.bg \? nc\.actIcons\[a\.k\]\.bg : \(nc\.actGridColorMode === 'theme' \? '#E6EDFF' : a\.bg\)"/, comp + ' 默认行底色输入占位与预览同链(theme 叠白近似/preset 浅底)'); assert.doesNotMatch(html, /actIcons\[a\.k\]\.color \? nc\.actIcons\[a\.k\]\.color : '#2F6BFF'/, comp + ' 默认行前景占位不再恒 #2F6BFF(对多彩档是谎报)'); assert.match(html, /:value="c\.color \|\| '#2F6BFF'"/, comp + ' 自定义行前景占位(与自定义预览回落一致)'); @@ -618,6 +697,35 @@ test('design.html ×2: 图层式管理面板 + 素材图标库 + 预览显隐/ } }); +test('design.html ×2: 2.19.10 detail 对齐面板控件 + 预览消费齐备', () => { + for (const comp of ['show', 'self']) { + const html = readFileSync(resolve(BE, 'component/view/diy-card-' + comp + '/design.html'), 'utf8'); + // 面板控件: 9 字段 v-model / chips / toggle + assert.match(html, /nc\.rowsBlockBg/, comp + ' rowsBlockBg chips'); + assert.match(html, /nc\.rowsBlockRadius/, comp + ' rowsBlockRadius slide'); + assert.match(html, /nc\.rowsBlockPad/, comp + ' rowsBlockPad slide'); + assert.match(html, /nc\.qrBlockBg/, comp + ' qrBlockBg chips'); + assert.match(html, /nc\.qrBlockRadius/, comp + ' qrBlockRadius slide'); + assert.match(html, /nc\.qrBlockPad/, comp + ' qrBlockPad slide'); + assert.doesNotMatch(html, /layoutCards/, comp + ' layoutCards 已移除'); + assert.match(html, /v-model="nc\.badgeShow"/, comp + ' badgeShow toggle'); + assert.match(html, /v-model="nc\.badgeText"/, comp + ' badgeText input'); + assert.match(html, /v-model="nc\.showRowLabels"/, comp + ' showRowLabels toggle'); + assert.match(html, /nc\.copyBtnScope/, comp + ' copyBtnScope chips'); + assert.match(html, /nc\.copyBtnLook/, comp + ' copyBtnLook chips'); + assert.match(html, /v-model="nc\.qrTipEm"/, comp + ' qrTipEm input'); + assert.match(html, /v-model="nc\.qrTipSub"/, comp + ' qrTipSub input'); + assert.match(html, /v-model="nc\.qrUrlShow"/, comp + ' qrUrlShow toggle'); + // 预览消费: 徽章 + 三列行 + 复制按钮 + qrTip 多段 + URL 行 + assert.match(html, /nc\.badgeShow.*badgeText|badgeText.*nc\.badgeShow/s, comp + ' 预览消费 badge'); + assert.match(html, /nc\.showRowLabels/, comp + ' 预览消费 showRowLabels'); + assert.match(html, /nc\.copyBtnLook/, comp + ' 预览消费 copyBtnLook'); + assert.match(html, /nc\.qrTipEm/, comp + ' 预览消费 qrTipEm'); + assert.match(html, /nc\.qrTipSub/, comp + ' 预览消费 qrTipSub'); + assert.match(html, /nc\.qrUrlShow/, comp + ' 预览消费 qrUrlShow'); + } +}); + test('card-act-icon.js: 类名委托修 2.18 选图标 Bug + $set 写回 + 素材库唯一图标来源 + 次序调整(2.19+2.19.1)', () => { const src = readFileSync(resolve(BE, 'component/view/public/js/card-act-icon.js'), 'utf8'); assert.match(src, /on\('click', '\.js-act-lib'/, '素材图标库走类名委托');