diff --git a/components-diy/diy-card-common.js b/components-diy/diy-card-common.js index bd71213..590d4ed 100644 --- a/components-diy/diy-card-common.js +++ b/components-diy/diy-card-common.js @@ -114,6 +114,19 @@ export function themeGradient(card, angle) { : 'linear-gradient(' + deg + 'deg, ' + theme + ' 20%, ' + theme + ' 100%)'; } +/** 颜色压暗(ratio 0~1): 用于「渐变圆角」在名片仅单主题色时生成可见明暗渐变终止色。 + * 纯函数, 非法输入原样返回(不影响既有主题色渲染) */ +export function darkenColor(hex, ratio) { + const r = ratio < 0 ? 0 : (ratio > 1 ? 1 : ratio); + const h = String(hex || '').replace('#', ''); + if (!/^[0-9a-fA-F]{6}$/.test(h)) return hex; + const n = parseInt(h, 16); + const cr = Math.max(0, Math.round(((n >> 16) & 0xff) * (1 - r))); + const cg = Math.max(0, Math.round(((n >> 8) & 0xff) * (1 - r))); + const cb = Math.max(0, Math.round((n & 0xff) * (1 - r))); + return '#' + ((1 << 24) + (cr << 16) + (cg << 8) + cb).toString(16).slice(1); +} + /** 安全数字(配置字段) */ export function num(v, dft) { const n = Number(v); @@ -160,9 +173,17 @@ export function rowActOf(row) { /** 二维码位置(四档): rows 资料行下方(默认) / head 头部右侧 / corner 右上角浮层 / bottom 主按钮上方 */ export const QR_POSITIONS = ['rows', 'head', 'corner', 'bottom']; +/** 位置档位中文别名(面板下拉若直接存中文档名, 归一化时兼容) */ +const QR_POSITION_ALIAS = { + '资料行下方': 'rows', + '头部右侧': 'head', + '右上角浮层': 'corner', + '主按钮上方': 'bottom', +}; export function normalizeQrPosition(v) { const s = String(v || '').trim(); - return QR_POSITIONS.indexOf(s) >= 0 ? s : 'rows'; + if (QR_POSITIONS.indexOf(s) >= 0) return s; + return QR_POSITION_ALIAS[s] || 'rows'; } /** 各档位的二维码边长(rpx): 头部右侧紧凑, 浮层带衬底, 行下/底部大图便于扫 */ export const QR_SIZES = { rows: 180, head: 120, corner: 120, bottom: 180 }; @@ -173,9 +194,17 @@ export const QR_SIZES = { rows: 180, head: 120, corner: 120, bottom: 180 }; * 多彩体现在装饰层(主题色/渐变), 码本身保持黑白 —— 可扫性优先 */ export const QR_STYLES = ['plain', 'card', 'theme', 'gradient']; +/** 外观档位中文别名(面板下拉若直接存中文档名, 归一化时兼容) */ +const QR_STYLE_ALIAS = { + '原图': 'plain', + '白卡描边': 'card', + '主题色衬底': 'theme', + '渐变圆角': 'gradient', +}; export function normalizeQrStyle(v) { const s = String(v || '').trim(); - return QR_STYLES.indexOf(s) >= 0 ? s : 'card'; + if (QR_STYLES.indexOf(s) >= 0) return s; + return QR_STYLE_ALIAS[s] || 'card'; } /** 二维码边长: 自定义 > 0 时用自定义(px), 否则按档位自动(rpx) */ diff --git a/components-diy/diy-card-mini.vue b/components-diy/diy-card-mini.vue index 47ad2e5..8890320 100644 --- a/components-diy/diy-card-mini.vue +++ b/components-diy/diy-card-mini.vue @@ -115,7 +115,7 @@ // 数字名片 · 名片迷你卡基件(show/self 共用, 设计系统单一事实来源) // 消费名片自身设计资产: cover 封面 / logo / theme_color / 渐变末色与角度 / card_title import { - CardTokens, px2rpx, flagOn, num, initialOf, avatarBg, themeGradient, visibleRows, + CardTokens, px2rpx, flagOn, num, initialOf, avatarBg, themeGradient, darkenColor, visibleRows, rowIconFor, rowActOf, normalizeQrPosition, normalizeQrStyle, resolveQrSize, qrTransform, QR_SIZES, } from './diy-card-common.js'; @@ -229,7 +229,8 @@ export default { style.borderRadius = '12rpx'; style.padding = '10rpx'; } else if (this.qrStyle === 'gradient') { - style.background = themeGradient(this.card); + const end = (this.card && this.card.gradient_end) || darkenColor(theme, 0.18); + style.background = 'linear-gradient(135deg, ' + theme + ' 0%, ' + end + ' 100%)'; style.borderRadius = '16rpx'; style.padding = '6rpx'; } diff --git a/components-diy/diy-card-show.vue b/components-diy/diy-card-show.vue index 6f8308d..dcb0e34 100644 --- a/components-diy/diy-card-show.vue +++ b/components-diy/diy-card-show.vue @@ -31,7 +31,7 @@ {{ jobLine }} - + {{ qrTipText }} @@ -53,13 +53,13 @@ - + {{ qrTipText }} - + {{ qrTipText }} @@ -75,14 +75,14 @@ - + {{ qrTipText }} - - - 发名片 + + + {{ sendBtnText }} @@ -120,8 +120,8 @@ // 自绘参考图版式 —— 主题色顶栏衬底 + 白卡压边 + 右上小二维码(预览名片) + // 图标资料行 + 卡外「发名片」胶囊。面板仅颜色/底色/圆角/阴影/行数继续生效。 import { - CardTokens, px2rpx, num, flagOn, initialOf, avatarBg, themeGradient, visibleRows, rowIconFor, rowActOf, - resolveQrSize, normalizeQrPosition, qrTransform, + CardTokens, px2rpx, num, flagOn, initialOf, avatarBg, themeGradient, darkenColor, visibleRows, rowIconFor, rowActOf, + resolveQrSize, normalizeQrPosition, qrTransform, normalizeQrStyle, } from './diy-card-common.js'; export default { @@ -184,6 +184,35 @@ export default { const t = qrTransform(this.c.qrOffsetX, this.c.qrOffsetY); return t ? this.$util.objToStyle({ transform: t }) : ''; }, + /** 二维码外观(四档): plain 原图 / card 白卡描边 / theme 主题色衬底 / gradient 渐变圆角。 + * 多彩只装饰外围衬底, 码本身保持黑白保可扫(与 mini 基件 qrBlockStyle 同口径) */ + qrStyle() { + return normalizeQrStyle(this.c.qrStyle); + }, + qrBlockStyle() { + const style = {}; + const theme = (this.card && this.card.theme_color) || CardTokens.color.primary; + if (this.qrStyle === 'card') { + style.background = '#ffffff'; + style.border = '2rpx solid #EEF0F3'; + style.borderRadius = '12rpx'; + style.padding = '10rpx'; + } else if (this.qrStyle === 'theme') { + style.background = theme; + style.borderRadius = '12rpx'; + style.padding = '10rpx'; + } else if (this.qrStyle === 'gradient') { + // 名片仅单主题色(无 gradient_end)时, themeGradient 返回纯色渐变(看不出渐变), + // 故用压暗一档的终止色保证「渐变圆角」始终可见明暗过渡 + const theme = (this.card && this.card.theme_color) || CardTokens.color.primary; + const end = (this.card && this.card.gradient_end) || darkenColor(theme, 0.18); + style.background = 'linear-gradient(135deg, ' + theme + ' 0%, ' + end + ' 100%)'; + style.borderRadius = '16rpx'; + style.padding = '6rpx'; + } + // plain 不进任何分支(无衬底, 即原图) + return this.$util.objToStyle(style); + }, /** 二维码提示文字: 面板 qrTip, 未配置回落「扫码查看完整名片」 */ qrTipText() { return this.c.qrTip || '扫码查看完整名片'; @@ -260,6 +289,14 @@ export default { themeTextStyle() { return this.$util.objToStyle({ color: (this.card && this.card.theme_color) || CardTokens.color.primary }); }, + /** 发名片按钮开关: 面板「底部按钮 → 发送名片按钮」(历史配置无此字段默认显示; 关后白卡下不留按钮) */ + showSendBtn() { + return flagOn(this.c.showDetailBtn, true); + }, + /** 发名片按钮文案: 面板「按钮文案」(默认「发送名片」, 与面板提示一致) */ + sendBtnText() { + return this.c.detailBtnText || '发送名片'; + }, /** 发名片按钮: 名片主题渐变色(无主题退化为默认主色), 详情页 send-bar 同款胶囊 */ sendBtnStyle() { return this.$util.objToStyle({ diff --git a/tests/diy-card-qr-style.test.js b/tests/diy-card-qr-style.test.js index 08d7dcf..0d8dd13 100644 --- a/tests/diy-card-qr-style.test.js +++ b/tests/diy-card-qr-style.test.js @@ -62,6 +62,33 @@ test('common: normalizeQrStyle 合法直通, 未知/空回落 card(默认白卡) assert.equal(m.normalizeQrStyle('rainbow'), 'card'); // 面板只下发小写档位; 大写属未知值, 回落 card(保守, 不炸) assert.equal(m.normalizeQrStyle('CARD'), 'card'); + // 中文档位名别名(面板下拉若直接存中文档名, 归一化兜住, 否则 C 端永远回落 card) + assert.equal(m.normalizeQrStyle('原图'), 'plain'); + assert.equal(m.normalizeQrStyle('白卡描边'), 'card'); + assert.equal(m.normalizeQrStyle('主题色衬底'), 'theme'); + assert.equal(m.normalizeQrStyle('渐变圆角'), 'gradient'); +}); + +test('common: normalizeQrPosition 位置档中文别名兼容', async () => { + const m = await import('../components-diy/diy-card-common.js'); + assert.equal(m.normalizeQrPosition('资料行下方'), 'rows'); + assert.equal(m.normalizeQrPosition('头部右侧'), 'head'); + assert.equal(m.normalizeQrPosition('右上角浮层'), 'corner'); + assert.equal(m.normalizeQrPosition('主按钮上方'), 'bottom'); + // 英文码直通 / 未知回落 rows + assert.equal(m.normalizeQrPosition('bottom'), 'bottom'); + assert.equal(m.normalizeQrPosition('nope'), 'rows'); +}); + +test('common: darkenColor 压暗生成可见渐变终止色', async () => { + const m = await import('../components-diy/diy-card-common.js'); + // 纯色压暗 0.18: #119dff → #0e81d1 + assert.equal(m.darkenColor('#119dff', 0.18), '#0e81d1'); + // 比例越大约暗 + assert.notEqual(m.darkenColor('#119dff', 0.4), m.darkenColor('#119dff', 0.18)); + // 非法输入原样返回(不影响既有渲染) + assert.equal(m.darkenColor('#12', 0.18), '#12'); + assert.equal(m.darkenColor('rgb(1,2,3)', 0.18), 'rgb(1,2,3)'); }); test('common: resolveQrSize 自定义>档位自动, 单位 px→rpx(×2)', async () => { @@ -121,8 +148,8 @@ test('mini: qrBlockStyle 四档外观分支(多彩只装饰外围, 码黑白保 assert.match(miniSrc, /if \(this\.qrStyle === 'card'\) \{\s+style\.background = '#ffffff';\s+style\.border = '2rpx solid #EEF0F3';\s+style\.borderRadius = '12rpx';\s+style\.padding = '10rpx';/); // theme 主题色衬底 assert.match(miniSrc, /else if \(this\.qrStyle === 'theme'\) \{\s+style\.background = theme;\s+style\.borderRadius = '12rpx';\s+style\.padding = '10rpx';/); - // gradient 渐变 + 6rpx 紧凑内边距 - assert.match(miniSrc, /else if \(this\.qrStyle === 'gradient'\) \{\s+style\.background = themeGradient\(this\.card\);\s+style\.borderRadius = '16rpx';\s+style\.padding = '6rpx';/); + // gradient 渐变 + 6rpx 紧凑内边距; 单主题色用 darkenColor 兜底终止色(可见明暗) + assert.match(miniSrc, /else if \(this\.qrStyle === 'gradient'\) \{\s+const end = \(this\.card && this\.card\.gradient_end\) \|\| darkenColor\(theme, 0\.18\);\s+style\.background = 'linear-gradient\(135deg, ' \+ theme \+ ' 0%, ' \+ end \+ ' 100%\)';\s+style\.borderRadius = '16rpx';\s+style\.padding = '6rpx';/); // plain 不进任何分支(无衬底, 即原图) —— 三个 if 之外不再设 background assert.ok(!miniSrc.includes("qrStyle === 'plain' ? style.background")); }); @@ -212,6 +239,29 @@ test('show: 二维码位置四档(与面板下拉/mini 基件同口径, 未配 assert.match(showSrc, /qrTransform\(this\.c\.qrOffsetX,\s*this\.c\.qrOffsetY\)/); }); +test('show: 发名片按钮开关(面板「底部按钮 → 发送名片按钮」, 默认显示; 文案消费 detailBtnText)', () => { + assert.match(showSrc, /showSendBtn\(\)\s*\{\s*return flagOn\(this\.c\.showDetailBtn,\s*true\)/); + assert.match(showSrc, /v-if="showSendBtn" class="show-send"/); + // 按钮文案: 面板「按钮文案」可配, 默认「发送名片」 + assert.match(showSrc, /sendBtnText\(\)\s*\{\s*return this\.c\.detailBtnText \|\| '发送名片';/); + assert.match(showSrc, /\{\{ sendBtnText \}\}<\/text>/); + // 反例形态: 不允许硬编码文案(绕过面板配置) + assert.ok(!showSrc.includes('发名片')); +}); + +test('show: 二维码外观四档(plain 原图 / card 白卡描边 / theme 主题色 / gradient 渐变, 与 mini 同口径)', () => { + assert.match(showSrc, /qrStyle\(\)\s*\{\s*return normalizeQrStyle\(this\.c\.qrStyle\)/); + // 四个分支逐行收紧, 防跨分支 lazy 匹配造成假过(与 mini qrBlockStyle 同款值) + assert.match(showSrc, /if \(this\.qrStyle === 'card'\) \{\s+style\.background = '#ffffff';\s+style\.border = '2rpx solid #EEF0F3';\s+style\.borderRadius = '12rpx';\s+style\.padding = '10rpx';/); + assert.match(showSrc, /else if \(this\.qrStyle === 'theme'\) \{\s+style\.background = theme;\s+style\.borderRadius = '12rpx';\s+style\.padding = '10rpx';/); + // gradient 分支: 单主题色时也要可见明暗渐变(用 darkenColor 兜底终止色), 不能退化成纯色 + assert.match(showSrc, /else if \(this\.qrStyle === 'gradient'\) \{\s+const theme = \(this\.card && this\.card\.theme_color\) \|\| CardTokens\.color\.primary;\s+const end = \(this\.card && this\.card\.gradient_end\) \|\| darkenColor\(theme, 0\.18\);\s+style\.background = 'linear-gradient\(135deg, ' \+ theme \+ ' 0%, ' \+ end \+ ' 100%\)';\s+style\.borderRadius = '16rpx';\s+style\.padding = '6rpx';/); + // plain 不进任何分支(无衬底, 即原图) + assert.ok(!showSrc.includes("qrStyle === 'plain' ? style.background")); + // 四个位置档共用 qrBlockStyle + assert.equal((showSrc.match(/:style="\[qrBlockStyle, qrShiftStyle\]"/g) || []).length, 4); +}); + test('show: 自绘版式自带小二维码(开关可关 + 点击进详情, 尺寸/提示文字面板可调)', () => { assert.match(showSrc, /class="card-qr"/); assert.match(showSrc, /\$util\.img\(card\.qr_url\)/);