Commit d45b0c86 by luoqi

feat(web): 画像标签并入身份卡 + hover 合并取值/算法 + 隐藏治疗链卡

详情页左栏调整:
- 去掉独立'画像标签'卡片,标签云并入左上第一个卡片(IdentityCard)底部(手机号下,带分隔线 + 详情→)。
- 标签 hover:去掉原生 title(避免 title+hovercard 双浮窗);把 title 内容(该患者实际取值)
  并入 hovercard 顶部(teal 框)→ 一个浮窗同时看'是什么(取值)+ 怎么算的(规则)'。
  PersonaFeatureHover 加 value prop,meta 缺失时也能只显取值。
- 隐藏'治疗链'卡片(链已弃用;chains 仍传 drawer 备用)+ 删未用 ChainSidebar import。
- web tsc 通过(next dev 热更,无需重启)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parent 02e1e2ed
...@@ -14,13 +14,16 @@ import { HoverCard, HoverCardTrigger, HoverCardContent } from '@/components/ui/h ...@@ -14,13 +14,16 @@ import { HoverCard, HoverCardTrigger, HoverCardContent } from '@/components/ui/h
*/ */
export function PersonaFeatureHover({ export function PersonaFeatureHover({
featureKey, featureKey,
value,
children, children,
}: { }: {
featureKey: string; featureKey: string;
/// 该患者的实际取值(原生 title 内容并入 hovercard 顶部)
value?: string;
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const meta = ALGORITHMS[featureKey]; const meta = ALGORITHMS[featureKey];
if (!meta) return <>{children}</>; if (!meta && !value) return <>{children}</>;
return ( return (
<HoverCard openDelay={150} closeDelay={80}> <HoverCard openDelay={150} closeDelay={80}>
{/* 直接把 children 作为 trigger,radix 锚定 children 本身的位置(避免多包一层 0 尺寸 span)*/} {/* 直接把 children 作为 trigger,radix 锚定 children 本身的位置(避免多包一层 0 尺寸 span)*/}
...@@ -28,14 +31,21 @@ export function PersonaFeatureHover({ ...@@ -28,14 +31,21 @@ export function PersonaFeatureHover({
<HoverCardContent align="end" sideOffset={6} className="w-80 p-3 text-[11.5px]"> <HoverCardContent align="end" sideOffset={6} className="w-80 p-3 text-[11.5px]">
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-baseline justify-between border-b border-slate-200 pb-1.5"> <div className="flex items-baseline justify-between border-b border-slate-200 pb-1.5">
<span className="text-[13px] font-semibold text-slate-900">{meta.title}</span> <span className="text-[13px] font-semibold text-slate-900">{meta?.title ?? featureKey}</span>
<span className="text-[10.5px] text-slate-500">{meta.subtitle}</span> {meta?.subtitle && <span className="text-[10.5px] text-slate-500">{meta.subtitle}</span>}
</div> </div>
{meta.formula && ( {/* 本患者实际取值(原 title 内容)*/}
{value && (
<div className="rounded bg-teal-50 px-2 py-1.5 text-[11px] text-teal-800 leading-relaxed">
{value}
</div>
)}
{meta?.formula && (
<div className="rounded bg-slate-50 px-2 py-1.5 font-mono text-[10.5px] text-slate-700 leading-relaxed"> <div className="rounded bg-slate-50 px-2 py-1.5 font-mono text-[10.5px] text-slate-700 leading-relaxed">
{meta.formula} {meta.formula}
</div> </div>
)} )}
{meta && (
<ul className="space-y-1 text-slate-600 leading-relaxed"> <ul className="space-y-1 text-slate-600 leading-relaxed">
{meta.rules.map((r, i) => ( {meta.rules.map((r, i) => (
<li key={i} className="flex gap-2"> <li key={i} className="flex gap-2">
...@@ -47,7 +57,8 @@ export function PersonaFeatureHover({ ...@@ -47,7 +57,8 @@ export function PersonaFeatureHover({
</li> </li>
))} ))}
</ul> </ul>
{meta.note && ( )}
{meta?.note && (
<p className="border-t border-slate-100 pt-1.5 text-[10.5px] text-slate-500 leading-relaxed"> <p className="border-t border-slate-100 pt-1.5 text-[10.5px] text-slate-500 leading-relaxed">
{meta.note} {meta.note}
</p> </p>
......
...@@ -43,7 +43,6 @@ import { HoverCard, HoverCardTrigger, HoverCardContent } from '@/components/ui/h ...@@ -43,7 +43,6 @@ import { HoverCard, HoverCardTrigger, HoverCardContent } from '@/components/ui/h
import { shortPersonaValueLabel } from './persona-display'; import { shortPersonaValueLabel } from './persona-display';
import { PersonaFeatureHover } from './persona-feature-hover'; import { PersonaFeatureHover } from './persona-feature-hover';
import { ReasonLine } from './reason-line'; import { ReasonLine } from './reason-line';
import { ChainSidebar } from './chain-viz';
import { ScriptView, type ScriptViewMode } from './script-viewer'; import { ScriptView, type ScriptViewMode } from './script-viewer';
import { OutcomeForm } from './outcome-form'; import { OutcomeForm } from './outcome-form';
import { Drawer, type DrawerKind } from './drawer'; import { Drawer, type DrawerKind } from './drawer';
...@@ -311,6 +310,8 @@ export function PlanDetailApp({ ...@@ -311,6 +310,8 @@ export function PlanDetailApp({
<aside className="min-h-0 flex flex-col gap-2.5 overflow-y-auto pr-1 h-full"> <aside className="min-h-0 flex flex-col gap-2.5 overflow-y-auto pr-1 h-full">
<IdentityCard <IdentityCard
patient={patient} patient={patient}
features={persona.features}
onOpenPersona={() => setDrawerOpen('persona')}
onOpenImage={() => onOpenImage={() =>
showToast('slate', '影像调阅', '跳转宿主页面') showToast('slate', '影像调阅', '跳转宿主页面')
} }
...@@ -322,20 +323,6 @@ export function PlanDetailApp({ ...@@ -322,20 +323,6 @@ export function PlanDetailApp({
visibleReasons={visibleReasons} visibleReasons={visibleReasons}
onOpenMedical={() => setDrawerOpen('medical')} onOpenMedical={() => setDrawerOpen('medical')}
/> />
<SidebarCard
title="患者信息 · 标签"
meta={`${persona.features.length} 个 · 更新于 ${fmtRel(persona.computedAt)}`}
action={
<button
onClick={() => setDrawerOpen('persona')}
className="text-[10.5px] text-teal-700 hover:underline"
>
详情 →
</button>
}
>
<PersonaTagCloud features={persona.features} />
</SidebarCard>
<KeyFactsCard <KeyFactsCard
patient={patient} patient={patient}
persona={persona} persona={persona}
...@@ -350,20 +337,7 @@ export function PlanDetailApp({ ...@@ -350,20 +337,7 @@ export function PlanDetailApp({
<RecallHistoryCard history={recallHistory} fmtRel={fmtRel} /> <RecallHistoryCard history={recallHistory} fmtRel={fmtRel} />
)} )}
{returnVisits.length > 0 && <ReturnVisitsCard visits={returnVisits} />} {returnVisits.length > 0 && <ReturnVisitsCard visits={returnVisits} />}
<SidebarCard {/* 治疗链卡片已隐藏(链已弃用;chains 仍传 drawer 备用)*/}
title="治疗链"
meta={`${chains.length} 条`}
action={
<button
onClick={() => setDrawerOpen('chain-detail')}
className="text-[10.5px] text-teal-700 hover:underline"
>
详情 →
</button>
}
>
<ChainSidebar chains={chains} />
</SidebarCard>
</aside> </aside>
} }
centerPane={ centerPane={
...@@ -950,10 +924,14 @@ function RecycleCountdown({ recycleAt }: { recycleAt: Date | null }) { ...@@ -950,10 +924,14 @@ function RecycleCountdown({ recycleAt }: { recycleAt: Date | null }) {
// ────────────────────────────────────────── // ──────────────────────────────────────────
function IdentityCard({ function IdentityCard({
patient, patient,
features,
onOpenPersona,
onOpenImage, onOpenImage,
onOpenProfile, onOpenProfile,
}: { }: {
patient: typeof mockPatient; patient: typeof mockPatient;
features: typeof mockPersona.features;
onOpenPersona: () => void;
onOpenImage: () => void; onOpenImage: () => void;
onOpenProfile: () => void; onOpenProfile: () => void;
}) { }) {
...@@ -1091,6 +1069,18 @@ function IdentityCard({ ...@@ -1091,6 +1069,18 @@ function IdentityCard({
</span> </span>
)} )}
</div> </div>
{/* 画像标签(原独立卡片并入此处)— hover 看"是什么 + 怎么算的" */}
{features.length > 0 && (
<div className="mt-2 pt-2 border-t border-slate-100">
<div className="mb-1 flex items-center justify-between">
<span className="text-[10px] font-medium text-slate-400">画像标签</span>
<button onClick={onOpenPersona} className="text-[10px] text-teal-700 hover:underline">
详情 →
</button>
</div>
<PersonaTagCloud features={features} />
</div>
)}
</div> </div>
</div> </div>
</section> </section>
...@@ -1652,14 +1642,13 @@ function PersonaTagCloud({ features }: { features: typeof mockPersona.features } ...@@ -1652,14 +1642,13 @@ function PersonaTagCloud({ features }: { features: typeof mockPersona.features }
const T = tone(f.tone); const T = tone(f.tone);
const short = shortPersonaValueLabel(f.value); const short = shortPersonaValueLabel(f.value);
return ( return (
<PersonaFeatureHover key={f.key} featureKey={f.key}> <PersonaFeatureHover key={f.key} featureKey={f.key} value={f.value}>
<span <span
className={cn( className={cn(
'inline-flex items-center gap-1 px-1.5 py-0.5 rounded ring-1 cursor-help max-w-full', 'inline-flex items-center gap-1 px-1.5 py-0.5 rounded ring-1 cursor-help max-w-full',
T.bg, T.bg,
T.ring, T.ring,
)} )}
title={f.value}
> >
<span className={cn('flex-none text-[10px] font-semibold', T.text)}>{f.label}</span> <span className={cn('flex-none text-[10px] font-semibold', T.text)}>{f.label}</span>
{short && short !== f.label && ( {short && short !== f.label && (
......
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