Merge branch 'feat/camcard' into dev/1.0
This commit is contained in:
177
common/js/card-share.js
Normal file
177
common/js/card-share.js
Normal file
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* 名片分享 · 跨端共享工具(小程序 / 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(访客看到分享者那张卡);
|
||||
* 同时作为 pickShareCardId 的第三级兜底(落地页上右上角「···」转发的续传)。
|
||||
*
|
||||
* 纯 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 兜底, 路由 query 再兜底)
|
||||
*
|
||||
* 2026-10-28 新增第三级: 路由 query 续传。
|
||||
* 背景: 右上角「···」转发时微信不传事件对象, storage 通道又只在"点过分享按钮"
|
||||
* 后 5 分钟内有效 —— 访客经分享落地(query 带 card_id)后直接右上角转发,
|
||||
* 原先两路都取不到 → 分享出去不带 card_id → 源头卡断链变官方卡。
|
||||
* 落地页本身带着 card_id, 把它作为第三级兜底即可闭合"接收方续传"链:
|
||||
* 任何落地页上发生的转发(按钮/右上角/朋友圈)都自动续传当前落地的卡。
|
||||
*
|
||||
* @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 */
|
||||
}
|
||||
const remembered = takeRememberedShareCardId();
|
||||
if (remembered) return remembered;
|
||||
// 落地续传: 分享落地页 query 带 card_id → 右上角「···」转发/超时后的转发也绑源
|
||||
// (裸 Tab 路由无 card_id → 返回空串, 分享行为与旧行为一致, 官方卡兜底不受影响)
|
||||
return routeQueryValue('card_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 取当前页面路由 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) : '';
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import Config from '@/common/js/config.js';
|
||||
import util from '@/common/js/util.js';
|
||||
import { classifyHttpError, httpErrorKey } from '@/common/js/http-error.js';
|
||||
import { consumeNeedStartupAd, isColdLaunchPending } from '@/common/js/startup-ad.js';
|
||||
import { pickShareCardId, appendPathParam, appendQueryParam, pathQueryPart, routeQueryValue } from '@/common/js/card-share.js';
|
||||
|
||||
let systemInfo = util.getDeviceInfo();
|
||||
|
||||
@@ -797,6 +798,11 @@ export default {
|
||||
let shareUrl = this.$config.h5Domain + this.diyRoute;
|
||||
if (this.id) shareUrl += '?id=' + this.id;
|
||||
else if (this.name) shareUrl += '?name=' + this.name;
|
||||
// 2026-10-28: H5 分享链接补 card_id —— 与小程序 path 口径对齐。
|
||||
// 落地页 query 带 card_id(员工/访客从分享进入)时, 公众号转发链接续传源头卡;
|
||||
// 裸 Tab 无 card_id → 行为零变化(官方卡兜底)。
|
||||
const shareCardId = routeQueryValue('card_id');
|
||||
if (shareCardId) shareUrl = appendPathParam(shareUrl, 'card_id', shareCardId);
|
||||
this.$util.setPublicShare({
|
||||
title: this.$langConfig.getField(this.diyData.global, 'title'),
|
||||
desc: '',
|
||||
@@ -823,11 +829,50 @@ export default {
|
||||
}, 50);
|
||||
},
|
||||
// 分享给好友
|
||||
onShareAppMessage() {
|
||||
//
|
||||
// 2026-10-28 名片分享带卡 ID:
|
||||
// 名片组件(「我的名片」diy-card-self / 精选名片)的「发送名片」按钮在小程序端是
|
||||
// <button open-type="share">, 微信只认**页面级**钩子。本钩子来自本 mixin,
|
||||
// Vue2 defaultStrat(子覆盖父)会顶掉全局兜底 mixin(mp-share-default.js)里的
|
||||
// dataset.cardId 分支 —— 所以在这里把名片 ID 重新接回来:
|
||||
// 拿到 card_id → 分享 path 追加 card_id → 访客落地即可看到分享者那张卡。
|
||||
// 拿不到(右上角「···」转发等无按钮上下文的场景)→ 回落原有 mpShareData。
|
||||
onShareAppMessage(options) {
|
||||
const cardId = pickShareCardId(options);
|
||||
if (cardId) {
|
||||
const base = this.mpShareData?.appMessage || {};
|
||||
// 分享 path 优先用**当前页** path(带装修模板 id 等 query) ——
|
||||
// 后台配置的 base.path 未必是用户当前所在的那一页;
|
||||
// 拿不到当前页时(极少见)才回落 base.path。
|
||||
const target = this.$util.getCurrentRoute().path || base.path || '';
|
||||
return {
|
||||
title: base.title || '小程序分享',
|
||||
path: appendPathParam(target, 'card_id', cardId),
|
||||
imageUrl: base.imageUrl || ''
|
||||
};
|
||||
}
|
||||
return this.mpShareData?.appMessage;
|
||||
},
|
||||
// 分享到朋友圈
|
||||
//
|
||||
// 朋友圈只支持 query(且 onShareTimeline **不接受事件参数**) → 拿不到 dataset,
|
||||
// 只能走 storage 通道(按钮 @tap 时记住 card_id)。
|
||||
// 注意: util.getMpShare() 塞进 timeLine.query 的其实是**完整 path**(不是 query 段),
|
||||
// 这里统一用 pathQueryPart 取真正的 query 段再拼 card_id。
|
||||
onShareTimeline() {
|
||||
return this.mpShareData?.timeLine;
|
||||
const base = this.mpShareData?.timeLine;
|
||||
const cardId = pickShareCardId(null);
|
||||
if (!cardId) {
|
||||
return base;
|
||||
}
|
||||
let q = pathQueryPart((base && base.query) || '');
|
||||
if (!q) {
|
||||
q = pathQueryPart(this.$util.getCurrentRoute().path);
|
||||
}
|
||||
return {
|
||||
title: (base && base.title) || '小程序分享',
|
||||
query: appendQueryParam(q, 'card_id', cardId),
|
||||
imageUrl: (base && base.imageUrl) || ''
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user