Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
P
pac
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ai-tools
pac
Commits
d587df92
Commit
d587df92
authored
Oct 15, 2026
by
luoqi
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix(web): connect host image links and appointment phone placeholders
parent
fa9ba3be
Pipeline
#3816
failed in 0 seconds
Changes
9
Pipelines
1
Show whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
245 additions
and
33 deletions
+245
-33
apps/pac-docs/content/docs/integration/overview.mdx
+5
-3
apps/pac-web/src/components/plan-detail/drawer.tsx
+19
-9
apps/pac-web/src/components/plan-detail/facts-timeline.tsx
+13
-8
apps/pac-web/src/components/plan-detail/host-image-link.test.tsx
+113
-0
apps/pac-web/src/components/plan-detail/host-image-link.tsx
+34
-0
apps/pac-web/src/components/plan-detail/plan-detail-app.tsx
+14
-11
apps/pac-web/src/lib/image-action-url.ts
+37
-0
apps/pac-web/vitest.config.ts
+8
-0
packages/types/src/enums/index.ts
+2
-2
No files found.
apps/pac-docs/content/docs/integration/overview.mdx
View file @
d587df92
...
...
@@ -93,15 +93,17 @@ PAC 出站 API 统一 ISO 8601 带时区 + 整数分。
```jsonc
{
"VIEW_PATIENT": "https://host.example.com/patient/{patientId}",
"VIEW_
EMR":
"https://host.example.com/emr?patientId={patientId}",
"CREATE_APPOINTMENT": "https://host.example.com/appointment/new?patientId={patientId}",
"VIEW_
XRAY": null, // null = 不支持,按钮不渲染
"VIEW_
MEDICAL_RECORD":
"https://host.example.com/emr?patientId={patientId}",
"CREATE_APPOINTMENT": "https://host.example.com/appointment/new?patientId={patientId}
&phone={phone}
",
"VIEW_
IMAGE": "https://host.example.com/patients/{patientId}/images",
"CALL_PATIENT": null
}
```
**只提供宿主方能支持的**,不写或填 `null` 都行。占位符 `{patientId}` / `{appointmentId}` / `{emrId}` / `{clinicId}` / `{userId}` 按字面替换。
`{patientId}` 使用宿主患者 ID;新建预约可用 `{phone}` 带入患者联系电话。查看影像可按 `{patientId}` 打开患者影像中心,或按 `{imageId}` 打开单张影像。已配置影像模板时优先使用模板;未配置时,影像记录中的完整文件链接仍可打开。
**打开方式**:PAC 前端 `window.open(url)`(新标签)或 `_top`(整页跳宿主页)。患者上下文经 URL 参数带过去,宿主页预填。
**动作没有独立页(是弹窗 / 组件)**:宿主给一个能触发弹窗的**深链参数**即可,仍是链接 —— 无需 postMessage:
...
...
apps/pac-web/src/components/plan-detail/drawer.tsx
View file @
d587df92
...
...
@@ -6,6 +6,8 @@ import { Chip } from './shared';
import
{
ChainDetailView
}
from
'./chain-viz'
;
import
{
EmrSoapView
}
from
'./emr-soap-view'
;
import
{
FactsTimeline
}
from
'./facts-timeline'
;
import
{
HostImageLink
}
from
'./host-image-link'
;
import
type
{
HostActionContext
}
from
'@/lib/image-action-url'
;
import
{
ToothTimeline
}
from
'./tooth-timeline'
;
import
{
PersonaDetailList
}
from
'./persona-detail-list'
;
import
type
{
Chain
,
PersonaFeature
,
PlanReason
}
from
'./mock-data'
;
...
...
@@ -25,6 +27,8 @@ export function Drawer({
reason
,
reasons
,
facts
,
hostActionContext
,
sourceUnit
,
fmtRel
,
returnVisits
,
summaryOverride
,
...
...
@@ -47,6 +51,8 @@ export function Drawer({
reasons
:
PlanReason
[];
/// v2.1:所有 active fact(FactsTimeline + ImageDrawer 用)
facts
:
AdaptedFact
[];
hostActionContext
:
HostActionContext
;
sourceUnit
?:
string
|
null
;
fmtRel
:
(
d
:
Date
)
=>
string
;
/// 历史联系(诊所回访)结构化记录 — 「历史联系」卡片「详情」抽屉用
returnVisits
:
ReturnVisitItem
[];
...
...
@@ -91,19 +97,19 @@ export function Drawer({
subtitle
=
imageFacts
.
length
>
0
?
`最早
${
imageFacts
[
imageFacts
.
length
-
1
]?.
occurredAt
?.
slice
(
0
,
10
)
??
'—'
}
~
最近
$
{
imageFacts
[
0
]?.
occurredAt
?.
slice
(
0
,
10
)
??
'—'
}
`
: '无影像 fact';
body = <ImageFactsView facts={imageFacts} />;
body = <ImageFactsView facts={imageFacts}
hostActionContext={hostActionContext} sourceUnit={sourceUnit}
/>;
width = 'w-[640px]';
} else if (kind === 'facts') {
title = `
患者事实时间轴
(
$
{
facts
.
length
})
`;
subtitle = '按时间倒序';
body = <FactsTimeline facts={facts} />;
body = <FactsTimeline facts={facts}
hostActionContext={hostActionContext} sourceUnit={sourceUnit}
/>;
width = 'w-[640px]';
} else if (kind === 'treatments') {
// 治疗历史卡「详情」:同 facts 时间轴,默认只勾选治疗(可在 chip 里再放开看其余)
const txCount = facts.filter((f) => f.type === 'treatment_record').length;
title = `
治疗时间轴
(
$
{
txCount
})
`;
subtitle = '仅治疗 · 时间倒序(治疗计划可在上方筛选打开)';
body = <FactsTimeline facts={facts} initialTypes={['treatment_record']} />;
body = <FactsTimeline facts={facts} initialTypes={['treatment_record']}
hostActionContext={hostActionContext} sourceUnit={sourceUnit}
/>;
width = 'w-[640px]';
} else if (kind === 'return-visits') {
title = `
历史联系
(
$
{
returnVisits
.
length
})
`;
...
...
@@ -207,14 +213,15 @@ function StreamingHint() {
// ImageFactsView — image_record fact 列表(真实数据)
// 影像原图走宿主系统(actionUrls.VIEW_IMAGE);此处只展示 metadata + finding
// ──────────────────────────────────────────
function ImageFactsView({ facts }: { facts: AdaptedFact[] }) {
function ImageFactsView({ facts, hostActionContext, sourceUnit }: {
facts: AdaptedFact[];
hostActionContext: HostActionContext;
sourceUnit?: string | null;
}) {
if (facts.length === 0) {
return (
<div className="text-center py-12 text-sm text-slate-400">
无影像 fact
<div className="text-[10.5px] mt-1.5">
(DW 给的是 metadata + URL,原图走宿主影像系统跳转)
</div>
暂无已同步的影像资料
</div>
);
}
...
...
@@ -255,11 +262,14 @@ function ImageFactsView({ facts }: { facts: AdaptedFact[] }) {
source encounter: {encId}
</div>
)}
<div className="mt-1.5">
<HostImageLink context={hostActionContext} sourceUnit={sourceUnit} fact={f} />
</div>
</div>
);
})}
<div className="text-[10.5px] text-slate-400 pt-2 border-t">
如需查看原始 DICOM,请跳转宿主影像系统(deep-link 待 host actionUrls 接入)
影像原件由宿主影像系统提供
</div>
</div>
);
...
...
apps/pac-web/src/components/plan-detail/facts-timeline.tsx
View file @
d587df92
...
...
@@ -21,6 +21,8 @@ import {
import
{
diagnosisCodeNameZh
,
treatmentCategoryNameZh
}
from
'@pac/types'
;
import
{
cn
}
from
'@/lib/utils'
;
import
type
{
AdaptedFact
}
from
'./adapt-data'
;
import
{
HostImageLink
}
from
'./host-image-link'
;
import
type
{
HostActionContext
}
from
'@/lib/image-action-url'
;
/**
* FactsTimeline — 患者事实时间轴(drawer 用)
...
...
@@ -34,10 +36,14 @@ import type { AdaptedFact } from './adapt-data';
export
function
FactsTimeline
({
facts
,
initialTypes
,
hostActionContext
,
sourceUnit
,
}:
{
facts
:
AdaptedFact
[];
/// 初始勾选的 fact_type(缺省全选);如治疗历史卡「详情」传 ['treatment_record'] → 默认只看治疗时间轴
initialTypes
?:
string
[];
hostActionContext
:
HostActionContext
;
sourceUnit
?:
string
|
null
;
})
{
if
(
facts
.
length
===
0
)
{
return
<
div
className=
"text-center py-12 text-sm text-slate-400"
>
无 fact
</
div
>;
...
...
@@ -155,7 +161,7 @@ export function FactsTimeline({
<
div
className=
"relative pl-6"
>
<
div
className=
"absolute left-[10px] top-1 bottom-1 w-px bg-slate-200"
/>
{
sorted
.
map
((
f
)
=>
(
<
TimelineRow
key=
{
f
.
id
}
fact=
{
f
}
/>
<
TimelineRow
key=
{
f
.
id
}
fact=
{
f
}
hostActionContext=
{
hostActionContext
}
sourceUnit=
{
sourceUnit
}
/>
))
}
</
div
>
)
}
...
...
@@ -292,7 +298,11 @@ function StatBlock({ label, children }: { label: string; children: React.ReactNo
// 单个 fact 时间轴行
// ─────────────────────────────────────────────
function
TimelineRow
({
fact
}:
{
fact
:
AdaptedFact
})
{
function
TimelineRow
({
fact
,
hostActionContext
,
sourceUnit
}:
{
fact
:
AdaptedFact
;
hostActionContext
:
HostActionContext
;
sourceUnit
?:
string
|
null
;
})
{
const
meta
=
FACT_META
[
fact
.
type
]
??
FACT_META_FALLBACK
;
const
Icon
=
meta
.
Icon
;
...
...
@@ -314,8 +324,6 @@ function TimelineRow({ fact }: { fact: AdaptedFact }) {
const
{
title
,
note
}
=
factSummary
(
fact
);
const
rightLines
=
rightColumn
(
fact
);
const
imageUrl
=
fact
.
content
?.
image_file_url
??
(
fact
.
type
===
'image_record'
?
fact
.
content
?.
file_url
:
null
);
return
(
<
div
className=
"relative pb-3"
>
...
...
@@ -340,10 +348,7 @@ function TimelineRow({ fact }: { fact: AdaptedFact }) {
{
note
&&
(
<
div
className=
"text-[11px] text-slate-500 leading-snug mt-0.5 line-clamp-2"
>
{
note
}
</
div
>
)
}
{
typeof
imageUrl
===
'string'
&&
/^https
?
:
\/\/
/i
.
test
(
imageUrl
)
&&
(
<
a
href=
{
imageUrl
}
target=
"_blank"
rel=
"noopener noreferrer"
className=
"text-[11px] text-brand-600 hover:underline"
>
查看影像
</
a
>
)
}
<
HostImageLink
context=
{
hostActionContext
}
sourceUnit=
{
sourceUnit
}
fact=
{
fact
}
/>
</
div
>
{
rightLines
.
length
>
0
&&
(
<
div
className=
"flex flex-col items-end gap-0.5 text-[11.5px] text-slate-600 tabular-nums whitespace-nowrap"
>
...
...
apps/pac-web/src/components/plan-detail/host-image-link.test.tsx
0 → 100644
View file @
d587df92
import
{
beforeEach
,
describe
,
expect
,
it
,
vi
}
from
'vitest'
;
import
{
renderToStaticMarkup
}
from
'react-dom/server'
;
import
{
HOST_ACTION_META
}
from
'@pac/types'
;
import
{
resolveActionUrl
}
from
'@/lib/action-url'
;
import
{
resolveImageActionUrl
,
type
ImageActionFact
}
from
'@/lib/image-action-url'
;
import
{
HostImageLink
}
from
'./host-image-link'
;
import
{
FactsTimeline
}
from
'./facts-timeline'
;
import
type
{
AdaptedFact
}
from
'./adapt-data'
;
const
state
=
vi
.
hoisted
(()
=>
({
actionUrls
:
{}
as
Record
<
string
,
string
|
null
>
}));
vi
.
mock
(
'@/stores/auth-store'
,
()
=>
({
useAuthStore
:
{
getState
:
()
=>
({
user
:
{
actionUrls
:
state
.
actionUrls
}
})
},
}));
const
context
=
{
patientId
:
'host-patient-123'
,
phone
:
'+86 13800000000'
,
brandId
:
'brand-1'
,
clinicId
:
'target-clinic'
,
medicalRecordNumber
:
'MR-123'
,
};
const
fact
:
AdaptedFact
=
{
id
:
'pac-fact-uuid'
,
type
:
'image_record'
,
kind
:
'actual'
,
status
:
'active'
,
occurredAt
:
'2026-10-15T06:40:00Z'
,
plannedFor
:
null
,
clinicId
:
'image-clinic'
,
title
:
null
,
summary
:
null
,
content
:
{
image_external_id
:
'host-image-456'
,
file_url
:
'oss/relative.jpg'
},
};
beforeEach
(()
=>
{
state
.
actionUrls
=
{};
});
describe
(
'宿主预约与影像跳转'
,
()
=>
{
it
(
'预约模板同时填入宿主患者 ID 和 URL 编码后的原始电话'
,
()
=>
{
state
.
actionUrls
.
CREATE_APPOINTMENT
=
'https://host.example/appointment?patientId={patientId}&phone={phone}'
;
const
result
=
new
URL
(
resolveActionUrl
(
'CREATE_APPOINTMENT'
,
context
,
'普尔'
)
!
);
expect
(
result
.
searchParams
.
get
(
'patientId'
)).
toBe
(
'host-patient-123'
);
expect
(
result
.
searchParams
.
get
(
'phone'
)).
toBe
(
'+86 13800000000'
);
expect
(
HOST_ACTION_META
.
CREATE_APPOINTMENT
.
placeholders
).
toContain
(
'{phone}'
);
});
it
(
'患者级影像中心不依赖已同步影像或 imageId'
,
()
=>
{
state
.
actionUrls
.
VIEW_IMAGE
=
'https://host.example/patients/{patientId}?tabKey=12'
;
expect
(
renderToStaticMarkup
(<
HostImageLink
context=
{
context
}
/>))
.
toContain
(
'href="https://host.example/patients/host-patient-123?tabKey=12"'
);
expect
(
HOST_ACTION_META
.
VIEW_IMAGE
.
placeholders
).
toContain
(
'{patientId}'
);
});
it
(
'患者卡片仅在配置了影像插槽时显示紧凑的「影像 →」链接'
,
()
=>
{
const
link
=
<
HostImageLink
context=
{
context
}
label=
"影像 →"
className=
"text-[10.5px] text-brand-700 hover:underline"
/>;
expect
(
renderToStaticMarkup
(
link
)).
toBe
(
''
);
state
.
actionUrls
.
VIEW_IMAGE
=
'https://host.example/patients/{patientId}?tabKey=12'
;
const
html
=
renderToStaticMarkup
(
link
);
expect
(
html
).
toContain
(
'影像 →'
);
expect
(
html
).
toContain
(
'text-[10.5px] text-brand-700 hover:underline'
);
expect
(
html
).
toContain
(
'/patients/host-patient-123?tabKey=12'
);
});
it
(
'影像事实优先用宿主模板,传源 imageId、该影像诊所及患者电话'
,
()
=>
{
state
.
actionUrls
.
VIEW_IMAGE
=
'https://host.example/images/{imageId}?patientId={patientId}&clinicId={clinicId}&phone={phone}'
;
const
result
=
new
URL
(
resolveImageActionUrl
(
context
,
'普尔'
,
fact
)
!
);
expect
(
result
.
pathname
).
toBe
(
'/images/host-image-456'
);
expect
(
result
.
searchParams
.
get
(
'patientId'
)).
toBe
(
'host-patient-123'
);
expect
(
result
.
searchParams
.
get
(
'clinicId'
)).
toBe
(
'image-clinic'
);
expect
(
result
.
searchParams
.
get
(
'phone'
)).
toBe
(
context
.
phone
);
});
it
(
'未配置模板时保留完整文件链接'
,
()
=>
{
expect
(
resolveImageActionUrl
(
context
,
null
,
{
...
fact
,
content
:
{
...
fact
.
content
,
file_url
:
'https://files.example/image.jpg?signature=abc'
},
})).
toBe
(
'https://files.example/image.jpg?signature=abc'
);
});
it
(
'相对文件路径不能直接打开'
,
()
=>
{
expect
(
resolveImageActionUrl
(
context
,
null
,
fact
)).
toBeUndefined
();
});
it
(
'没有 imageId 时不渲染单张影像模板的空链接'
,
()
=>
{
state
.
actionUrls
.
VIEW_IMAGE
=
'https://host.example/images/{imageId}'
;
expect
(
renderToStaticMarkup
(<
HostImageLink
context=
{
context
}
/>)).
toBe
(
''
);
});
it
(
'缺少模板所需的患者 ID 时回退到文件链接'
,
()
=>
{
state
.
actionUrls
.
VIEW_IMAGE
=
'https://host.example/patients/{patientId}'
;
expect
(
resolveImageActionUrl
({},
null
,
{
...
fact
,
content
:
{
file_url
:
'https://files.example/image.jpg'
},
})).
toBe
(
'https://files.example/image.jpg'
);
});
it
(
'沿用患者品牌换域规则,不把瑞泰影像带去瑞尔门户'
,
()
=>
{
state
.
actionUrls
.
VIEW_IMAGE
=
'https://arrail-test.5i5ya.com/images/{imageId}'
;
expect
(
resolveImageActionUrl
(
context
,
'瑞泰'
,
fact
))
.
toBe
(
'https://rytime-test.5i5ya.com/images/host-image-456'
);
});
it
(
'影像分析关联诊断使用对应的源影像 ID'
,
()
=>
{
state
.
actionUrls
.
VIEW_IMAGE
=
'https://host.example/images/{imageId}'
;
const
linked
:
ImageActionFact
=
{
type
:
'diagnosis_record'
,
content
:
{
source_image_external_id
:
'linked-image'
},
};
expect
(
resolveImageActionUrl
(
context
,
null
,
linked
)).
toBe
(
'https://host.example/images/linked-image'
);
expect
(
resolveImageActionUrl
(
context
,
null
,
{
type
:
'diagnosis_record'
,
content
:
{
code
:
'K02'
}
}))
.
toBeUndefined
();
});
it
(
'真实时间轴的影像入口使用患者模板,按新标签页打开'
,
()
=>
{
state
.
actionUrls
.
VIEW_IMAGE
=
'https://host.example/patients/{patientId}?tabKey=12'
;
const
html
=
renderToStaticMarkup
(
<
FactsTimeline
facts=
{
[
fact
]
}
hostActionContext=
{
context
}
sourceUnit=
"普尔"
/>,
);
expect
(
html
).
toContain
(
'href="https://host.example/patients/host-patient-123?tabKey=12"'
);
expect
(
html
).
toContain
(
'target="_blank"'
);
expect
(
html
).
toContain
(
'rel="noopener noreferrer"'
);
});
});
apps/pac-web/src/components/plan-detail/host-image-link.tsx
0 → 100644
View file @
d587df92
'use client'
;
import
{
HOST_LINK_PROPS
,
openHostUrl
}
from
'@/lib/action-url'
;
import
{
resolveImageActionUrl
,
type
HostActionContext
,
type
ImageActionFact
}
from
'@/lib/image-action-url'
;
export
function
HostImageLink
({
context
,
sourceUnit
,
fact
,
label
=
'查看影像'
,
className
=
'text-[11px] text-brand-600 hover:underline'
,
}:
{
context
:
HostActionContext
;
sourceUnit
?:
string
|
null
;
fact
?:
ImageActionFact
;
label
?:
string
;
className
?:
string
;
})
{
const
url
=
resolveImageActionUrl
(
context
,
sourceUnit
,
fact
);
if
(
!
url
)
return
null
;
return
(
<
a
href=
{
url
}
{
...
HOST_LINK_PROPS
}
className=
{
className
}
onClick=
{
(
e
)
=>
{
e
.
preventDefault
();
openHostUrl
(
url
);
}
}
>
{
label
}
</
a
>
);
}
apps/pac-web/src/components/plan-detail/plan-detail-app.tsx
View file @
d587df92
...
...
@@ -33,6 +33,7 @@ import { isEmbedded } from '@/lib/embed';
import
{
hostActionMode
,
openHostAction
}
from
'@/lib/host-message'
;
import
{
CloseOpportunityDialog
}
from
'./close-opportunity-dialog'
;
import
{
resolveActionUrl
,
openHostUrl
,
HOST_LINK_PROPS
}
from
'@/lib/action-url'
;
import
{
HostImageLink
}
from
'./host-image-link'
;
import
{
cn
,
formatGender
,
...
...
@@ -464,6 +465,7 @@ export function PlanDetailApp({
// 未配置 → 回调传 undefined → 按钮不渲染(PAC 默认无此动作)。预约:CREATE_APPOINTMENT 跳转。
const
hostActionCtx
=
{
patientId
:
patient
.
externalId
,
phone
:
patient
.
phone
,
brandId
:
patient
.
brandId
,
clinicId
:
plan
?.
targetClinicId
,
medicalRecordNumber
:
patient
.
medicalRecordNumber
,
...
...
@@ -586,12 +588,7 @@ export function PlanDetailApp({
// 宿主 actionUrls.CREATE_APPOINTMENT(会话下发)→ 通用占位替换 → 打开宿主页。
// 未配置则提示去配,不写死任何 URL。缺失键清空。
// ⚠️ URL 先解析:没配的话**不该先把单结了**再告诉他跳不过去。
const
url
=
resolveActionUrl
(
'CREATE_APPOINTMENT'
,
{
patientId
:
patient
.
externalId
,
brandId
:
patient
.
brandId
,
clinicId
:
plan
?.
targetClinicId
,
medicalRecordNumber
:
patient
.
medicalRecordNumber
,
},
patient
.
sourceUnit
);
const
url
=
resolveActionUrl
(
'CREATE_APPOINTMENT'
,
hostActionCtx
,
patient
.
sourceUnit
);
if
(
!
url
)
{
showToast
(
'amber'
,
'未配置新建预约'
,
'请在宿主管理页配置 actionUrls.CREATE_APPOINTMENT'
);
return
false
;
...
...
@@ -713,9 +710,7 @@ export function PlanDetailApp({
setPersonaFocusKey
(
key
);
setDrawerOpen
(
'persona'
);
}
}
onOpenImage=
{
()
=>
showToast
(
'slate'
,
'影像调阅'
,
'跳转宿主页面'
)
}
hostActionContext=
{
hostActionCtx
}
onOpenProfile=
{
()
=>
showToast
(
'slate'
,
'患者档案'
,
'跳转宿主页面'
)
}
...
...
@@ -994,6 +989,8 @@ export function PlanDetailApp({
reason=
{
focusedReason
??
reasons
[
0
]
!
}
reasons=
{
reasons
}
facts=
{
facts
}
hostActionContext=
{
hostActionCtx
}
sourceUnit=
{
patient
.
sourceUnit
}
fmtRel=
{
fmtRel
}
returnVisits=
{
returnVisits
}
summaryOverride=
{
summaryOverride
}
...
...
@@ -1629,7 +1626,7 @@ function IdentityCard({
patient
,
features
,
onOpenPersonaFeature
,
onOpenImage
,
hostActionContext
,
onOpenProfile
,
}:
{
patient
:
typeof
mockPatient
;
...
...
@@ -1637,7 +1634,7 @@ function IdentityCard({
features
:
typeof
mockPersona
.
features
;
/** 点关键标签 → 开画像详情抽屉并滚到那一条 */
onOpenPersonaFeature
:
(
featureKey
:
string
)
=>
void
;
onOpenImage
:
()
=>
void
;
hostActionContext
:
Record
<
string
,
string
|
null
|
undefined
>
;
onOpenProfile
:
()
=>
void
;
})
{
// 关键标签 —— 姓名下面直接展示,客服不用点开抽屉就能判断「能不能打、该聊什么」。
...
...
@@ -1699,6 +1696,12 @@ function IdentityCard({
</
span
>
</
div
>
<
div
className=
"flex items-center gap-2 flex-none"
>
<
HostImageLink
context=
{
hostActionContext
}
sourceUnit=
{
patient
.
sourceUnit
}
label=
"影像 →"
className=
"text-[10.5px] text-brand-700 hover:underline whitespace-nowrap"
/>
{
/* 潜在治疗入口已上移到顶栏「潜在」按钮(与 预约 / 回访 同处右上角动作区)。 */
}
{
/* 原始档案 —— 宿主 VIEW_PATIENT 跳转(占位 {patientId}/{medicalRecordNumber});未配则不渲染 */
}
{
originalArchiveUrl
?
(
...
...
apps/pac-web/src/lib/image-action-url.ts
0 → 100644
View file @
d587df92
import
{
actionTemplate
,
resolveActionUrl
}
from
'./action-url'
;
export
type
HostActionContext
=
Record
<
string
,
string
|
null
|
undefined
>
;
export
interface
ImageActionFact
{
type
:
string
;
clinicId
?:
string
|
null
;
content
?:
Record
<
string
,
unknown
>
|
null
;
}
/** 影像入口统一用宿主模板;没有可用模板时保留完整文件链接。 */
export
function
resolveImageActionUrl
(
context
:
HostActionContext
,
sourceUnit
?:
string
|
null
,
fact
?:
ImageActionFact
,
):
string
|
undefined
{
const
content
=
fact
?.
content
??
{};
const
imageId
=
content
.
image_external_id
??
content
.
source_image_external_id
;
if
(
fact
&&
fact
.
type
!==
'image_record'
&&
fact
.
type
!==
'image_analysis_record'
&&
!
imageId
&&
!
content
.
image_file_url
)
return
undefined
;
const
ctx
:
HostActionContext
=
{
...
context
,
clinicId
:
fact
?.
clinicId
??
context
.
clinicId
,
imageId
:
typeof
imageId
===
'string'
?
imageId
:
undefined
,
};
const
template
=
actionTemplate
(
'VIEW_IMAGE'
);
// 患者级入口没有 imageId,不能把单张影像模板里的标识清空后跳走。
const
missingId
=
[
'patientId'
,
'imageId'
].
some
((
key
)
=>
template
?.
includes
(
`{
${
key
}
}`
)
&&
!
ctx
[
key
]);
if
(
template
?.
trim
()
&&
!
missingId
)
{
return
resolveActionUrl
(
'VIEW_IMAGE'
,
ctx
,
sourceUnit
);
}
const
fileUrl
=
content
.
image_file_url
??
(
fact
?.
type
===
'image_record'
?
content
.
file_url
:
null
);
return
typeof
fileUrl
===
'string'
&&
/^https
?
:
\/\/
/i
.
test
(
fileUrl
)
?
fileUrl
:
undefined
;
}
apps/pac-web/vitest.config.ts
0 → 100644
View file @
d587df92
import
{
fileURLToPath
}
from
'node:url'
;
import
{
defineConfig
}
from
'vitest/config'
;
export
default
defineConfig
({
resolve
:
{
alias
:
{
'@'
:
fileURLToPath
(
new
URL
(
'./src'
,
import
.
meta
.
url
))
}
},
esbuild
:
{
jsx
:
'automatic'
},
test
:
{
maxWorkers
:
1
},
});
packages/types/src/enums/index.ts
View file @
d587df92
...
...
@@ -127,11 +127,11 @@ export type HostActionKey = (typeof HostActionKey)[keyof typeof HostActionKey];
export
const
HOST_ACTION_META
:
Record
<
HostActionKey
,
{
label
:
string
;
placeholders
:
string
[]
}
>
=
{
VIEW_PATIENT
:
{
label
:
'查看患者档案(原始档案)'
,
placeholders
:
[
'{patientId}'
,
'{medicalRecordNumber}'
]
},
EDIT_PATIENT
:
{
label
:
'编辑患者信息'
,
placeholders
:
[
'{patientId}'
]
},
CREATE_APPOINTMENT
:
{
label
:
'新建预约'
,
placeholders
:
[
'{patientId}'
,
'{doctorId}'
]
},
CREATE_APPOINTMENT
:
{
label
:
'新建预约'
,
placeholders
:
[
'{patientId}'
,
'{
phone}'
,
'{
doctorId}'
]
},
VIEW_APPOINTMENT
:
{
label
:
'查看预约'
,
placeholders
:
[
'{appointmentId}'
]
},
VIEW_ENCOUNTER
:
{
label
:
'查看接诊'
,
placeholders
:
[
'{encounterId}'
]
},
VIEW_MEDICAL_RECORD
:
{
label
:
'查看病历(电子病历)'
,
placeholders
:
[
'{brandId}'
,
'{clinicId}'
,
'{medicalRecordNumber}'
,
'{patientId}'
]
},
VIEW_IMAGE
:
{
label
:
'查看影像'
,
placeholders
:
[
'{
imageId
}'
]
},
VIEW_IMAGE
:
{
label
:
'查看影像'
,
placeholders
:
[
'{
patientId}'
,
'{imageId}'
,
'{phone}'
,
'{brandId}'
,
'{clinicId}'
,
'{medicalRecordNumber
}'
]
},
VIEW_PAYMENT
:
{
label
:
'查看账单/收款'
,
placeholders
:
[
'{paymentId}'
,
'{orderId}'
]
},
ADD_NOTE
:
{
label
:
'添加备注'
,
placeholders
:
[
'{patientId}'
]
},
OPEN_WECOM_CHAT
:
{
label
:
'打开企微会话'
,
placeholders
:
[
'{patientId}'
,
'{wecomExternalUserId}'
]
},
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment