Files
lucky_shop/components-diy/diy-card-common.js
ZF sun 805bbb9598 feat(diy-card): 4.0 头像开关 + 二维码四档外观/自定义尺寸/微调位移/提示开关
- 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 全绿
2026-10-21 12:28:18 +08:00

195 lines
6.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 名片 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)';
}