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\)/);