From 9b315fd77dec1d6e4c4fab45f21397f27b46c9ae Mon Sep 17 00:00:00 2001 From: Karlcarlo Date: Thu, 25 Jun 2026 10:57:26 +0800 Subject: [PATCH] feat: add bridge service for mini program web-view integration - Implemented a bridge utility for sharing, copying links, and setting the navigation bar title in mini program web-view. - Added methods for sharing content and copying links to clipboard. - Included error handling for non-mini program environments. --- frontend/src/app/pages/SharePage.tsx | 1168 +++++++++++++------------- frontend/src/app/services/bridge.ts | 82 ++ 2 files changed, 676 insertions(+), 574 deletions(-) create mode 100644 frontend/src/app/services/bridge.ts diff --git a/frontend/src/app/pages/SharePage.tsx b/frontend/src/app/pages/SharePage.tsx index 0a977c4..07d2ec1 100644 --- a/frontend/src/app/pages/SharePage.tsx +++ b/frontend/src/app/pages/SharePage.tsx @@ -1,574 +1,594 @@ -import { useState, useEffect, useRef, useCallback } from "react"; -import { useNavigate, useParams } from "react-router"; -import { motion, AnimatePresence } from "motion/react"; -import { - ChevronLeft, - Share2, - CheckCircle2, - AlertCircle, - Info, - Loader2, - Heart, - Baby, - TrendingUp, - Download, - Check, - Image as ImageIcon, -} from "lucide-react"; -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"; - -type SaveStatus = "idle" | "generating" | "success" | "error"; - -export function SharePage() { - const navigate = useNavigate(); - const { reportId } = useParams(); - const [loading, setLoading] = useState(true); - const [shareData, setShareData] = useState(null); - const [saveStatus, setSaveStatus] = useState("idle"); - const [imageUrl, setImageUrl] = useState(null); - const cardRef = useRef(null); - - useEffect(() => { - if (!reportId) return; - generateShareData(reportId) - .then((data) => { - setShareData(data.share_data); - }) - .catch(() => {/* keep null */}) - .finally(() => setLoading(false)); - }, [reportId]); - - // 生成长图 - const generateImage = useCallback(async (): Promise => { - if (!cardRef.current) return null; - - try { - // html-to-image: 将 DOM 节点转为 PNG data URL - const dataUrl = await toPng(cardRef.current, { - cacheBust: true, - pixelRatio: 2, // 2x 高清 - backgroundColor: "#ffffff", - style: { - // 确保截图时移除圆角阴影的裁切问题 - borderRadius: "0", - }, - }); - return dataUrl; - } catch (err) { - console.error("生成图片失败:", err); - return null; - } - }, []); - - // 一键保存到相册 / 下载 - const handleSaveImage = useCallback(async () => { - setSaveStatus("generating"); - - try { - const dataUrl = imageUrl || (await generateImage()); - if (!dataUrl) { - setSaveStatus("error"); - setTimeout(() => setSaveStatus("idle"), 2000); - return; - } - - if (!imageUrl) setImageUrl(dataUrl); - - // 尝试使用 Web Share API(移动端可直接分享/保存到相册) - if (navigator.share && navigator.canShare) { - try { - const response = await fetch(dataUrl); - const blob = await response.blob(); - const file = new File([blob], `孕育无忧-报告解读-${reportId}.png`, { - type: "image/png", - }); - - if (navigator.canShare({ files: [file] })) { - await navigator.share({ - files: [file], - title: "孕育无忧 · AI 报告解读", - }); - setSaveStatus("success"); - setTimeout(() => setSaveStatus("idle"), 3000); - return; - } - } catch (shareErr) { - // 用户取消分享或不支持,降级到下载 - if ((shareErr as Error).name === "AbortError") { - setSaveStatus("idle"); - return; - } - } - } - - // 降级方案:触发下载(iOS Safari / Android 浏览器) - const link = document.createElement("a"); - link.download = `孕育无忧-报告解读-${reportId}.png`; - link.href = dataUrl; - link.style.display = "none"; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - - setSaveStatus("success"); - setTimeout(() => setSaveStatus("idle"), 3000); - } catch (err) { - console.error("保存失败:", err); - setSaveStatus("error"); - setTimeout(() => setSaveStatus("idle"), 2000); - } - }, [imageUrl, generateImage, reportId]); - - // 预览:在页面加载数据后自动生成图片预览 - useEffect(() => { - if (shareData && cardRef.current && !imageUrl) { - // 延迟一帧确保 DOM 渲染完毕 - const timer = setTimeout(async () => { - const url = await generateImage(); - if (url) setImageUrl(url); - }, 500); - return () => clearTimeout(timer); - } - }, [shareData, generateImage, imageUrl]); - - if (loading || !shareData) { - return ( -
-
- -

正在准备分享内容...

-
-
- ); - } - - const getStatusColor = (status: string) => { - switch (status) { - case "normal": - return { - bg: "bg-morandi-sage-100", - text: "text-morandi-sage-600", - icon: "text-morandi-sage-400", - }; - case "attention": - return { - bg: "bg-morandi-rose-100", - text: "text-morandi-600", - icon: "text-morandi-rose-400", - }; - default: - return { - bg: "bg-gray-100", - text: "text-gray-600", - icon: "text-gray-400", - }; - } - }; - - const statusColors = getStatusColor(shareData.overall_status); - - const getSaveButtonContent = () => { - switch (saveStatus) { - case "generating": - return ( - <> - - 正在生成图片... - - ); - case "success": - return ( - <> - - 已保存到相册 - - ); - case "error": - return ( - <> - - 保存失败,请重试 - - ); - default: - return ( - <> - - 保存图片到相册 - - ); - } - }; - - return ( -
- {/* 顶部导航 */} -
-
-
- -
-
- -
-
-

分享长图

-

生成精美报告长图

-
-
-
-
-
- -
- {/* 图片预览区域 */} - - {/* 如果已经生成了图片,直接展示图片(可长按保存) */} - {imageUrl ? ( -
- 分享长图 - {/* 长按提示覆盖层 */} -
-

- 💡 长按图片也可直接保存 -

-
-
- ) : ( -
-
- -

正在生成长图预览...

-
-
- )} -
- - {/* 隐藏的 HTML 卡片(用于截图,不展示给用户) */} -
-
- -
-
- - {/* 操作提示 */} - - -
- -
-

💡 分享说明

-
-

- • 点击下方按钮,一键保存图片到手机相册 -

-

- • 也可以长按上方图片直接保存 -

-

- • 个人信息已自动脱敏处理,保护您的隐私 -

-
-
-
-
-
-
- - {/* 底部固定操作栏 */} -
-
- -

- 图片将保存到您的手机相册中 -

-
-
-
- ); -} - -/** - * 分享卡片内容组件(纯 HTML/CSS,不使用 motion 动画,用于截图) - * 独立提取,确保截图时样式完整、无动画干扰 - */ -function ShareCardContent({ - shareData, - getStatusColor, - statusColors, -}: { - shareData: any; - getStatusColor: (status: string) => { bg: string; text: string; icon: string }; - statusColors: { bg: string; text: string; icon: string }; -}) { - return ( -
-
- {/* 标题区 */} -
-
- - 孕育无忧 · AI 智能解读 -
-

- {shareData.report_type} -

-
- {shareData.pregnancy_week && 孕 {shareData.pregnancy_week} 周} - {shareData.user_name && ( - <> - · - {shareData.user_name} - - )} -
-
- - {/* 整体评估 */} -
-
-
- -
-
-

- 整体评估 -

- - {shareData.overall_status === "normal" ? "一切正常" : "需要关注"} - -
-
-

- {shareData.emotional_summary || shareData.overall_summary} -

-
- - {/* 详细指标 */} - {shareData.categories && shareData.categories.length > 0 && ( -
-

- - 详细指标 -

- {shareData.categories.map((category: any, idx: number) => { - const catColors = getStatusColor(category.status); - return ( -
-
-

{category.category}

- - {category.status === "normal" ? "正常" : "需关注"} - -
-
- {category.indicators.map((ind: any, indIdx: number) => ( -
-
-
-
- {ind.name} - {ind.nickname && ( - ({ind.nickname}) - )} -
- {ind.value && ( -
- {ind.value} {ind.unit} -
- )} -
- - {ind.status === "normal" ? "正常" : ind.status === "high" ? "偏高" : "偏低"} - -
- {ind.what_is_it && ( -

- {ind.what_is_it} -

- )} - {ind.suggestion && ( -
-

💡 {ind.suggestion}

-
- )} -
- ))} -
-
- ); - })} -
- )} - - {/* 行动建议 */} - {shareData.action_items && shareData.action_items.length > 0 && ( -
-

- 接下来该做什么? -

-
- {shareData.action_items.map((item: any, idx: number) => ( -
-
- - {idx + 1} - -
-
- {item.title || item.content} -
- {item.description && ( -

{item.description}

- )} -
-
-
- ))} -
-
- )} - - {/* 温馨提示 */} - {shareData.tips && shareData.tips.length > 0 && ( -
-

- - 温馨提示 -

-
    - {shareData.tips.map((tip: string, idx: number) => ( -
  • - • - {tip} -
  • - ))} -
-
- )} - - {/* 底部声明 */} -
-

- 本报告由 AI 生成,仅供参考,不能替代医生诊断 -

-

- 如有疑问请及时咨询专业医生 -

-
- - 孕育无忧 · yun.tun88.com -
-
-
-
- ); -} +import { useState, useEffect, useRef, useCallback } from "react"; +import { useNavigate, useParams } from "react-router"; +import { motion, AnimatePresence } from "motion/react"; +import { + ChevronLeft, + Share2, + CheckCircle2, + AlertCircle, + Info, + Loader2, + Heart, + Baby, + TrendingUp, + Download, + Check, + Image as ImageIcon, +} from "lucide-react"; +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 bridge from "../services/bridge"; + +type SaveStatus = "idle" | "generating" | "success" | "error"; + +export function SharePage() { + const navigate = useNavigate(); + const { reportId } = useParams(); + const [loading, setLoading] = useState(true); + const [shareData, setShareData] = useState(null); + const [saveStatus, setSaveStatus] = useState("idle"); + const [imageUrl, setImageUrl] = useState(null); + const cardRef = useRef(null); + + useEffect(() => { + if (!reportId) return; + generateShareData(reportId) + .then((data) => { + setShareData(data.share_data); + }) + .catch(() => {/* keep null */}) + .finally(() => setLoading(false)); + }, [reportId]); + + // 生成长图 + const generateImage = useCallback(async (): Promise => { + if (!cardRef.current) return null; + + try { + // html-to-image: 将 DOM 节点转为 PNG data URL + const dataUrl = await toPng(cardRef.current, { + cacheBust: true, + pixelRatio: 2, // 2x 高清 + backgroundColor: "#ffffff", + style: { + // 确保截图时移除圆角阴影的裁切问题 + borderRadius: "0", + }, + }); + return dataUrl; + } catch (err) { + console.error("生成图片失败:", err); + return null; + } + }, []); + + // 一键保存到相册 / 下载 + const handleSaveImage = useCallback(async () => { + setSaveStatus("generating"); + + try { + const dataUrl = imageUrl || (await generateImage()); + if (!dataUrl) { + setSaveStatus("error"); + setTimeout(() => setSaveStatus("idle"), 2000); + return; + } + + if (!imageUrl) setImageUrl(dataUrl); + + // 尝试使用 Web Share API(移动端可直接分享/保存到相册) + if (navigator.share && navigator.canShare) { + try { + const response = await fetch(dataUrl); + const blob = await response.blob(); + const file = new File([blob], `孕育无忧-报告解读-${reportId}.png`, { + type: "image/png", + }); + + if (navigator.canShare({ files: [file] })) { + await navigator.share({ + files: [file], + title: "孕育无忧 · AI 报告解读", + }); + setSaveStatus("success"); + setTimeout(() => setSaveStatus("idle"), 3000); + return; + } + } catch (shareErr) { + // 用户取消分享或不支持,降级到下载 + if ((shareErr as Error).name === "AbortError") { + setSaveStatus("idle"); + return; + } + } + } + + // 降级方案:触发下载(iOS Safari / Android 浏览器) + const link = document.createElement("a"); + link.download = `孕育无忧-报告解读-${reportId}.png`; + link.href = dataUrl; + link.style.display = "none"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + setSaveStatus("success"); + setTimeout(() => setSaveStatus("idle"), 3000); + } catch (err) { + console.error("保存失败:", err); + setSaveStatus("error"); + setTimeout(() => setSaveStatus("idle"), 2000); + } + }, [imageUrl, generateImage, reportId]); + + // 预览:在页面加载数据后自动生成图片预览 + useEffect(() => { + if (shareData && cardRef.current && !imageUrl) { + // 延迟一帧确保 DOM 渲染完毕 + const timer = setTimeout(async () => { + const url = await generateImage(); + if (url) setImageUrl(url); + }, 500); + return () => clearTimeout(timer); + } + }, [shareData, generateImage, imageUrl]); + + if (loading || !shareData) { + return ( +
+
+ +

正在准备分享内容...

+
+
+ ); + } + + const getStatusColor = (status: string) => { + switch (status) { + case "normal": + return { + bg: "bg-morandi-sage-100", + text: "text-morandi-sage-600", + icon: "text-morandi-sage-400", + }; + case "attention": + return { + bg: "bg-morandi-rose-100", + text: "text-morandi-600", + icon: "text-morandi-rose-400", + }; + default: + return { + bg: "bg-gray-100", + text: "text-gray-600", + icon: "text-gray-400", + }; + } + }; + + const statusColors = getStatusColor(shareData.overall_status); + + const getSaveButtonContent = () => { + switch (saveStatus) { + case "generating": + return ( + <> + + 正在生成图片... + + ); + case "success": + return ( + <> + + 已保存到相册 + + ); + case "error": + return ( + <> + + 保存失败,请重试 + + ); + default: + return ( + <> + + 保存图片到相册 + + ); + } + }; + + return ( +
+ {/* 顶部导航 */} +
+
+
+ +
+
+ +
+
+

分享长图

+

生成精美报告长图

+
+
+
+
+
+ +
+ {/* 图片预览区域 */} + + {/* 如果已经生成了图片,直接展示图片(可长按保存) */} + {imageUrl ? ( +
+ 分享长图 + {/* 长按提示覆盖层 */} +
+

+ 💡 长按图片也可直接保存 +

+
+
+ ) : ( +
+
+ +

正在生成长图预览...

+
+
+ )} +
+ + {/* 隐藏的 HTML 卡片(用于截图,不展示给用户) */} +
+
+ +
+
+ + {/* 操作提示 */} + + +
+ +
+

💡 分享说明

+
+

+ • 点击下方按钮,一键保存图片到手机相册 +

+

+ • 也可以长按上方图片直接保存 +

+

+ • 个人信息已自动脱敏处理,保护您的隐私 +

+
+
+
+
+
+
+ + {/* 底部固定操作栏 */} +
+
+ {/* 微信分享按钮(仅在 web-view 环境下生效) */} + + + +

+ 分享到微信或保存图片到相册 +

+
+
+
+ ); +} + +/** + * 分享卡片内容组件(纯 HTML/CSS,不使用 motion 动画,用于截图) + * 独立提取,确保截图时样式完整、无动画干扰 + */ +function ShareCardContent({ + shareData, + getStatusColor, + statusColors, +}: { + shareData: any; + getStatusColor: (status: string) => { bg: string; text: string; icon: string }; + statusColors: { bg: string; text: string; icon: string }; +}) { + return ( +
+
+ {/* 标题区 */} +
+
+ + 孕育无忧 · AI 智能解读 +
+

+ {shareData.report_type} +

+
+ {shareData.pregnancy_week && 孕 {shareData.pregnancy_week} 周} + {shareData.user_name && ( + <> + · + {shareData.user_name} + + )} +
+
+ + {/* 整体评估 */} +
+
+
+ +
+
+

+ 整体评估 +

+ + {shareData.overall_status === "normal" ? "一切正常" : "需要关注"} + +
+
+

+ {shareData.emotional_summary || shareData.overall_summary} +

+
+ + {/* 详细指标 */} + {shareData.categories && shareData.categories.length > 0 && ( +
+

+ + 详细指标 +

+ {shareData.categories.map((category: any, idx: number) => { + const catColors = getStatusColor(category.status); + return ( +
+
+

{category.category}

+ + {category.status === "normal" ? "正常" : "需关注"} + +
+
+ {category.indicators.map((ind: any, indIdx: number) => ( +
+
+
+
+ {ind.name} + {ind.nickname && ( + ({ind.nickname}) + )} +
+ {ind.value && ( +
+ {ind.value} {ind.unit} +
+ )} +
+ + {ind.status === "normal" ? "正常" : ind.status === "high" ? "偏高" : "偏低"} + +
+ {ind.what_is_it && ( +

+ {ind.what_is_it} +

+ )} + {ind.suggestion && ( +
+

💡 {ind.suggestion}

+
+ )} +
+ ))} +
+
+ ); + })} +
+ )} + + {/* 行动建议 */} + {shareData.action_items && shareData.action_items.length > 0 && ( +
+

+ 接下来该做什么? +

+
+ {shareData.action_items.map((item: any, idx: number) => ( +
+
+ + {idx + 1} + +
+
+ {item.title || item.content} +
+ {item.description && ( +

{item.description}

+ )} +
+
+
+ ))} +
+
+ )} + + {/* 温馨提示 */} + {shareData.tips && shareData.tips.length > 0 && ( +
+

+ + 温馨提示 +

+
    + {shareData.tips.map((tip: string, idx: number) => ( +
  • + • + {tip} +
  • + ))} +
+
+ )} + + {/* 底部声明 */} +
+

+ 本报告由 AI 生成,仅供参考,不能替代医生诊断 +

+

+ 如有疑问请及时咨询专业医生 +

+
+ + 孕育无忧 · yun.tun88.com +
+
+
+
+ ); +} diff --git a/frontend/src/app/services/bridge.ts b/frontend/src/app/services/bridge.ts new file mode 100644 index 0000000..bca0813 --- /dev/null +++ b/frontend/src/app/services/bridge.ts @@ -0,0 +1,82 @@ +/** + * 小程序 WebView 桥接工具 + * + * 在 H5(app_03)中调用此工具,可实现分享、复制链接等功能。 + * 小程序侧(crmeb web_view)通过 @message 监听处理。 + * + * 使用方式: + * import bridge from '../services/bridge' + * + * // 分享给好友 + * bridge.share({ title: '标题', href: window.location.href }) + * + * // 复制链接 + * bridge.copyLink(window.location.href) + */ + +declare const wx: any + +function getMiniProgram() { + try { + if (typeof wx !== 'undefined' && wx.miniProgram) { + return wx.miniProgram + } + } catch { + // ignore + } + return null +} + +const bridge = { + postMessage(data: Record) { + const mp = getMiniProgram() + if (mp) { + mp.postMessage({ data }) + } else { + console.warn('[bridge] 不在小程序 web-view 环境中') + } + }, + + /** + * 分享给微信好友 / 朋友圈 + */ + share(opts: { + title: string + href?: string + summary?: string + imageUrl?: string + scene?: 'WXSceneSession' | 'WXSceneTimeline' + }) { + this.postMessage({ + action: 'share', + title: opts.title || '', + href: opts.href || window.location.href, + summary: opts.summary || '', + imageUrl: opts.imageUrl || '', + scene: opts.scene || 'WXSceneSession', + }) + }, + + /** + * 复制链接到剪贴板 + */ + copyLink(link?: string) { + this.postMessage({ + action: 'copyLink', + href: link || window.location.href, + }) + }, + + /** + * 设置小程序导航栏标题 + */ + setTitle(title: string) { + if (!title) return + this.postMessage({ + action: 'setTitle', + title, + }) + }, +} + +export default bridge