- common.js: QR_STYLES 四档枚举 + normalizeQrStyle(未知回落 card) + resolveQrSize(自定义 px×2>档位自动 rpx) + qrTransform(位移经 translate, WXSS 不支持 calc) - mini.vue 基件: 头像可隐藏(默认显示, 兼容历史配置); qrBlockStyle 统一四档外观(plain 原图/card 白卡/theme 主题色/gradient 渐变, 多彩只装饰外围衬底, 码黑白保可扫); cornerClearStyle 按实际尺寸 反算让位(自定义大尺寸/关 tip 自动跟随); 扫码提示可关 - show/self: 6 新字段透传; ab-visual 测试 corner 断言改锚动态让位 - 新增 qr-style 契约测试 20 项(纯函数真值 + 基件 + 透传 + 两个 design.html 面板控件绑定与预览消费 + 反例), 9 项变异验证全抓住 - 前端四测试文件共 66 项全绿; E2E 143 项; 三回归 79/41/56 全绿
195 lines
6.8 KiB
JavaScript
195 lines
6.8 KiB
JavaScript
/**
|
||
* 名片 DIY 组件 · 共享设计系统层
|
||
*
|
||
* 目标(UI Designer 设计系统先行原则):
|
||
* - 4 个组件共用同一套 token(颜色/字阶/间距/圆角/阴影/动效时长)
|
||
* - 通用 helpers(px2rpx/头像兜底色/显隐开关解析)集中一处, 避免组件间漂移
|
||
* - 严格遵循小程序规范: 禁 CSS 变量, 动态样式一律 $util.objToStyle, 单位 rpx
|
||
*
|
||
* 注: 本文件为纯 JS(无 wxss), 小程序/H5 通用。
|
||
*/
|
||
|
||
export const CardTokens = {
|
||
color: {
|
||
primary: '#2F6BFF', // 主题默认蓝(名片 wap 公开页同色系)
|
||
text: '#1F2329', // 主文字
|
||
textSub: '#8A8F99', // 次文字(公司/职位)
|
||
textLabel: '#B0B4BC', // 字段标签
|
||
line: '#EEF0F3', // 分割线
|
||
bg: '#FFFFFF', // 卡片底
|
||
skeleton: '#F2F3F5', // 骨架块
|
||
skeletonHi: '#E8EAEE', // 骨架高亮扫过
|
||
scrim: 'rgba(15, 23, 42, 0.45)', // 封面图遮罩(对比度保障)
|
||
},
|
||
radius: {
|
||
sm: 8,
|
||
md: 16,
|
||
lg: 24,
|
||
pill: 999,
|
||
},
|
||
/** 字阶(px, 输出时 ×2 转 rpx) */
|
||
fontSize: {
|
||
display: 40, // 入口主标题
|
||
title: 32, // 分组标题/名片姓名
|
||
name: 30,
|
||
meta: 24, // 公司/职位/字段值
|
||
caption: 22, // 辅助说明
|
||
},
|
||
/** 间距(px) */
|
||
space: {
|
||
xs: 4,
|
||
sm: 8,
|
||
md: 12,
|
||
lg: 16,
|
||
xl: 20,
|
||
xxl: 24,
|
||
},
|
||
/** 阴影 */
|
||
shadow: {
|
||
card: '0 4rpx 16rpx rgba(15, 23, 42, 0.06)',
|
||
raised: '0 8rpx 24rpx rgba(15, 23, 42, 0.10)',
|
||
},
|
||
/** 动效时长(ms) */
|
||
duration: {
|
||
fast: 150,
|
||
normal: 240,
|
||
slow: 320,
|
||
},
|
||
/** 最小触达尺寸(rpx, 小程序可点性下限 44px≈88rpx, 芯片取 72rpx 高度) */
|
||
touch: {
|
||
chipHeight: 72,
|
||
btnHeight: 80,
|
||
avatar: 104,
|
||
},
|
||
};
|
||
|
||
/** px → rpx(组件配置字段均为 px 口径) */
|
||
export function px2rpx(v) {
|
||
const n = Number(v);
|
||
return isNaN(n) ? 0 : Math.round(n * 2);
|
||
}
|
||
|
||
/**
|
||
* 显隐开关解析: 兼容 true/false、1/0、"1"/"0"、"true"/"false"
|
||
* 未下发(undefined)时返回 fallback(由调用方决定默认值)
|
||
*/
|
||
export function flagOn(v, fallback) {
|
||
if (v === undefined || v === null || v === '') return fallback;
|
||
if (v === true || v === 1 || v === '1' || v === 'true') return true;
|
||
if (v === false || v === 0 || v === '0' || v === 'false') return false;
|
||
return !!v;
|
||
}
|
||
|
||
/** 姓名首字兜底(头像无图时) */
|
||
export function initialOf(name) {
|
||
const s = String(name || '').trim();
|
||
return s ? s.charAt(0) : '名';
|
||
}
|
||
|
||
/** 由字符串确定性派生色相(同名同色, 列表视觉稳定) */
|
||
export function hueOf(str) {
|
||
const s = String(str || '');
|
||
let h = 0;
|
||
for (let i = 0; i < s.length; i++) {
|
||
h = (h * 31 + s.charCodeAt(i)) % 360;
|
||
}
|
||
return h;
|
||
}
|
||
|
||
/** 头像兜底底色: 主题色优先, 否则按名片确定性色相 */
|
||
export function avatarBg(card) {
|
||
const theme = card && card.theme_color;
|
||
if (theme) return theme;
|
||
const h = hueOf((card && (card.card_id || card.name)) || 'card');
|
||
return 'hsl(' + h + ', 62%, 55%)';
|
||
}
|
||
|
||
/** 主题渐变(头部饰条/主按钮): 有渐变末色用双色, 否则单色 */
|
||
export function themeGradient(card, angle) {
|
||
const theme = (card && card.theme_color) || CardTokens.color.primary;
|
||
const end = card && card.gradient_end;
|
||
const deg = (card && card.gradient_angle) || angle || 135;
|
||
return end
|
||
? 'linear-gradient(' + deg + 'deg, ' + theme + ' 0%, ' + end + ' 100%)'
|
||
: 'linear-gradient(' + deg + 'deg, ' + theme + ' 20%, ' + theme + ' 100%)';
|
||
}
|
||
|
||
/** 安全数字(配置字段) */
|
||
export function num(v, dft) {
|
||
const n = Number(v);
|
||
return isNaN(n) ? dft : n;
|
||
}
|
||
|
||
/** 字段行: 过滤空值, 限制条数(其余折叠) */
|
||
export function visibleRows(rows, max) {
|
||
const list = Array.isArray(rows) ? rows.filter(r => r && String(r.value || '').trim() !== '') : [];
|
||
if (max && max > 0 && list.length > max) {
|
||
return { shown: list.slice(0, max), rest: list.slice(max), hasMore: true };
|
||
}
|
||
return { shown: list, rest: [], hasMore: false };
|
||
}
|
||
|
||
/**
|
||
* 资料行图标(参考图: 电话/微信/地址带头像, 邮箱/网站/自定义无字形→退化为文字标签)
|
||
* 口径与 pages_tool/card/detail.vue 的 fieldRows 映射一致(同 glyph 同规则)
|
||
*/
|
||
const ROW_ICONS = {
|
||
mobile: 'icon-dianhua',
|
||
phone: 'icon-dianhua',
|
||
wechat: 'icon-weixin',
|
||
address: 'icon-dizhi',
|
||
};
|
||
export function rowIconFor(field) {
|
||
return ROW_ICONS[String(field || '')] || '';
|
||
}
|
||
|
||
/**
|
||
* 资料行点击动作: 由服务端下发的 href 派生(tel:/mailto:/url/空)
|
||
* tel: → 拨号 mailto: → 邮箱(H5) / 复制(其它端)
|
||
* http(s) → 复制链接 空 → 无动作(纯展示, 如地址/自定义)
|
||
* 返回 null 表示不可点; type 取值与父组件 doAct 的 tel/mail/copy 分支对齐
|
||
*/
|
||
export function rowActOf(row) {
|
||
const href = String((row && row.href) || '').trim();
|
||
const value = String((row && row.value) || '').trim();
|
||
if (!href || !value) return null;
|
||
if (/^tel:/i.test(href)) return { type: 'tel', value: value };
|
||
if (/^mailto:/i.test(href)) return { type: 'mail', value: value };
|
||
return { type: 'copy', value: value };
|
||
}
|
||
|
||
/** 二维码位置(四档): rows 资料行下方(默认) / head 头部右侧 / corner 右上角浮层 / bottom 主按钮上方 */
|
||
export const QR_POSITIONS = ['rows', 'head', 'corner', 'bottom'];
|
||
export function normalizeQrPosition(v) {
|
||
const s = String(v || '').trim();
|
||
return QR_POSITIONS.indexOf(s) >= 0 ? s : 'rows';
|
||
}
|
||
/** 各档位的二维码边长(rpx): 头部右侧紧凑, 浮层带衬底, 行下/底部大图便于扫 */
|
||
export const QR_SIZES = { rows: 180, head: 120, corner: 120, bottom: 180 };
|
||
|
||
/**
|
||
* 二维码外观样式(四档): plain 原图 / card 白卡描边(默认) / theme 主题色底 /
|
||
* gradient 渐变描边(码芯白底衬底, 保可扫)
|
||
* 多彩体现在装饰层(主题色/渐变), 码本身保持黑白 —— 可扫性优先
|
||
*/
|
||
export const QR_STYLES = ['plain', 'card', 'theme', 'gradient'];
|
||
export function normalizeQrStyle(v) {
|
||
const s = String(v || '').trim();
|
||
return QR_STYLES.indexOf(s) >= 0 ? s : 'card';
|
||
}
|
||
|
||
/** 二维码边长: 自定义 > 0 时用自定义(px), 否则按档位自动(rpx) */
|
||
export function resolveQrSize(position, custom) {
|
||
const n = Number(custom);
|
||
if (!isNaN(n) && n > 0) return n * 2;
|
||
return QR_SIZES[position] || QR_SIZES.rows;
|
||
}
|
||
|
||
/** 微调位移: 任一轴非零才输出 transform(WXSS 支持 translate; 不支持 calc) */
|
||
export function qrTransform(offsetX, offsetY) {
|
||
const x = Number(offsetX) || 0;
|
||
const y = Number(offsetY) || 0;
|
||
if (x === 0 && y === 0) return '';
|
||
return 'translate(' + (x * 2) + 'rpx, ' + (y * 2) + 'rpx)';
|
||
}
|