Files
lucky_shop/common/js/card-share.js
ZF sun 5e6eec166a feat(card): 名片区块流与基础信息补齐(2.22/2.23) + 分享带名片ID
- 组件支持六个「可独立成块」模块任意排序(区块流): 资料行/快捷操作/社交/二维码/
  标签来源的位置可调, 头部恒最上、发名片恒最下; 二维码位置档位归并为 flow。
- 补齐「名片基础信息」遗漏字段: 简介区块、社交区块、名片附加信息(过期遮罩/
  地址细分/来源), 并与 wap 端展示口径对齐。
- 小程序分享给游客时携带名片 ID: 分享 path 追加 card_id, 访客打开「我的名片」
  即看到分享者那张卡。新增 common/js/card-share.js 统一处理「取卡ID / 拼参数」
  (dataset 通道 + storage 通道兜底, 朋友圈场景靠 storage)。
2026-10-28 16:37:12 +08:00

165 lines
5.6 KiB
JavaScript

/**
* 名片分享 · 跨端共享工具(小程序 / H5 / 鸿蒙通用)
*
* 背景(2026-10-28):
* 「我的名片」组件(components-diy/diy-card-self)的「发送名片」按钮在小程序端是
* <button open-type="share">; 微信只允许**页面级** onShareAppMessage 响应,
* 组件内无法自定义转发内容。而承载该组件的 DIY 页自带
* onShareAppMessage(common/js/diy.js) —— Vue2 的 defaultStrat(子覆盖父)
* 会让它顶掉全局兜底 mixin(common/js/mp-share-default.js)里那段
* dataset.cardId 分支, 导致"分享给游客时名片 ID 丢失"。
*
* 本模块提供三条通道(任一命中即可):
* ① dataset 通道: 页面钩子 onShareAppMessage(options) 里读
* options.target.dataset.cardId(微信会把 data-card-id 转成 dataset.cardId)
* —— 最准, 但只覆盖"发送给朋友"。
* ② storage 通道: 按钮 @tap 时把 card_id 写进 storage, 钩子读取后立即清除
* —— onShareTimeline 不接受事件参数, 只能走这条(朋友圈)。
* ③ 路由 query 通道: 分享落地页从当前页 options 取 card_id,
* 供「我的名片」组件回传给 /card/api/diy/mycard(访客看到分享者那张卡)。
*
* 纯 JS(无 wxss), 小程序/H5/鸿蒙通用; 无 uni 运行时环境(如 node 单测)全部静默降级。
*/
/** storage 槽: 记住"最近一次点分享按钮的卡片" */
export const SHARE_CARD_STORAGE_KEY = '_cardShareCardId';
/** storage 记忆的最长有效期(毫秒) —— 超过则认为已过期, 不再用于分享 */
export const SHARE_CARD_MEMORY_TTL = 5 * 60 * 1000;
/**
* 记住"正在分享的卡片"(按钮 @tap 时调用)
* @param {string|number} cardId
* @returns {string} 实际记住的 card_id(空串表示未记住)
*/
export function rememberShareCardId(cardId) {
const v = String(cardId == null ? '' : cardId).trim();
if (!v) return '';
try {
uni.setStorageSync(SHARE_CARD_STORAGE_KEY, { card_id: v, at: Date.now() });
} catch (e) {
/* 无 uni 运行时 / 存储失败: 静默, 退回 dataset 通道 */
}
return v;
}
/**
* 取出并清除记住的 card_id(钩子里调用)
* 读取后立即清除 —— 避免下一次"右上角···转发"复用了上一次按钮点击的卡片。
* @param {number} [maxAgeMs] 有效期上限, 默认 5 分钟
* @returns {string}
*/
export function takeRememberedShareCardId(maxAgeMs = SHARE_CARD_MEMORY_TTL) {
let v = '';
try {
const raw = uni.getStorageSync(SHARE_CARD_STORAGE_KEY);
if (raw && typeof raw === 'object' && raw.card_id) {
const at = Number(raw.at || 0);
if (!maxAgeMs || Date.now() - at <= maxAgeMs) {
v = String(raw.card_id).trim();
}
} else if (typeof raw === 'string' && raw) {
// 兼容早期纯字符串格式
v = raw.trim();
}
} catch (e) {
v = '';
}
try {
uni.removeStorageSync(SHARE_CARD_STORAGE_KEY);
} catch (e) {
/* ignore */
}
return v;
}
/**
* 从分享事件里挑出名片 ID(dataset 优先, storage 兜底)
* @param {object} [options] onShareAppMessage 的事件对象
* @returns {string}
*/
export function pickShareCardId(options) {
try {
const ds = options && options.target && options.target.dataset;
if (ds && ds.cardId) {
const v = String(ds.cardId).trim();
if (v) return v;
}
} catch (e) {
/* ignore */
}
return takeRememberedShareCardId();
}
/**
* 取当前页面路由 query 的某个值(分享落地页带 card_id 时用)
* 兼容三种取法: 原生 options / uni 封装的 $page.options / 字符串 query
* @param {string} key
* @returns {string}
*/
export function routeQueryValue(key) {
try {
const pages = typeof getCurrentPages === 'function' ? getCurrentPages() : [];
if (!pages.length) return '';
const cur = pages[pages.length - 1] || {};
let opts = cur.options || (cur.$page && cur.$page.options) || null;
if (opts && typeof opts === 'object') {
const v = opts[key];
return v == null ? '' : String(v).trim();
}
if (typeof cur.query === 'string' && cur.query) {
const m = cur.query.match(new RegExp('(?:^|&)' + key + '=([^&]*)'));
return m ? decodeURIComponent(m[1]) : '';
}
if (cur.query && typeof cur.query === 'object') {
const v = cur.query[key];
return v == null ? '' : String(v).trim();
}
} catch (e) {
/* ignore */
}
return '';
}
/**
* 给页面 path 追加一个 query 参数
* @param {string} path 形如 /pages_tool/index/diy?id=1
* @param {string} key
* @param {string} value
* @returns {string}
*/
export function appendPathParam(path, key, value) {
const p = String(path == null ? '' : path).trim();
const v = String(value == null ? '' : value).trim();
if (!v) return p;
if (!p) return '?' + encodeURIComponent(key) + '=' + encodeURIComponent(v);
const sep = p.indexOf('?') >= 0 ? '&' : '?';
return p + sep + encodeURIComponent(key) + '=' + encodeURIComponent(v);
}
/**
* 给 query 串追加一个参数(朋友圈 onShareTimeline 只认 query)
* @param {string} query 形如 id=1&name=xx
* @param {string} key
* @param {string} value
* @returns {string}
*/
export function appendQueryParam(query, key, value) {
const q = String(query == null ? '' : query).trim().replace(/^[?&]+/, '');
const v = String(value == null ? '' : value).trim();
if (!v) return q;
if (!q) return encodeURIComponent(key) + '=' + encodeURIComponent(v);
return q + '&' + encodeURIComponent(key) + '=' + encodeURIComponent(v);
}
/**
* 取 path 里的 query 段(不带 ? 前缀)
* @param {string} path
* @returns {string}
*/
export function pathQueryPart(path) {
const p = String(path == null ? '' : path);
const i = p.indexOf('?');
return i >= 0 ? p.slice(i + 1) : '';
}