feat(share): SharePage 集成 box_app 太阳码

- 调 box_app /api/wxacode 生成小程序太阳码
  目标路径: pages/annex/web_view/index(CRMEB web-view 容器)
  scene 留空——扫码直接进 app_03 H5 首页
- ShareCardContent 末尾新增太阳码区
  200×200 PNG + 引导文字「微信扫码下载/长按保存」
  太阳码在长图截图中可见
- 截图前等待太阳码就绪(最多 1.5s),避免截图缺失
- 卸载时清理 blob URL 防内存泄漏
- crossOrigin="anonymous" 防止 html-to-image 截取跨域图失败
- 太阳码生成失败时降级:长图照常生成,仅缺二维码区

设计决策:
- 不引入匿名 token 机制:收件人通过 CRMEB 微信小程序 web-view 进入
  共享 CRMEB 主体 openid 即可正常使用 app_03
- SharePage 仍走 ProtectedRoute(受保护)
  收件人访问 /share/:reportId 由 CRMEB 注入 openid 完成鉴权
This commit is contained in:
2026-07-08 15:28:07 +08:00
parent d4734d48f5
commit 772700cd85
2 changed files with 105 additions and 3 deletions

View File

@@ -20,7 +20,7 @@ import { toPng } from "html-to-image";
import { Card } from "../components/ui/card";
import { Badge } from "../components/ui/badge";
import { Button } from "../components/ui/button";
import { generateShareData } from "../services/api"
import { generateShareData, generateWxacode } from "../services/api"
import bridge from "../services/bridge";
type SaveStatus = "idle" | "generating" | "success" | "error";
@@ -42,6 +42,7 @@ export function SharePage() {
const [imageUrl, setImageUrl] = useState<string | null>(null);
const [linkCopied, setLinkCopied] = useState(false);
const [canShareToWechat, setCanShareToWechat] = useState(false);
const [wxacodeUrl, setWxacodeUrl] = useState<string | null>(null);
const cardRef = useRef<HTMLDivElement>(null);
useEffect(() => {
@@ -85,10 +86,51 @@ export function SharePage() {
.finally(() => setLoading(false));
}, [reportId]);
// 太阳码:shareData 就绪后异步生成,blob URL 存到 wxacodeUrl
useEffect(() => {
if (!shareData) return;
let cancelled = false;
generateWxacode({ width: 430 })
.then((url) => {
if (!cancelled) setWxacodeUrl(url);
})
.catch((err) => {
console.warn("[SharePage] 太阳码生成失败:", err);
});
return () => {
cancelled = true;
};
}, [shareData]);
// 卸载时清理 blob URL
useEffect(() => {
return () => {
if (wxacodeUrl?.startsWith("blob:")) URL.revokeObjectURL(wxacodeUrl);
};
}, [wxacodeUrl]);
// 生成长图
const generateImage = useCallback(async (): Promise<string | null> => {
if (!cardRef.current) return null;
// 等待太阳码渲染完成(blob URL 需浏览器解码)
if (shareData && !wxacodeUrl) {
await new Promise<void>((resolve) => {
const start = Date.now();
const tick = () => {
if (wxacodeUrl || Date.now() - start > 1500) {
resolve();
return;
}
setTimeout(tick, 100);
};
tick();
});
if (!wxacodeUrl) {
console.warn("[SharePage] 太阳码未就绪,截图中可能缺失二维码");
}
}
try {
// html-to-image: 将 DOM 节点转为 PNG data URL
const dataUrl = await toPng(cardRef.current, {
@@ -105,7 +147,7 @@ export function SharePage() {
console.error("生成图片失败:", err);
return null;
}
}, []);
}, [shareData, wxacodeUrl]);
// 一键保存到相册 / 下载
const handleSaveImage = useCallback(async () => {
@@ -622,6 +664,42 @@ function ShareCardContent({
</div>
)}
{/* 太阳码区(截图包含) */}
{wxacodeUrl && (
<div
style={{
textAlign: "center",
paddingTop: "20px",
borderTop: "1px dashed #e5e7eb",
}}
>
<img
src={wxacodeUrl}
alt="扫码下载孕育无忧"
crossOrigin="anonymous"
style={{
width: 200,
height: 200,
borderRadius: 8,
background: "#ffffff",
}}
/>
<p
style={{
marginTop: 8,
fontSize: 12,
color: "#9ca3af",
lineHeight: 1.5,
margin: "8px 0 0 0",
}}
>
微信扫码下载「孕育无忧」APP
<br />
长按图片也可保存到相册
</p>
</div>
)}
{/* 底部声明 */}
<div style={{ paddingTop: "20px", borderTop: "2px solid #f3f4f6", textAlign: "center" }}>
<p style={{ fontSize: "12px", color: "#9ca3af", marginBottom: "4px", margin: "0 0 4px 0" }}>
@@ -643,7 +721,7 @@ function ShareCardContent({
}}
>
<Heart style={{ width: 12, height: 12, fill: "#b8a9c9", color: "#b8a9c9" }} />
<span>孕育无忧 · yun.tun88.com</span>
<span>孕育无忧 · 兜爸心选+</span>
</div>
</div>
</div>

View File

@@ -533,3 +533,27 @@ export async function generateShareData(
body: JSON.stringify({ privacy_mode: true, include_details: false, ...options }),
});
}
// ─── WXacode(外部服务,box_app 太阳码生成)───
//
// 调用 box_app /api/wxacode,生成可被微信扫码直接进入 CRMEB 小程序的太阳码。
// scene 留空——目标页是 CRMEB 通用 web-view 容器 pages/annex/web_view/index,
// 扫码后收件人直接进入 app_03 H5 首页(与「分享 → 扫码进首页」流程一致)。
const WXACODE_API_HOST = "https://mp.yizhezhe.com";
const WXACODE_PATH = "pages/annex/web_view/index";
/** 调用 box_app 生成小程序太阳码,返回 blob URL(用于 <img> 展示) */
export async function generateWxacode(
options: { width?: number } = {}
): Promise<string> {
const params = new URLSearchParams({
path: WXACODE_PATH,
width: String(options.width ?? 430),
});
const res = await fetch(`${WXACODE_API_HOST}/api/wxacode?${params}`);
if (!res.ok) {
throw new Error(`太阳码生成失败 (${res.status})`);
}
const blob = await res.blob();
return URL.createObjectURL(blob);
}