Commit d98dcbd4 by luoqi

feat(embed): 患者卡片「潜在治疗」/ 通话结果「回访」走 postMessage 通知宿主

- 新增两个 postMessage 动作 OPEN_POTENTIAL_TREATMENT / OPEN_RETURN_VISIT(入 HostActionKey
  + HOST_ACTION_META,admin 下拉可选);HOST_ORIGIN 作 postMessage targetOrigin,走 host 配置
  (会话下发的 actionUrls.HOST_ORIGIN),禁用 '*'
- 患者卡片右上角「影像/档案」→「潜在治疗」;通话结果「拨打」→「回访」,固定就是这套(不判断 iframe)
- 信封只含 { source:'pac', action, payload:{patientId} };patientId=external_id
- postToHost 只在未配 HOST_ORIGIN 时提示;宿主侧监听校验 origin + source 后弹自己的组件

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
parent 1506e2fa
Pipeline #3343 failed in 0 seconds
'use client';
import { Phone } from 'lucide-react';
import { toast } from 'sonner';
import { CalendarClock } from 'lucide-react';
import { postToHost } from '@/lib/host-message';
/**
* CallWidget — 网页拨号占位按钮。
*
* 外呼真正落地需国内合规外呼线路(企业资质认证后开通);
* Twilio 等海外线路无法拨打中国大陆。现阶段点击仅提示,链路待接国内 provider。
* 通话结果区右上角「回访」按钮 —— postMessage 通知宿主打开它自己的回访组件(带 patientId)。
* 固定就是这套(不分嵌入与否);未配置 HOST_ORIGIN 时点击会提示去配。
*/
export function CallWidget() {
export function CallWidget({ patientId }: { patientId?: string }) {
return (
<button
onClick={() =>
toast('外呼功能待开通', {
description: '需完成企业资质认证 + 接入国内合规外呼线路后开放',
})
}
type="button"
onClick={() => postToHost('OPEN_RETURN_VISIT', patientId ?? '')}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md bg-teal-600 text-white text-[12px] font-medium hover:bg-teal-700 transition-colors"
>
<Phone className="w-3.5 h-3.5" />
拨打
<CalendarClock className="w-3.5 h-3.5" />
回访
</button>
);
}
......@@ -24,6 +24,7 @@ import { usePlanSyncStore } from '@/stores/plan-sync-store';
import { useAuthStore } from '@/stores/auth-store';
import { IdentityCluster } from '@/components/identity-cluster';
import { isEmbedded } from '@/lib/embed';
import { postToHost } from '@/lib/host-message';
import {
cn,
formatGender,
......@@ -478,7 +479,7 @@ export function PlanDetailApp({
<h2 className="text-[14px] font-semibold text-slate-900 leading-tight">通话结果</h2>
<p className="text-[10.5px] text-slate-500 mt-0.5">提交后本次任务自动归档</p>
</div>
<CallWidget />
<CallWidget patientId={patient.externalId} />
</header>
<div className="flex-1 min-h-0 overflow-y-auto p-3">
<OutcomeForm
......@@ -1032,11 +1033,13 @@ function IdentityCard({
</span>
</div>
<div className="flex items-center gap-2 flex-none">
<button onClick={onOpenImage} className="text-[10.5px] text-teal-700 hover:underline">
影像 →
</button>
<button onClick={onOpenProfile} className="text-[10.5px] text-teal-700 hover:underline">
档案 →
{/* 潜在治疗 —— postMessage 通知宿主打开其组件(固定就是这套,不分嵌入) */}
<button
type="button"
onClick={() => postToHost('OPEN_POTENTIAL_TREATMENT', patient.externalId ?? '')}
className="text-[10.5px] text-teal-700 hover:underline"
>
潜在治疗 →
</button>
</div>
</div>
......
'use client';
import { toast } from 'sonner';
import { useAuthStore } from '@/stores/auth-store';
/**
* PAC → 宿主 的动作事件(postMessage)。单向,信封只含 source / action / payload。
*
* 用于"宿主动作是弹窗/组件、没有独立 URL"的场景:PAC 只喊一声 + 带 patientId,
* 宿主父页监听后弹自己的组件。targetOrigin 走 host 配置 actionUrls.HOST_ORIGIN(不允许 '*')。
* 不做 iframe 判断:按钮固定就是这套;非嵌入时 targetOrigin 与当前窗口不符,浏览器自动丢弃(无副作用)。
*/
export type HostAction = 'OPEN_POTENTIAL_TREATMENT' | 'OPEN_RETURN_VISIT';
/** 宿主 origin(postMessage targetOrigin)—— 来自会话下发的 actionUrls.HOST_ORIGIN。 */
export function hostOrigin(): string | undefined {
return useAuthStore.getState().user?.actionUrls?.HOST_ORIGIN || undefined;
}
/** 向宿主父窗发一条动作事件。未配置 origin 时提示并返 false。 */
export function postToHost(action: HostAction, patientId: string): boolean {
const origin = hostOrigin();
if (!origin) {
toast('未配置宿主 Origin', { description: '请在宿主管理页配置 actionUrls.HOST_ORIGIN' });
return false;
}
// 信封:source + action + payload(无 version / doctorId / meta)
window.parent.postMessage({ source: 'pac', action, payload: { patientId } }, origin);
return true;
}
......@@ -81,6 +81,12 @@ export const HostActionKey = {
ADD_NOTE: 'ADD_NOTE', // 加备注
OPEN_WECOM_CHAT: 'OPEN_WECOM_CHAT', // 打开企微会话
CALL_PATIENT: 'CALL_PATIENT', // 拨号
// postMessage 动作(无 URL,PAC 通过 postMessage 通知宿主打开其组件,payload 带 patientId)
OPEN_POTENTIAL_TREATMENT: 'OPEN_POTENTIAL_TREATMENT', // 打开潜在治疗
OPEN_RETURN_VISIT: 'OPEN_RETURN_VISIT', // 打开回访
// 非跳转 URL,而是 postMessage 目标源(embed 配置)—— 上面两个 postMessage 动作的 targetOrigin;
// 值填宿主承载 iframe 的页面 origin(如 https://host.example.com)。
HOST_ORIGIN: 'HOST_ORIGIN',
} as const;
export type HostActionKey = (typeof HostActionKey)[keyof typeof HostActionKey];
......@@ -97,6 +103,9 @@ export const HOST_ACTION_META: Record<HostActionKey, { label: string; placeholde
ADD_NOTE: { label: '添加备注', placeholders: ['{patientId}'] },
OPEN_WECOM_CHAT: { label: '打开企微会话', placeholders: ['{patientId}', '{wecomExternalUserId}'] },
CALL_PATIENT: { label: '拨号', placeholders: ['{phone}'] },
OPEN_POTENTIAL_TREATMENT: { label: '打开潜在治疗(postMessage)', placeholders: ['{patientId}'] },
OPEN_RETURN_VISIT: { label: '打开回访(postMessage)', placeholders: ['{patientId}'] },
HOST_ORIGIN: { label: '宿主 Origin(postMessage 目标源)', placeholders: [] },
};
export const PatientStatus = {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment