feat(card-diy): 四组件字号定制化消费 + 契约测试(12 项)
- 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 截图目检通过
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
<!-- 按钮(可选; link 模式为文字箭头) -->
|
||||
<view v-if="buttonStyle !== 'none' && value.btnText" :class="['entry-btn', 'btn-' + buttonStyle]" :style="btnStyle" hover-class="btn-press" hover-stay-time="60">
|
||||
<text class="entry-btn-text" :style="btnTextStyle">{{ value.btnText }}</text>
|
||||
<text v-if="buttonStyle === 'link'" class="btn-arrow" :style="btnTextStyle">›</text>
|
||||
<text v-if="buttonStyle === 'link'" class="btn-arrow" :style="btnArrowStyle">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -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: {
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
<image v-if="item.avatar" class="avatar-img" :src="$util.img(item.avatar)" mode="aspectFill" />
|
||||
<text v-else class="avatar-initial">{{ initialOf(item.name) }}</text>
|
||||
</view>
|
||||
<text class="scroll-name" :style="nameStyle">{{ item.name || '未命名名片' }}</text>
|
||||
<text v-if="infoLine(item)" class="scroll-meta" :style="infoStyle">{{ infoLine(item) }}</text>
|
||||
<text class="scroll-name" :style="scrollNameStyle">{{ item.name || '未命名名片' }}</text>
|
||||
<text v-if="infoLine(item)" class="scroll-meta" :style="scrollMetaStyle">{{ infoLine(item) }}</text>
|
||||
<text v-if="isEnterprise(item)" class="item-badge" :style="badgeStyle(item)">企业</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
@@ -46,10 +46,10 @@
|
||||
</view>
|
||||
<view class="item-info">
|
||||
<view class="name-row">
|
||||
<text class="item-name" :style="nameStyle">{{ item.name || '未命名名片' }}</text>
|
||||
<text class="item-name" :style="gridNameStyle">{{ item.name || '未命名名片' }}</text>
|
||||
<text v-if="isEnterprise(item)" class="item-badge" :style="badgeStyle(item)">企业</text>
|
||||
</view>
|
||||
<text v-if="infoLine(item)" class="item-meta" :style="infoStyle">{{ infoLine(item) }}</text>
|
||||
<text v-if="infoLine(item)" class="item-meta" :style="metaStyle">{{ infoLine(item) }}</text>
|
||||
</view>
|
||||
<text v-if="showArrow" class="item-arrow" :style="infoStyle">›</text>
|
||||
</view>
|
||||
@@ -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);
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<text class="mini-name" :style="nameStyle">{{ card.name || '未命名名片' }}</text>
|
||||
<text v-if="isEnterprise" class="mini-badge" :style="badgeStyle">企业</text>
|
||||
</view>
|
||||
<text v-if="card.card_title" class="mini-card-title" :style="subTextStyle">{{ card.card_title }}</text>
|
||||
<text v-if="card.card_title" class="mini-card-title" :style="cardTitleStyle">{{ card.card_title }}</text>
|
||||
<text v-if="subLine" class="mini-sub" :style="subTextStyle">{{ subLine }}</text>
|
||||
</view>
|
||||
<!-- 二维码·头部右侧(head 档: 头像|信息|二维码 三段 flex, 零遮挡) -->
|
||||
@@ -70,7 +70,7 @@
|
||||
<view v-for="(row, i) in shownRows" :key="i" class="mini-row"
|
||||
:class="{ 'row-tappable': rowTapable(row) }" :hover-class="rowTapable(row) ? 'btn-press' : 'none'" @tap="onRowTap(row)">
|
||||
<text v-if="rowIcon(row)" class="row-icon iconfont" :class="rowIcon(row)" :style="labelStyle"></text>
|
||||
<text v-else class="row-label" :style="labelStyle">{{ row.label }}</text>
|
||||
<text v-else class="row-label" :style="rowLabelStyle">{{ row.label }}</text>
|
||||
<text class="row-value" :style="valueStyle">{{ row.value }}</text>
|
||||
</view>
|
||||
<view v-if="hasMoreRows" class="row-toggle" hover-class="btn-press" @tap="expanded = !expanded">
|
||||
@@ -100,7 +100,7 @@
|
||||
|
||||
<!-- 发送名片 -->
|
||||
<view v-if="showDetailBtn" class="mini-detail" :style="detailBtnStyle" hover-class="btn-press" @tap.stop="onDetail">
|
||||
<text class="detail-text">{{ detailBtnText || '发送名片' }}</text>
|
||||
<text class="detail-text" :style="detailTextStyle">{{ detailBtnText || '发送名片' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
215
tests/diy-card-font-size.test.js
Normal file
215
tests/diy-card-font-size.test.js
Normal file
@@ -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(/<button type="button" class="diy-card-reset" @click="([^"]+)"/);
|
||||
assert.ok(btn, comp + ' 重置按钮缺少内联 @click 赋值');
|
||||
const click = btn[1];
|
||||
// 字号(px 数值)
|
||||
for (const [field, dft] of Object.entries(cfg.defaults)) {
|
||||
assert.ok(click.includes('nc.' + field + '=' + dft), comp + ' 重置缺少 ' + field + '=' + dft);
|
||||
}
|
||||
// 颜色(带引号)
|
||||
for (const [field, dft] of Object.entries(cfg.colors)) {
|
||||
assert.ok(click.includes("nc." + field + "='" + dft + "'"), comp + ' 重置缺少 ' + field + "='" + dft + "'");
|
||||
}
|
||||
// 圆角/间距(px 数值)
|
||||
for (const [field, dft] of Object.entries(cfg.layout)) {
|
||||
assert.ok(click.includes('nc.' + field + '=' + dft), comp + ' 重置缺少 ' + field + '=' + dft);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('四个 design.html 面板: 每个字号字段都有 slide 控件', () => {
|
||||
for (const comp of ['diy-card-entry', 'diy-card-list', 'diy-card-show', 'diy-card-self']) {
|
||||
const html = readFileSync(resolve(BE, 'component/view/' + comp + '/design.html'), 'utf8');
|
||||
const fields = Object.keys(CONTRACT[comp.replace('diy-card-', '')].defaults);
|
||||
for (const field of fields) {
|
||||
assert.match(html, new RegExp("<slide :data=\"\\{ field: '" + field + "'"), comp + ' 面板缺少 ' + field + ' 的 slide');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('design.html 预览必须消费字号字段(防「面板可配预览不动」漂移)', () => {
|
||||
const expectPreview = {
|
||||
'diy-card-entry': ['nc.titleSize', 'nc.subSize', 'nc.btnSize'],
|
||||
'diy-card-list': ['nc.titleSize', 'nc.nameSize', 'nc.infoSize'],
|
||||
'diy-card-show': ['nc.nameSize', 'nc.subSize', 'nc.labelSize', 'nc.valueSize', 'nc.actSize', 'nc.btnSize'],
|
||||
'diy-card-self': ['nc.nameSize', 'nc.subSize', 'nc.labelSize', 'nc.valueSize', 'nc.actSize', 'nc.btnSize'],
|
||||
};
|
||||
for (const [comp, tokens] of Object.entries(expectPreview)) {
|
||||
const html = readFileSync(resolve(BE, 'component/view/' + comp + '/design.html'), 'utf8');
|
||||
for (const token of tokens) {
|
||||
assert.ok(html.includes(token), comp + ' 预览未消费 ' + token);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('C 端 vue 按默认值消费字号字段(px2rpx 渲染)', () => {
|
||||
for (const [comp, cfg] of Object.entries(CONTRACT)) {
|
||||
const src = readFileSync(resolve(FE, cfg.vueFile), 'utf8');
|
||||
for (const [field, dft] of Object.entries(cfg.defaults)) {
|
||||
const literal = 'num(' + cfg.vueRead + field + ', ' + dft + ')';
|
||||
assert.ok(src.includes(literal), cfg.vueFile + ' 缺少消费 ' + literal);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('self.vue miniConfig 透传全部字号字段到基件', () => {
|
||||
const src = readFileSync(resolve(FE, 'diy-card-self.vue'), 'utf8');
|
||||
for (const field of Object.keys(CONTRACT.self.defaults)) {
|
||||
assert.ok(src.includes(field + ': v.' + field), 'miniConfig 缺少透传 ' + field);
|
||||
}
|
||||
});
|
||||
|
||||
test('派生关系: 横滑/徽标 -1 档, 箭头 +1 档(默认渲染与历史一致)', () => {
|
||||
const list = readFileSync(resolve(FE, 'diy-card-list.vue'), 'utf8');
|
||||
assert.ok(list.includes("num(this.v.nameSize, 14) - 1"), 'list 横滑姓名应比网格小 1px');
|
||||
assert.ok(list.includes("num(this.v.infoSize, 11) - 1"), 'list 横滑信息/人数徽标应比网格小 1px');
|
||||
const entry = readFileSync(resolve(FE, 'diy-card-entry.vue'), 'utf8');
|
||||
assert.ok(entry.includes("num(this.v.btnSize, 12) + 2"), 'entry 箭头应比按钮文字大 2px(28rpx)');
|
||||
});
|
||||
|
||||
test('design.html 每个 slide 控件都带 default 且与 diy_view.php 注册值一致(逐项重置口径)', () => {
|
||||
for (const [comp, cfg] of Object.entries(CONTRACT)) {
|
||||
const html = readFileSync(resolve(BE, 'component/view/diy-card-' + comp + '/design.html'), 'utf8');
|
||||
const value = readValueJson(comp);
|
||||
// 所有 <slide> 都必须携带 default(含内容页的 maxRows/qrSize)
|
||||
const slides = html.match(/<slide[^>]*>/g) || [];
|
||||
assert.ok(slides.length > 0, comp + ' 应存在 slide 控件');
|
||||
for (const tag of slides) {
|
||||
const fm = tag.match(/field:\s*'([A-Za-z0-9]+)'/);
|
||||
const dm = tag.match(/default:\s*(-?\d+)/);
|
||||
assert.ok(fm && dm, comp + ' slide 缺 field 或 default: ' + tag);
|
||||
const field = fm[1];
|
||||
const dft = Number(dm[1]);
|
||||
assert.equal(
|
||||
Number(value[field]), dft,
|
||||
comp + '.' + field + ' slide default=' + dft + ' 应与 diy_view.php 值 ' + value[field] + ' 一致'
|
||||
);
|
||||
}
|
||||
// 契约中全部字号字段的 slide 必须存在(防漏配)
|
||||
const allFields = slides.map(t => (t.match(/field:\s*'([A-Za-z0-9]+)'/) || [])[1]);
|
||||
for (const field of Object.keys(cfg.defaults)) {
|
||||
assert.ok(allFields.includes(field), comp + ' 缺少 ' + field + ' 的 slide(default 参数)');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('design.html 颜色控件带 value 兜底(= defaultColor, 防存量行缺字段回落 #303133)', () => {
|
||||
for (const [comp, cfg] of Object.entries(CONTRACT)) {
|
||||
const html = readFileSync(resolve(BE, 'component/view/diy-card-' + comp + '/design.html'), 'utf8');
|
||||
const tags = html.match(/<color :data="\{[^}]+\}"><\/color>/g) || [];
|
||||
assert.ok(tags.length >= Object.keys(cfg.colors).length, comp + ' color 控件数量不足');
|
||||
for (const [field, dft] of Object.entries(cfg.colors)) {
|
||||
const tag = tags.find(t => t.includes("field: '" + field + "'"));
|
||||
assert.ok(tag, comp + ' 缺少 ' + field + ' 的 color 控件');
|
||||
assert.ok(tag.includes("defaultColor: '" + dft + "'"), comp + '.' + field + ' defaultColor 应为 ' + dft);
|
||||
assert.ok(tag.includes("value: '" + dft + "'"), comp + '.' + field + ' 缺 value 兜底(应为 ' + dft + ')');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('核心 slide 组件支持 opt-in 逐项重置(不传 default 的调用方零影响)', () => {
|
||||
const js = readFileSync(resolve(BE, '../../public/static/ext/diyview/js/components.js'), 'utf8');
|
||||
assert.ok(js.includes('v-if="canReset"'), 'slide 模板缺少 canReset 条件渲染');
|
||||
assert.ok(js.includes('resetDefault'), 'slide 缺少 resetDefault 方法');
|
||||
// canReset 只能由 default 参数打开
|
||||
assert.ok(js.match(/compData\.default !== undefined[\s\S]{0,60}canReset = true/), 'canReset 开关未绑定 default 参数');
|
||||
// 颜色控件的平台自带重置仍在(逐项重置的颜色口径)
|
||||
assert.ok(js.includes('class="color-selector-reset text-color" @click="reset()"'), 'color 组件内置重置丢失');
|
||||
});
|
||||
|
||||
test('反例自测: 契约定义缺字段时测试必须失败', () => {
|
||||
// 校验 readValueJson 真的在解析 diy_view.php 而非恒真
|
||||
const php = readFileSync(resolve(BE, 'config/diy_view.php'), 'utf8');
|
||||
assert.ok(!php.includes('"titleSizeNonExist"'), '探针字段不应存在(存在则本测试失效)');
|
||||
for (const comp of Object.keys(CONTRACT)) {
|
||||
const value = readValueJson(comp);
|
||||
assert.equal(value.titleSizeNonExist, undefined, comp + ' 不应有探针字段');
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user