Files
lucky_shop/components-diy/diy-card-common.js

238 lines
8.9 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%)';
}
/** 颜色压暗(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);
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',
// 方案1(2026-10-22): 邮箱图标 —— 需同时在 iconfont 字库补充信封字形 `icon-youxiang`
// (见 common/css/iconfont.css), 字库更新前该行会渲染空白, 故上线前必须配套更新字库。
// field 键名多写几套兜底(后端可能用 email / mail / 中文「邮箱」), 命中其一即可
email: 'icon-youxiang',
mail: 'icon-youxiang',
'邮箱': 'icon-youxiang',
// 网站/链接行(2026-10-22): 字库已有链条字形 icon-fuzhilianjie(\e03f), 直接复用
website: 'icon-fuzhilianjie',
link: 'icon-fuzhilianjie',
url: 'icon-fuzhilianjie',
'网站': 'icon-fuzhilianjie',
};
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'];
/** 位置档位中文别名(面板下拉若直接存中文档名, 归一化时兼容) */
const QR_POSITION_ALIAS = {
'资料行下方': 'rows',
'头部右侧': 'head',
'右上角浮层': 'corner',
'主按钮上方': 'bottom',
};
export function normalizeQrPosition(v) {
const s = String(v || '').trim();
if (QR_POSITIONS.indexOf(s) >= 0) return s;
return QR_POSITION_ALIAS[s] || 'rows';
}
/** 各档位的二维码边长(rpx): 头部右侧紧凑, 浮层带衬底, 行下/底部大图便于扫 */
/** 二维码默认尺寸(rpx); 面板「二维码尺寸」留空/0 = 按位自动取此档。
* rows/bottom(流内大码)240, head(头部右侧)200 —— 以 2026-10-22 用户参考图(C 端运行效果)定稿;
* corner(右上浮层, 占 Logo 位)空间受限保持 120 */
export const QR_SIZES = { rows: 240, head: 200, corner: 120, bottom: 240 };
/**
* 二维码外观样式(四档): plain 原图 / card 白卡描边(默认) / theme 主题色底 /
* gradient 渐变描边(码芯白底衬底, 保可扫)
* 多彩体现在装饰层(主题色/渐变), 码本身保持黑白 —— 可扫性优先
*/
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();
if (QR_STYLES.indexOf(s) >= 0) return s;
return QR_STYLE_ALIAS[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)';
}