Files
lucky_shop/components-diy/diy-card-common.js
jing a08d0c2026 Merge remote-tracking branch 'origin/feat/camcard' into feat/camcard
# Conflicts:
#	components-diy/diy-card-mini.vue
#	components-diy/diy-card-show.vue
#	tests/diy-card-ab-visual.test.js
#	tests/diy-card-qr-style.test.js
2026-10-24 10:56:20 +08:00

498 lines
32 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 || '')] || '';
}
/**
* 资料行 CSS 描边图标(2.17.1): iconfont 无邮箱/网站字形, 同宫格口径
* 用纯 view 画信封/线框地球(与 ACT_GRID_ICONS.css 同一套图形, 尺寸缩小)
*/
const ROW_CSS_ICONS = {
email: 'mail',
website: 'web',
};
export function rowCssIconFor(field) {
return ROW_CSS_ICONS[String(field || '')] || '';
}
/**
* 资料行点击动作: 由服务端下发的 href 派生(tel:/mailto:/url/空)
* tel: → 拨号 mailto: → 邮箱(H5 调起 / 其它端复制引导)
* http(s) → web(跳转网站, 经 web-view 承载页) 空 → 无动作(纯展示, 如地址/自定义)
* 返回 null 表示不可点; type 取值与父组件 doAct 的 tel/mail/web/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 };
if (/^https?:\/\//i.test(href)) return { type: 'web', 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)';
}
/**
* 头部形态(2.17): plain 白卡头部(历史默认) / theme 主题渐变整块头部
* theme 档: 渐变铺头部 + 装饰圆 + 姓名副行白字(headNameColor/headSubColor 可覆盖)
*/
export const HEAD_STYLES = ['plain', 'theme'];
export function normalizeHeadStyle(v) {
const s = String(v || '').trim();
return HEAD_STYLES.indexOf(s) >= 0 ? s : 'plain';
}
/**
* 快捷操作布局(2.17): chips 文字胶囊(历史默认) / grid 图标宫格
* grid 档图标: 有字形用 iconfont, 无字形退化为动作首字(与头像首字同构)
* 注: 小程序端 iconfont 暂无邮件/网站字形(iconfont.css 无 youxiang/wangzhan),
* 扩展字形后补此表即可, 无需改组件
*/
export const ACTS_LAYOUTS = ['chips', 'grid'];
export function normalizeActsLayout(v) {
const s = String(v || '').trim();
return ACTS_LAYOUTS.indexOf(s) >= 0 ? s : 'chips';
}
/**
* 宫格图标(2.18.1): 与 wap 公开页 detail 快捷操作同源的 SVG 线稿
* (路径逐一取自 wap/view/index/detail.html quickGrid, 保证两端观感一致)
* mode=stroke: 描边线稿(颜色动态跟随配色档/每按钮定制)
* mode=fill: 实心填充(wx 微信绿 / save 通讯录橙 —— detail 同款固定色)
* 渲染: svgLineIconBg 生成 data-URI 作为 background-image(H5+小程序通用,
* WXSS 对 base64 data URI 的 background-image 原生支持)
*/
export const WAP_LINE_ICONS = {
tel: {
mode: 'stroke',
body: '<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z"/>',
},
wx: {
mode: 'fill',
color: '#07C160',
body: '<path d="M8.69 4C4.98 4 2 6.46 2 9.5c0 1.77 1.05 3.35 2.68 4.4l-.67 2.02 2.35-1.18c.75.2 1.53.31 2.33.31h.28a5.42 5.42 0 0 1-.22-1.55c0-3.02 2.94-5.46 6.56-5.46h.28C15.03 5.9 12.14 4 8.69 4zM6.28 8.05a.94.94 0 1 1 0-1.88.94.94 0 0 1 0 1.88zm4.82 0a.94.94 0 1 1 0-1.88.94.94 0 0 1 0 1.88zM22 13.5c0-2.6-2.6-4.7-5.81-4.7s-5.81 2.1-5.81 4.7 2.6 4.7 5.81 4.7c.68 0 1.33-.09 1.94-.26l1.97 1-.57-1.7C20.99 16.36 22 15.02 22 13.5zm-7.55-.94a.8.8 0 1 1 0-1.6.8.8 0 0 1 0 1.6zm3.48 0a.8.8 0 1 1 0-1.6.8.8 0 0 1 0 1.6z"/>',
},
mail: {
mode: 'stroke',
body: '<rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>',
},
web: {
mode: 'stroke',
body: '<circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>',
},
save: {
mode: 'stroke',
color: '#FF9500',
body: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M19 8v6M22 11h-6"/>',
},
};
/** 纯 ASCII base64(SVG 路径/颜色均为 ASCII, 小程序无 btoa 自带实现) */
const B64T = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
function b64ascii(s) {
let out = '';
for (let i = 0; i < s.length; i += 3) {
const a = s.charCodeAt(i);
const b = s.charCodeAt(i + 1);
const c = s.charCodeAt(i + 2);
out += B64T[a >> 2]
+ B64T[((a & 3) << 4) | ((isNaN(b) ? 0 : b) >> 4)]
+ (isNaN(b) ? '=' : B64T[((b & 15) << 2) | ((isNaN(c) ? 0 : c) >> 6)])
+ (isNaN(c) ? '=' : B64T[c & 63]);
}
return out;
}
/**
* 宫格 SVG 图标 → background-image 值(data URI, 颜色内嵌)
* color 仅对 stroke 模式生效; fill 模式用 WAP_LINE_ICONS 内置固定色
*/
export function svgLineIconBg(type, color) {
const ico = WAP_LINE_ICONS[String(type || '')];
if (!ico) return '';
const attrs = ico.mode === 'fill'
? 'fill="' + (ico.color || '#07C160') + '"'
: 'fill="none" stroke="' + (color || '#2F6BFF') + '" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" ' + attrs + '>' + ico.body + '</svg>';
return 'url("data:image/svg+xml;base64,' + b64ascii(svg) + '")';
}
/**
* 宫格配色(2.17): preset 多彩预设(参考图: 浅底+深字五色) / theme 主题色
* theme 档浅底由 hexToRgba(theme, 0.12) 动态派生 —— WXSS 无 color-mix, JS 端换算
*/
export const ACT_GRID_PRESETS = {
tel: { bg: '#E1F5EE', fg: '#0F6E56' },
wx: { bg: '#EAF3DE', fg: '#3B6D11' },
mail: { bg: '#E6F1FB', fg: '#185FA5' },
web: { bg: '#FAEEDA', fg: '#854F0B' },
save: { bg: '#F0E9FB', fg: '#5B3DA8' },
};
export const ACT_GRID_PRESET_FALLBACK = { bg: '#F2F3F5', fg: '#5F5E5A' };
export function normalizeActGridColorMode(v) {
const s = String(v || '').trim();
return s === 'theme' ? 'theme' : 'preset';
}
/**
* 快捷操作区位置(2.18): below 资料区下方(历史默认) / above 资料区上方
* 历史配置无此字段时回落 below —— 渲染零变化
*/
export const ACTS_POSITIONS = ['below', 'above'];
export function normalizeActsPosition(v) {
const s = String(v || '').trim();
return ACTS_POSITIONS.indexOf(s) >= 0 ? s : 'below';
}
/**
* 每按钮图标配置(2.18): actIcons = { type: { img, color, bg } }
* img 自定义图标图片(媒体库, 空=自动档: iconfont 字形 / CSS 描边图标)
* color 图标前景色(空=跟随 preset/theme 配色档)
* bg 图标浅底色(空=跟随 preset/theme 配色档)
* 仅 grid 宫格档消费; 任意字段留空即回落自动档(历史形态零变化)
*/
export function actIconCfgOf(actIcons, type) {
const cfg = actIcons && actIcons[String(type || '')];
if (!cfg || typeof cfg !== 'object') return {};
return cfg;
}
/** hex(#RRGGBB / #RGB) → rgba 字符串(宫格主题浅底/复制按钮浅底用); 非法入参返回 null */
export function hexToRgba(hex, alpha) {
const s = String(hex || '').trim().replace(/^#/, '');
let full = '';
if (/^[0-9a-fA-F]{6}$/.test(s)) {
full = s;
} else if (/^[0-9a-fA-F]{3}$/.test(s)) {
full = s[0] + s[0] + s[1] + s[1] + s[2] + s[2];
} else {
return null;
}
const n = parseInt(full, 16);
return 'rgba(' + ((n >> 16) & 255) + ',' + ((n >> 8) & 255) + ',' + (n & 255) + ',' + alpha + ')';
}
/* ------------------------------------------------------------------
* 2.19 快捷操作: 显示控制 + 文案改名 + 自定义动作 + 素材图标库
* ------------------------------------------------------------------ */
/** 默认动作类型序(图层面板行序与 C 端合并序一致) */
export const ACT_TYPES = ['tel', 'wx', 'mail', 'web', 'save'];
/** 自定义动作类型白名单(doAct 天然消费: tel/wx/mail/copy 分支 + else 兜底) */
export const ACT_ACTION_TYPES = ['tel', 'wx', 'mail', 'web', 'copy'];
export const ACT_LABEL_DEFAULTS = {
tel: '打电话',
wx: '微信号',
mail: '发邮件',
web: '网站',
save: '存通讯录',
};
/** 默认动作次序(2.19.1): 历史渲染顺序 = 后端 quickActs 下发顺序 */
export const ACT_ORDER_DEFAULT = ['tel', 'wx', 'mail', 'web', 'save'];
/**
* 默认动作次序(2.19.1): actOrder = [type...] —— 只收白名单内类型并去重,
* 缺失类型按默认顺序尾补(保证 5 类型齐全), 非数组/全非法 = 历史顺序
*/
export function normalizeActOrder(order) {
const out = [];
(Array.isArray(order) ? order : []).forEach(k => {
const t = String(k || '');
if (ACT_TYPES.indexOf(t) >= 0 && out.indexOf(t) < 0) out.push(t);
});
ACT_ORDER_DEFAULT.forEach(t => {
if (out.indexOf(t) < 0) out.push(t);
});
return out;
}
/**
* 默认动作显隐(2.19): actVis = { type: 0|1 }, 缺键/非法 = 显示(1)
* 历史配置无此字段 → 全 1, 渲染零变化
*/
export function normalizeActVis(vis) {
const src = vis && typeof vis === 'object' ? vis : {};
const out = {};
ACT_TYPES.forEach(t => {
out[t] = String(src[t]) === '0' ? 0 : 1;
});
return out;
}
/**
* 默认动作文案(2.19): actLabels = { type: string }, 空/缺 = 内置默认
* 历史配置无此字段 → 全默认, 渲染零变化
*/
export function normalizeActLabels(labels) {
const src = labels && typeof labels === 'object' ? labels : {};
const out = {};
ACT_TYPES.forEach(t => {
const s = String(src[t] || '').trim();
out[t] = s || ACT_LABEL_DEFAULTS[t];
});
return out;
}
/**
* 自定义动作列表(2.19): customActs = [{ label, type, value, iconKey, img, color, bg, vis }]
* 白名单过滤 + 字段收敛(防御脏配置); 非数组/空 → [] (历史形态零变化)
*/
export function normalizeCustomActs(list) {
if (!Array.isArray(list)) return [];
return list.slice(0, 10).map((raw, i) => {
const it = raw && typeof raw === 'object' ? raw : {};
const type = ACT_ACTION_TYPES.indexOf(String(it.type || '')) >= 0 ? String(it.type) : 'copy';
return {
label: String(it.label || '').trim().slice(0, 6) || '自定义',
type: type,
value: String(it.value || '').trim().slice(0, 200),
iconKey: String(it.iconKey || '').trim(),
img: String(it.img || '').trim(),
color: String(it.color || '').trim(),
bg: String(it.bg || '').trim(),
vis: String(it.vis) === '0' ? 0 : 1,
custom: 1,
_i: i,
};
}).filter(it => it.label !== '自定义' || it.value || it.img || it.iconKey);
}
/**
* 2.19 素材图标库(C 端): 与后端 addon\card\service\CardIconLibrary 同源,
* 由 gen 脚本从 PHP 常量逐字节生成, 契约测试锁定同步。
* key 图标渲染优先级: img(媒体库) > iconKey(本库, 实时配色) > WAP_LINE_ICONS > 首字
*/
export const CARD_ICON_LIB = {"phone":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M6.62 10.79c1.44 2.83 3.76 5.15 6.59 6.59l2.2-2.2c.28-.28.67-.36 1.02-.25c1.12.37 2.32.57 3.57.57a1 1 0 0 1 1 1V20a1 1 0 0 1-1 1A17 17 0 0 1 3 4a1 1 0 0 1 1-1h3.5a1 1 0 0 1 1 1c0 1.25.2 2.45.57 3.57c.11.35.03.74-.25 1.02z\"/></svg>","sms":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M20 2a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6l-4 4V4a2 2 0 0 1 2-2zM4 4v13.17L5.17 16H20V4zm2 3h12v2H6zm0 4h9v2H6z\"/></svg>","email":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M22 6c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2zm-2 0l-8 5l-8-5zm0 12H4V8l8 5l8-5z\"/></svg>","wechat":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M9.5 4C5.36 4 2 6.69 2 10c0 1.89 1.08 3.56 2.78 4.66L4 17l2.5-1.5c.89.31 1.87.5 2.91.5A5.2 5.2 0 0 1 9 14c0-3.31 3.13-6 7-6c.19 0 .38 0 .56.03C15.54 5.69 12.78 4 9.5 4m-3 2.5a1 1 0 0 1 1 1a1 1 0 0 1-1 1a1 1 0 0 1-1-1a1 1 0 0 1 1-1m5 0a1 1 0 0 1 1 1a1 1 0 0 1-1 1a1 1 0 0 1-1-1a1 1 0 0 1 1-1M16 9c-3.31 0-6 2.24-6 5s2.69 5 6 5c.67 0 1.31-.08 1.91-.25L20 20l-.62-1.87C20.95 17.22 22 15.71 22 14c0-2.76-2.69-5-6-5m-2 2.5a1 1 0 0 1 1 1a1 1 0 0 1-1 1a1 1 0 0 1-1-1a1 1 0 0 1 1-1m4 0a1 1 0 0 1 1 1a1 1 0 0 1-1 1a1 1 0 0 1-1-1a1 1 0 0 1 1-1\"/></svg>","whatsapp":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M12.04 2c-5.46 0-9.91 4.45-9.91 9.91c0 1.75.46 3.45 1.32 4.95L2.05 22l5.25-1.38c1.45.79 3.08 1.21 4.74 1.21c5.46 0 9.91-4.45 9.91-9.91c0-2.65-1.03-5.14-2.9-7.01A9.82 9.82 0 0 0 12.04 2m.01 1.67c2.2 0 4.26.86 5.82 2.42a8.23 8.23 0 0 1 2.41 5.83c0 4.54-3.7 8.23-8.24 8.23c-1.48 0-2.93-.39-4.19-1.15l-.3-.17l-3.12.82l.83-3.04l-.2-.32a8.2 8.2 0 0 1-1.26-4.38c.01-4.54 3.7-8.24 8.25-8.24M8.53 7.33c-.16 0-.43.06-.66.31c-.22.25-.87.86-.87 2.07c0 1.22.89 2.39 1 2.56c.14.17 1.76 2.67 4.25 3.73c.59.27 1.05.42 1.41.53c.59.19 1.13.16 1.56.1c.48-.07 1.46-.6 1.67-1.18s.21-1.07.15-1.18c-.07-.1-.23-.16-.48-.27c-.25-.14-1.47-.74-1.69-.82c-.23-.08-.37-.12-.56.12c-.16.25-.64.81-.78.97c-.15.17-.29.19-.53.07c-.26-.13-1.06-.39-2-1.23c-.74-.66-1.23-1.47-1.38-1.72c-.12-.24-.01-.39.11-.5c.11-.11.27-.29.37-.44c.13-.14.17-.25.25-.41c.08-.17.04-.31-.02-.43c-.06-.11-.56-1.35-.77-1.84c-.2-.48-.4-.42-.56-.43c-.14 0-.3-.01-.47-.01\"/></svg>","linkedin":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M19 3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2zm-.5 15.5v-5.3a3.26 3.26 0 0 0-3.26-3.26c-.85 0-1.84.52-2.32 1.3v-1.11h-2.79v8.37h2.79v-4.93c0-.77.62-1.4 1.39-1.4a1.4 1.4 0 0 1 1.4 1.4v4.93zM6.88 8.56a1.68 1.68 0 0 0 1.68-1.68c0-.93-.75-1.69-1.68-1.69a1.69 1.69 0 0 0-1.69 1.69c0 .93.76 1.68 1.69 1.68m1.39 9.94v-8.37H5.5v8.37z\"/></svg>","weibo":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M9.82 13.87c1.07 0 1.95.87 1.95 1.95a1.95 1.95 0 0 1-1.95 1.95c-1.08 0-1.95-.88-1.95-1.95c0-1.08.87-1.95 1.95-1.95M14.5 3.34l.68-.03c3.76 0 6.82 3.06 6.82 6.82l-.05.82l-1.19-.37l.02-.45c0-3.09-2.51-5.6-5.6-5.6l-.35.01zm.82 2.89c2.06.07 3.73 1.77 3.76 3.83l-1.24-.38c-.19-1.12-1.06-2-2.17-2.18zM2 15.41c-.03-.61.07-2.77 2.95-5.44c3.4-3.16 4.87-2.92 4.87-2.92s3.18-.3 1.24 3.41h.07c.47-.5 1.49-1.25 3.56-1.46c2.08-.21 2.08 1.5 1.81 2.7c1.88.94 3.06 2.33 3.06 3.88c0 2.82-3.93 5.11-8.78 5.11h-.28c-3.5 0-6.5-1.27-7.79-3.1c-.46-.62-.71-1.3-.71-2.01zm7.82-3.49c-3.23 0-5.85 1.75-5.85 3.9s2.62 3.9 5.85 3.9s5.85-1.75 5.85-3.9s-2.62-3.9-5.85-3.9\"/></svg>","shortvideo":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M12 3v10.55A4 4 0 1 0 14 17V7h4V3m-8 16a2 2 0 1 1 2-2a2 2 0 0 1-2 2\"/></svg>","web":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M17.9 17.39c-.26-.8-1.01-1.39-1.9-1.39h-1v-3a1 1 0 0 0-1-1H8v-2h2a1 1 0 0 0 1-1V7h2a2 2 0 0 0 2-2v-.41a7.984 7.984 0 0 1 2.9 12.8M11 19.93c-3.95-.49-7-3.85-7-7.93c0-.62.08-1.22.21-1.79L9 15v1a2 2 0 0 0 2 2m1-16A10 10 0 0 0 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2\"/></svg>","location":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M12 11.5A2.5 2.5 0 0 1 9.5 9A2.5 2.5 0 0 1 12 6.5A2.5 2.5 0 0 1 14.5 9a2.5 2.5 0 0 1-2.5 2.5M12 2a7 7 0 0 0-7 7c0 5.25 7 13 7 13s7-7.75 7-13a7 7 0 0 0-7-7\"/></svg>","qrcode":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M3 11h2v2H3zm8-6h2v4h-2zm-2 6h4v4h-2v-2H9zm6 0h2v2h2v-2h2v2h-2v2h2v4h-2v2h-2v-2h-4v2h-2v-4h4v-2h2v-2h-2zm4 8v-4h-2v4zM15 3h6v6h-6zm2 2v2h2V5zM3 3h6v6H3zm2 2v2h2V5zM3 15h6v6H3zm2 2v2h2v-2z\"/></svg>","account":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M19 19H5V5h14m0-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2m-2.5 13.25c0-1.5-3-2.25-4.5-2.25s-4.5.75-4.5 2.25V17h9M12 12.25A2.25 2.25 0 0 0 14.25 10A2.25 2.25 0 0 0 12 7.75A2.25 2.25 0 0 0 9.75 10A2.25 2.25 0 0 0 12 12.25\"/></svg>","company":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M5 3v18h6v-3.5h2V21h6V3zm2 2h2v2H7zm4 0h2v2h-2zm4 0h2v2h-2zM7 9h2v2H7zm4 0h2v2h-2zm4 0h2v2h-2zm-8 4h2v2H7zm4 0h2v2h-2zm4 0h2v2h-2zm-8 4h2v2H7zm8 0h2v2h-2z\"/></svg>","job":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M20 6c.58 0 1.05.2 1.42.59c.38.41.58.86.58 1.41v11c0 .55-.2 1-.58 1.41c-.37.39-.84.59-1.42.59H4c-.58 0-1.05-.2-1.42-.59C2.2 20 2 19.55 2 19V8c0-.55.2-1 .58-1.41C2.95 6.2 3.42 6 4 6h4V4c0-.58.2-1.05.58-1.42C8.95 2.2 9.42 2 10 2h4c.58 0 1.05.2 1.42.58c.38.37.58.84.58 1.42v2zM4 8v11h16V8zm10-2V4h-4v2z\"/></svg>","handshake":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M21.71 8.71c1.25-1.25.68-2.71 0-3.42l-3-3c-1.26-1.25-2.71-.68-3.42 0L13.59 4H11C9.1 4 8 5 7.44 6.15L3 10.59v4l-.71.7c-1.25 1.26-.68 2.71 0 3.42l3 3c.54.54 1.12.74 1.67.74c.71 0 1.36-.35 1.75-.74l2.7-2.71H15c1.7 0 2.56-1.06 2.87-2.1c1.13-.3 1.75-1.16 2-2C21.42 14.5 22 13.03 22 12V9h-.59zM20 12c0 .45-.19 1-1 1h-1v1c0 .45-.19 1-1 1h-1v1c0 .45-.19 1-1 1h-4.41l-3.28 3.28c-.31.29-.49.12-.6.01l-2.99-2.98c-.29-.31-.12-.49-.01-.6L5 15.41v-4l2-2V11c0 1.21.8 3 3 3s3-1.79 3-3h7zm.29-4.71L18.59 9H11v2c0 .45-.19 1-1 1s-1-.55-1-1V8c0-.46.17-2 2-2h3.41l2.28-2.28c.31-.29.49-.12.6-.01l2.99 2.98c.29.31.12.49.01.6\"/></svg>","chart":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"m16 11.78l4.24-7.33l1.73 1l-5.23 9.05l-6.51-3.75L5.46 19H22v2H2V3h2v14.54L9.5 8z\"/></svg>","store":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"m18.36 9l.6 3H5.04l.6-3zM20 4H4v2h16zm0 3H4l-1 5v2h1v6h10v-6h4v6h2v-6h1v-2zM6 18v-4h6v4z\"/></svg>","calendar":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M19 19H5V8h14m0-5h-1V1h-2v2H8V1H6v2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2m-2.47 8.06L15.47 10l-4.88 4.88l-2.12-2.12l-1.06 1.06L10.59 17z\"/></svg>","document":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M6 2a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zm0 2h7v5h5v11H6zm2 8v2h8v-2zm0 4v2h5v-2z\"/></svg>","shield":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M21 11c0 5.55-3.84 10.74-9 12c-5.16-1.26-9-6.45-9-12V5l9-4l9 4zm-9 10c3.75-1 7-5.46 7-9.78V6.3l-7-3.12L5 6.3v4.92C5 15.54 8.25 20 12 21m-2-4l-4-4l1.41-1.41L10 14.17l6.59-6.59L18 9\"/></svg>","image":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M19 19H5V5h14m0-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2m-5.04 9.29l-2.75 3.54l-1.96-2.36L6.5 17h11z\"/></svg>","video":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M15 8v8H5V8zm1-2H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-3.5l4 4v-11l-4 4V7a1 1 0 0 0-1-1\"/></svg>","play":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M12 20c-4.41 0-8-3.59-8-8s3.59-8 8-8s8 3.59 8 8s-3.59 8-8 8m0-18A10 10 0 0 0 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2m-2 14.5l6-4.5l-6-4.5z\"/></svg>","camera":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M20 4h-3.17L15 2H9L7.17 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2m0 14H4V6h4.05l1.83-2h4.24l1.83 2H20zM12 7a5 5 0 0 0-5 5a5 5 0 0 0 5 5a5 5 0 0 0 5-5a5 5 0 0 0-5-5m0 8a3 3 0 0 1-3-3a3 3 0 0 1 3-3a3 3 0 0 1 3 3a3 3 0 0 1-3 3\"/></svg>","music":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M21 3v12.5a3.5 3.5 0 0 1-3.5 3.5a3.5 3.5 0 0 1-3.5-3.5a3.5 3.5 0 0 1 3.5-3.5c.54 0 1.05.12 1.5.34V6.47L9 8.6v8.9A3.5 3.5 0 0 1 5.5 21A3.5 3.5 0 0 1 2 17.5A3.5 3.5 0 0 1 5.5 14c.54 0 1.05.12 1.5.34V6z\"/></svg>","star":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"m12 15.39l-3.76 2.27l.99-4.28l-3.32-2.88l4.38-.37L12 6.09l1.71 4.04l4.38.37l-3.32 2.88l.99 4.28M22 9.24l-7.19-.61L12 2L9.19 8.63L2 9.24l5.45 4.73L5.82 21L12 17.27L18.18 21l-1.64-7.03z\"/></svg>","heart":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"m12.1 18.55l-.1.1l-.11-.1C7.14 14.24 4 11.39 4 8.5C4 6.5 5.5 5 7.5 5c1.54 0 3.04 1 3.57 2.36h1.86C13.46 6 14.96 5 16.5 5c2 0 3.5 1.5 3.5 3.5c0 2.89-3.14 5.74-7.9 10.05M16.5 3c-1.74 0-3.41.81-4.5 2.08C10.91 3.81 9.24 3 7.5 3C4.42 3 2 5.41 2 8.5c0 3.77 3.4 6.86 8.55 11.53L12 21.35l1.45-1.32C18.6 15.36 22 12.27 22 8.5C22 5.41 19.58 3 16.5 3\"/></svg>","gift":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M22 12v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-8a1 1 0 0 1-1-1V8a2 2 0 0 1 2-2h3.17A3 3 0 0 1 6 5a3 3 0 0 1 3-3c1 0 1.88.5 2.43 1.24v-.01L12 4l.57-.77v.01C13.12 2.5 14 2 15 2a3 3 0 0 1 3 3a3 3 0 0 1-.17 1H21a2 2 0 0 1 2 2v3a1 1 0 0 1-1 1M4 20h7v-8H4zm16 0v-8h-7v8zM9 4a1 1 0 0 0-1 1a1 1 0 0 0 1 1a1 1 0 0 0 1-1a1 1 0 0 0-1-1m6 0a1 1 0 0 0-1 1a1 1 0 0 0 1 1a1 1 0 0 0 1-1a1 1 0 0 0-1-1M3 8v2h8V8zm10 0v2h8V8z\"/></svg>","pay":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M20 8H4V6h16m0 12H4v-6h16m0-8H4c-1.11 0-2 .89-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2\"/></svg>","truck":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M18 18.5c.83 0 1.5-.67 1.5-1.5s-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5s.67 1.5 1.5 1.5m1.5-9H17V12h4.46zM6 18.5c.83 0 1.5-.67 1.5-1.5s-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5s.67 1.5 1.5 1.5M20 8l3 4v5h-2c0 1.66-1.34 3-3 3s-3-1.34-3-3H9c0 1.66-1.34 3-3 3s-3-1.34-3-3H1V6c0-1.11.89-2 2-2h14v4zM3 6v9h.76c.55-.61 1.35-1 2.24-1s1.69.39 2.24 1H15V6z\"/></svg>","book":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M21 4H3a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2M3 19V6h8v13zm18 0h-8V6h8zm-7-9.5h6V11h-6zm0 2.5h6v1.5h-6zm0 2.5h6V16h-6z\"/></svg>","share":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81c1.66 0 3-1.34 3-3s-1.34-3-3-3s-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.15c-.05.21-.08.43-.08.66c0 1.61 1.31 2.91 2.92 2.91s2.92-1.3 2.92-2.91s-1.31-2.92-2.92-2.92M18 4c.55 0 1 .45 1 1s-.45 1-1 1s-1-.45-1-1s.45-1 1-1M6 13c-.55 0-1-.45-1-1s.45-1 1-1s1 .45 1 1s-.45 1-1 1m12 7c-.55 0-1-.45-1-1s.45-1 1-1s1 .45 1 1s-.45 1-1 1\"/></svg>","settings":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M12 8a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 2a2 2 0 0 0-2 2a2 2 0 0 0 2 2a2 2 0 0 0 2-2a2 2 0 0 0-2-2m-2 12c-.25 0-.46-.18-.5-.42l-.37-2.65c-.63-.25-1.17-.59-1.69-.99l-2.49 1.01c-.22.08-.49 0-.61-.22l-2-3.46a.493.493 0 0 1 .12-.64l2.11-1.66L4.5 12l.07-1l-2.11-1.63a.493.493 0 0 1-.12-.64l2-3.46c.12-.22.39-.31.61-.22l2.49 1c.52-.39 1.06-.73 1.69-.98l.37-2.65c.04-.24.25-.42.5-.42h4c.25 0 .46.18.5.42l.37 2.65c.63.25 1.17.59 1.69.98l2.49-1c.22-.09.49 0 .61.22l2 3.46c.13.22.07.49-.12.64L19.43 11l.07 1l-.07 1l2.11 1.63c.19.15.25.42.12.64l-2 3.46c-.12.22-.39.31-.61.22l-2.49-1c-.52.39-1.06.73-1.69.98l-.37 2.65c-.04.24-.25.42-.5.42zm1.25-18l-.37 2.61c-1.2.25-2.26.89-3.03 1.78L5.44 7.35l-.75 1.3L6.8 10.2a5.55 5.55 0 0 0 0 3.6l-2.12 1.56l.75 1.3l2.43-1.04c.77.88 1.82 1.52 3.01 1.76l.37 2.62h1.52l.37-2.61c1.19-.25 2.24-.89 3.01-1.77l2.43 1.04l.75-1.3l-2.12-1.55c.4-1.17.4-2.44 0-3.61l2.11-1.55l-.75-1.3l-2.41 1.04a5.42 5.42 0 0 0-3.03-1.77L12.75 4z\"/></svg>"};
/** 素材库图标 SVG 字符串(缺 key 返回空串) */
export function libIconSvg(key) {
return CARD_ICON_LIB[String(key || '')] || '';
}
/** 素材库图标 → data URI background-image 值(小程序无 btoa, 复用 b64ascii) */
export function libIconBg(key, color) {
const svg = libIconSvg(key);
if (!svg) return '';
return 'url("data:image/svg+xml;base64,' + b64ascii(svg.split('currentColor').join(color || '#2F6BFF')) + '")';
}