From 0c0a6a1cb89d47945596447c938941fc95be58b0 Mon Sep 17 00:00:00 2001
From: ZF sun <34314687@qq.com>
Date: Fri, 23 Oct 2026 14:21:36 +0800
Subject: [PATCH] =?UTF-8?q?feat(card-diy):=20=E5=9B=9B=E7=BB=84=E4=BB=B6?=
=?UTF-8?q?=E5=AD=97=E5=8F=B7=E5=AE=9A=E5=88=B6=E5=8C=96=E6=B6=88=E8=B4=B9?=
=?UTF-8?q?=20+=20=E5=A5=91=E7=BA=A6=E6=B5=8B=E8=AF=95(12=20=E9=A1=B9)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- entry: 主/副标题与按钮字号内联消费(titleSize/subSize/btnSize, px→rpx ×2),
文字箭头拆 btnArrowStyle(比按钮大一档=28rpx)
- list: 拆 gridNameStyle/scrollNameStyle/metaStyle/scrollMetaStyle, 横滑与
人数徽标按 -1px 派生保持历史渲染; infoStyle 保持纯色(箭头/空态共用)
- mini(Show/Self 基件): name/cardTitle/sub/label/value/act/btn 七项字号内联,
拆 cardTitleStyle/rowLabelStyle/detailTextStyle; labelStyle 保持纯色给图标/qr tip
- self: miniConfig 透传 7 个字号字段
- 新增 tests/diy-card-font-size.test.js: 三层口径契约(diy_view 默认值 ↔
design.html 面板/重置 ↔ vue 消费) + 派生关系 + 颜色 value 兜底 + 反例自测
验证: 契约 12/12 绿; diy-card 全套测试 0 失败; E2E 截图目检通过
---
components-diy/diy-card-entry.vue | 21 ++-
components-diy/diy-card-list.vue | 50 +++++--
components-diy/diy-card-mini.vue | 54 ++++++--
components-diy/diy-card-self.vue | 20 ++-
tests/diy-card-font-size.test.js | 215 ++++++++++++++++++++++++++++++
5 files changed, 334 insertions(+), 26 deletions(-)
create mode 100644 tests/diy-card-font-size.test.js
diff --git a/components-diy/diy-card-entry.vue b/components-diy/diy-card-entry.vue
index 59b8669..3bb1c88 100644
--- a/components-diy/diy-card-entry.vue
+++ b/components-diy/diy-card-entry.vue
@@ -14,7 +14,7 @@
{{ value.btnText }}
- ›
+ ›
@@ -103,11 +103,13 @@ export default {
titleStyle() {
return this.$util.objToStyle({
color: this.v.textColor || (this.isMinimal ? CardTokens.color.text : '#FFFFFF'),
+ fontSize: px2rpx(num(this.v.titleSize, 16)) + 'rpx',
});
},
subStyle() {
return this.$util.objToStyle({
color: this.v.textColor || (this.isMinimal ? CardTokens.color.textSub : '#FFFFFF'),
+ fontSize: px2rpx(num(this.v.subSize, 12)) + 'rpx',
opacity: this.isMinimal ? 1 : 0.85,
});
},
@@ -135,13 +137,24 @@ export default {
},
btnTextStyle() {
const v = this.v;
+ const fontSize = px2rpx(num(this.v.btnSize, 12)) + 'rpx';
if (this.buttonStyle === 'ghost') {
- return this.$util.objToStyle({ color: v.accentColor || '#FFEB3B' });
+ return this.$util.objToStyle({ color: v.accentColor || '#FFEB3B', fontSize: fontSize });
}
if (this.buttonStyle === 'link') {
- return this.$util.objToStyle({ color: v.textColor || CardTokens.color.primary });
+ return this.$util.objToStyle({ color: v.textColor || CardTokens.color.primary, fontSize: fontSize });
}
- return this.$util.objToStyle({ color: '#333333' });
+ return this.$util.objToStyle({ color: '#333333', fontSize: fontSize });
+ },
+ /**
+ * 文字箭头按钮的箭头: 仅 link 档渲染, 颜色与该档按钮文字一致;
+ * 字号比按钮文字略大一档(默认 14px = 28rpx, 与历史渲染一致)
+ */
+ btnArrowStyle() {
+ return this.$util.objToStyle({
+ color: this.v.textColor || CardTokens.color.primary,
+ fontSize: px2rpx(num(this.v.btnSize, 12) + 2) + 'rpx',
+ });
},
},
methods: {
diff --git a/components-diy/diy-card-list.vue b/components-diy/diy-card-list.vue
index 09925b7..0537b9a 100644
--- a/components-diy/diy-card-list.vue
+++ b/components-diy/diy-card-list.vue
@@ -27,8 +27,8 @@
{{ initialOf(item.name) }}
- {{ item.name || '未命名名片' }}
- {{ infoLine(item) }}
+ {{ item.name || '未命名名片' }}
+ {{ infoLine(item) }}
企业
@@ -46,10 +46,10 @@
- {{ item.name || '未命名名片' }}
+ {{ item.name || '未命名名片' }}
企业
- {{ infoLine(item) }}
+ {{ infoLine(item) }}
›
@@ -129,11 +129,40 @@ export default {
});
},
titleStyle() {
- return this.$util.objToStyle({ color: this.v.titleColor || CardTokens.color.text });
+ return this.$util.objToStyle({
+ color: this.v.titleColor || CardTokens.color.text,
+ fontSize: px2rpx(num(this.v.titleSize, 16)) + 'rpx',
+ });
},
- nameStyle() {
- return this.$util.objToStyle({ color: this.v.nameColor || CardTokens.color.text });
+ /** 网格/列表卡片姓名: 字号可配(默认 14px = 28rpx) */
+ gridNameStyle() {
+ return this.$util.objToStyle({
+ color: this.v.nameColor || CardTokens.color.text,
+ fontSize: px2rpx(num(this.v.nameSize, 14)) + 'rpx',
+ });
},
+ /** 横滑竖卡姓名: 比网格小一档(默认 13px = 26rpx, 与历史渲染一致) */
+ scrollNameStyle() {
+ return this.$util.objToStyle({
+ color: this.v.nameColor || CardTokens.color.text,
+ fontSize: px2rpx(num(this.v.nameSize, 14) - 1) + 'rpx',
+ });
+ },
+ /** 网格/列表卡片信息行: 字号可配(默认 11px = 22rpx) */
+ metaStyle() {
+ return this.$util.objToStyle({
+ color: this.v.infoColor || CardTokens.color.textSub,
+ fontSize: px2rpx(num(this.v.infoSize, 11)) + 'rpx',
+ });
+ },
+ /** 横滑竖卡信息行: 比网格小一档(默认 10px = 20rpx, 与历史渲染一致) */
+ scrollMetaStyle() {
+ return this.$util.objToStyle({
+ color: this.v.infoColor || CardTokens.color.textSub,
+ fontSize: px2rpx(num(this.v.infoSize, 11) - 1) + 'rpx',
+ });
+ },
+ /** infoColor 纯色(箭头/空态用, 不带字号) */
infoStyle() {
return this.$util.objToStyle({ color: this.v.infoColor || CardTokens.color.textSub });
},
@@ -141,10 +170,15 @@ export default {
return this.$util.objToStyle({
color: CardTokens.color.primary,
background: 'rgba(47,107,255,0.08)',
+ // 人数徽标比信息行小一档(默认 10px = 20rpx, 与历史渲染一致)
+ fontSize: px2rpx(num(this.v.infoSize, 11) - 1) + 'rpx',
});
},
moreStyle() {
- return this.$util.objToStyle({ color: CardTokens.color.textLabel });
+ return this.$util.objToStyle({
+ color: CardTokens.color.textLabel,
+ fontSize: px2rpx(num(this.v.infoSize, 11)) + 'rpx',
+ });
},
itemStyle() {
const gap = num(this.v.gap, 10);
diff --git a/components-diy/diy-card-mini.vue b/components-diy/diy-card-mini.vue
index 1d573a4..d756bdf 100644
--- a/components-diy/diy-card-mini.vue
+++ b/components-diy/diy-card-mini.vue
@@ -55,7 +55,7 @@
{{ card.name || '未命名名片' }}
企业
- {{ card.card_title }}
+ {{ card.card_title }}
{{ subLine }}
@@ -70,7 +70,7 @@
- {{ row.label }}
+ {{ row.label }}
{{ row.value }}
@@ -100,7 +100,7 @@
- {{ detailBtnText || '发送名片' }}
+ {{ detailBtnText || '发送名片' }}
@@ -272,16 +272,41 @@ export default {
return this.$util.objToStyle({ background: avatarBg(this.card) });
},
nameStyle() {
- return this.$util.objToStyle({ color: this.c.nameColor || CardTokens.color.text });
+ return this.$util.objToStyle({
+ color: this.c.nameColor || CardTokens.color.text,
+ fontSize: px2rpx(num(this.c.nameSize, 17)) + 'rpx',
+ });
},
+ /** 名片头衔(设计器 card_title): 默认 13px = 26rpx, 与副行 24rpx 区分 */
+ cardTitleStyle() {
+ return this.$util.objToStyle({
+ color: this.c.infoColor || CardTokens.color.textSub,
+ fontSize: px2rpx(num(this.c.cardTitleSize, 13)) + 'rpx',
+ });
+ },
+ /** 副行(公司/职位)与空态提示共用: 默认 12px = 24rpx */
subTextStyle() {
- return this.$util.objToStyle({ color: this.c.infoColor || CardTokens.color.textSub });
+ return this.$util.objToStyle({
+ color: this.c.infoColor || CardTokens.color.textSub,
+ fontSize: px2rpx(num(this.c.subSize, 12)) + 'rpx',
+ });
},
+ /** labelColor 纯色(资料行图标/二维码提示用, 不带字号) */
labelStyle() {
return this.$util.objToStyle({ color: this.c.labelColor || CardTokens.color.textLabel });
},
+ /** 资料行文字标签: 默认 12px = 24rpx */
+ rowLabelStyle() {
+ return this.$util.objToStyle({
+ color: this.c.labelColor || CardTokens.color.textLabel,
+ fontSize: px2rpx(num(this.c.labelSize, 12)) + 'rpx',
+ });
+ },
valueStyle() {
- return this.$util.objToStyle({ color: this.c.infoColor || CardTokens.color.textSub });
+ return this.$util.objToStyle({
+ color: this.c.infoColor || CardTokens.color.textSub,
+ fontSize: px2rpx(num(this.c.valueSize, 12)) + 'rpx',
+ });
},
badgeStyle() {
return this.$util.objToStyle({ background: avatarBg(this.card) });
@@ -299,7 +324,10 @@ export default {
});
},
actPrimaryTextStyle() {
- return this.$util.objToStyle({ color: '#FFFFFF' });
+ return this.$util.objToStyle({
+ color: '#FFFFFF',
+ fontSize: px2rpx(num(this.c.actSize, 12)) + 'rpx',
+ });
},
actGhostStyle() {
const theme = (this.card && this.card.theme_color) || CardTokens.color.primary;
@@ -312,7 +340,17 @@ export default {
});
},
actGhostTextStyle() {
- return this.$util.objToStyle({ color: (this.card && this.card.theme_color) || CardTokens.color.primary });
+ return this.$util.objToStyle({
+ color: (this.card && this.card.theme_color) || CardTokens.color.primary,
+ fontSize: px2rpx(num(this.c.actSize, 12)) + 'rpx',
+ });
+ },
+ /** 发送名片按钮文字: 默认 13px = 26rpx */
+ detailTextStyle() {
+ return this.$util.objToStyle({
+ color: '#FFFFFF',
+ fontSize: px2rpx(num(this.c.btnSize, 13)) + 'rpx',
+ });
},
detailBtnStyle() {
return this.$util.objToStyle({
diff --git a/components-diy/diy-card-self.vue b/components-diy/diy-card-self.vue
index d0539e3..487ffec 100644
--- a/components-diy/diy-card-self.vue
+++ b/components-diy/diy-card-self.vue
@@ -68,12 +68,20 @@ export default {
miniConfig() {
const v = this.value || {};
return {
- bgColor: v.bgColor,
- nameColor: v.nameColor,
- infoColor: v.infoColor,
- labelColor: v.labelColor,
- borderRadius: v.borderRadius,
- padding: v.padding,
+ bgColor: v.bgColor,
+ nameColor: v.nameColor,
+ infoColor: v.infoColor,
+ labelColor: v.labelColor,
+ borderRadius: v.borderRadius,
+ padding: v.padding,
+ // 字号定制(px 口径, 缺省时基件按历史默认值渲染)
+ nameSize: v.nameSize,
+ cardTitleSize: v.cardTitleSize,
+ subSize: v.subSize,
+ labelSize: v.labelSize,
+ valueSize: v.valueSize,
+ actSize: v.actSize,
+ btnSize: v.btnSize,
showQuick: v.showQuick,
showDetailBtn: v.showDetailBtn,
detailBtnText: v.detailBtnText,
diff --git a/tests/diy-card-font-size.test.js b/tests/diy-card-font-size.test.js
new file mode 100644
index 0000000..b43cece
--- /dev/null
+++ b/tests/diy-card-font-size.test.js
@@ -0,0 +1,215 @@
+/**
+ * 名片 DIY 组件 · 字号定制化三层口径契约测试
+ *
+ * 锁死: diy_view.php 默认值 ↔ design.html 面板(slide+预览消费) ↔ C 端 vue 消费
+ * 口径: 字号字段一律 px 配置, C 端渲染 ×2 转 rpx(px2rpx); 默认值 = 历史硬编码值
+ *
+ * 派生关系(保证默认渲染与历史一致):
+ * - list 横滑竖卡: 姓名/信息比网格小一档(-1px)
+ * - list 人数徽标: 比信息行小一档(-1px); 「查看全部」= 信息字号
+ * - entry 文字箭头的箭头: 比按钮文字大一档(+2px)
+ */
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { readFileSync } from 'node:fs';
+import { resolve, dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const BE = resolve(__dirname, '../../backend/src/addon/card');
+const FE = resolve(__dirname, '../components-diy');
+
+/** 三层口径定义: 字段 → [默认值(px), 消费侧期望字面量] */
+const CONTRACT = {
+ entry: {
+ defaults: { titleSize: 16, subSize: 12, btnSize: 12 },
+ colors: { bgColorStart: '#2F6BFF', bgColorEnd: '#1E40AF', textColor: '#FFFFFF', accentColor: '#FFEB3B' },
+ layout: { borderRadius: 12, padding: 16 },
+ vueRead: 'this.v.',
+ vueFile: 'diy-card-entry.vue',
+ },
+ list: {
+ defaults: { titleSize: 16, nameSize: 14, infoSize: 11 },
+ colors: { bgColor: '#FFFFFF', titleColor: '#333333', nameColor: '#1F2329', infoColor: '#888888' },
+ layout: { borderRadius: 12, padding: 12, gap: 10 },
+ vueRead: 'this.v.',
+ vueFile: 'diy-card-list.vue',
+ },
+ show: {
+ defaults: { nameSize: 17, cardTitleSize: 13, subSize: 12, labelSize: 12, valueSize: 12, actSize: 12, btnSize: 13 },
+ colors: { bgColor: '#FFFFFF', nameColor: '#1F2329', infoColor: '#888888', labelColor: '#999999' },
+ layout: { borderRadius: 16, padding: 16 },
+ vueRead: 'this.c.',
+ vueFile: 'diy-card-mini.vue',
+ },
+ self: {
+ defaults: { nameSize: 17, cardTitleSize: 13, subSize: 12, labelSize: 12, valueSize: 12, actSize: 12, btnSize: 13 },
+ colors: { bgColor: '#FFFFFF', nameColor: '#1F2329', infoColor: '#888888', labelColor: '#999999' },
+ layout: { borderRadius: 16, padding: 16 },
+ vueRead: 'this.c.',
+ vueFile: 'diy-card-mini.vue',
+ },
+};
+
+/** 从 diy_view.php 提取组件 value JSON(容忍单引号 PHP 串内的转义) */
+const PHP_NAMES = { entry: 'DiyCardEntry', list: 'DiyCardList', show: 'DiyCardShow', self: 'DiyCardSelf' };
+function readValueJson(component) {
+ const php = readFileSync(resolve(BE, 'config/diy_view.php'), 'utf8');
+ const re = new RegExp("'name'\\s*=>\\s*'" + PHP_NAMES[component] + "'[\\s\\S]*?'value'\\s*=>\\s*'([^\']+)'");
+ const m = php.match(re);
+ assert.ok(m, 'diy_view.php 中找到组件 ' + PHP_NAMES[component]);
+ return JSON.parse(m[1]);
+}
+
+test('diy_view.php 四组件默认值包含全部字号字段且等于历史渲染值', () => {
+ for (const [comp, cfg] of Object.entries(CONTRACT)) {
+ const value = readValueJson(comp);
+ for (const [field, dft] of Object.entries(cfg.defaults)) {
+ assert.equal(Number(value[field]), dft, comp + '.' + field + ' 默认值应为 ' + dft);
+ }
+ }
+});
+
+test('diy_view.php 配色/圆角间距默认值与契约一致(重置按钮的最终事实来源)', () => {
+ for (const [comp, cfg] of Object.entries(CONTRACT)) {
+ const value = readValueJson(comp);
+ for (const [field, dft] of Object.entries(cfg.colors)) {
+ assert.equal(String(value[field]).toUpperCase(), dft.toUpperCase(), comp + '.' + field + ' 颜色默认值应为 ' + dft);
+ }
+ for (const [field, dft] of Object.entries(cfg.layout)) {
+ assert.equal(Number(value[field]), dft, comp + '.' + field + ' 默认值应为 ' + dft);
+ }
+ }
+});
+
+test('design.html 样式面板提供一键恢复默认, 重置字段与默认值逐一对应', () => {
+ for (const [comp, cfg] of Object.entries(CONTRACT)) {
+ const html = readFileSync(resolve(BE, 'component/view/diy-card-' + comp + '/design.html'), 'utf8');
+ assert.ok(html.includes('diy-card-reset'), comp + ' 样式面板缺少恢复默认按钮');
+ const btn = html.match(/