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:
@@ -20,7 +20,7 @@ import { toPng } from "html-to-image";
|
|||||||
import { Card } from "../components/ui/card";
|
import { Card } from "../components/ui/card";
|
||||||
import { Badge } from "../components/ui/badge";
|
import { Badge } from "../components/ui/badge";
|
||||||
import { Button } from "../components/ui/button";
|
import { Button } from "../components/ui/button";
|
||||||
import { generateShareData } from "../services/api"
|
import { generateShareData, generateWxacode } from "../services/api"
|
||||||
import bridge from "../services/bridge";
|
import bridge from "../services/bridge";
|
||||||
|
|
||||||
type SaveStatus = "idle" | "generating" | "success" | "error";
|
type SaveStatus = "idle" | "generating" | "success" | "error";
|
||||||
@@ -42,6 +42,7 @@ export function SharePage() {
|
|||||||
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
||||||
const [linkCopied, setLinkCopied] = useState(false);
|
const [linkCopied, setLinkCopied] = useState(false);
|
||||||
const [canShareToWechat, setCanShareToWechat] = useState(false);
|
const [canShareToWechat, setCanShareToWechat] = useState(false);
|
||||||
|
const [wxacodeUrl, setWxacodeUrl] = useState<string | null>(null);
|
||||||
const cardRef = useRef<HTMLDivElement>(null);
|
const cardRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -85,10 +86,51 @@ export function SharePage() {
|
|||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, [reportId]);
|
}, [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> => {
|
const generateImage = useCallback(async (): Promise<string | null> => {
|
||||||
if (!cardRef.current) return 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 {
|
try {
|
||||||
// html-to-image: 将 DOM 节点转为 PNG data URL
|
// html-to-image: 将 DOM 节点转为 PNG data URL
|
||||||
const dataUrl = await toPng(cardRef.current, {
|
const dataUrl = await toPng(cardRef.current, {
|
||||||
@@ -105,7 +147,7 @@ export function SharePage() {
|
|||||||
console.error("生成图片失败:", err);
|
console.error("生成图片失败:", err);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}, []);
|
}, [shareData, wxacodeUrl]);
|
||||||
|
|
||||||
// 一键保存到相册 / 下载
|
// 一键保存到相册 / 下载
|
||||||
const handleSaveImage = useCallback(async () => {
|
const handleSaveImage = useCallback(async () => {
|
||||||
@@ -622,6 +664,42 @@ function ShareCardContent({
|
|||||||
</div>
|
</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" }}>
|
<div style={{ paddingTop: "20px", borderTop: "2px solid #f3f4f6", textAlign: "center" }}>
|
||||||
<p style={{ fontSize: "12px", color: "#9ca3af", marginBottom: "4px", margin: "0 0 4px 0" }}>
|
<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" }} />
|
<Heart style={{ width: 12, height: 12, fill: "#b8a9c9", color: "#b8a9c9" }} />
|
||||||
<span>孕育无忧 · yun.tun88.com</span>
|
<span>孕育无忧 · 兜爸心选+</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -533,3 +533,27 @@ export async function generateShareData(
|
|||||||
body: JSON.stringify({ privacy_mode: true, include_details: false, ...options }),
|
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);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user