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
*/
export function PersonaFeatureHover({
featureKey,
value,
children,
}: {
featureKey: string;
/// 该患者的实际取值(原生 title 内容并入 hovercard 顶部)
value?: string;
children: React.ReactNode;
}) {
const meta = ALGORITHMS[featureKey];
if (!meta) return <>{children}</>;
if (!meta && !value) return <>{children}</>;
return (
<HoverCard openDelay={150} closeDelay={80}>
{/* 直接把 children 作为 trigger,radix 锚定 children 本身的位置(避免多包一层 0 尺寸 span)*/}
......@@ -28,26 +31,34 @@ export function PersonaFeatureHover({
<HoverCardContent align="end" sideOffset={6} className="w-80 p-3 text-[11.5px]">
<div className="space-y-2">
<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-[10.5px] text-slate-500">{meta.subtitle}</span>
<span className="text-[13px] font-semibold text-slate-900">{meta?.title ?? featureKey}</span>
{meta?.subtitle && <span className="text-[10.5px] text-slate-500">{meta.subtitle}</span>}
</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">
{meta.formula}
</div>
)}
<ul className="space-y-1 text-slate-600 leading-relaxed">
{meta.rules.map((r, i) => (
<li key={i} className="flex gap-2">
<span className="text-slate-400 flex-none">·</span>
<span>
{r.label && <strong className="text-slate-800 mr-1">{r.label}</strong>}
{r.body}
</span>
</li>
))}
</ul>
{meta.note && (
{meta && (
<ul className="space-y-1 text-slate-600 leading-relaxed">
{meta.rules.map((r, i) => (
<li key={i} className="flex gap-2">
<span className="text-slate-400 flex-none">·</span>
<span>
{r.label && <strong className="text-slate-800 mr-1">{r.label}</strong>}
{r.body}
</span>
</li>
))}
</ul>
)}
{meta?.note && (
<p className="border-t border-slate-100 pt-1.5 text-[10.5px] text-slate-500 leading-relaxed">
{meta.note}
</p>
......
......@@ -43,7 +43,6 @@ import { HoverCard, HoverCardTrigger, HoverCardContent } from '@/components/ui/h
import { shortPersonaValueLabel } from './persona-display';
import { PersonaFeatureHover } from './persona-feature-hover';
import { ReasonLine } from './reason-line';
import { ChainSidebar } from './chain-viz';
import { ScriptView, type ScriptViewMode } from './script-viewer';
import { OutcomeForm } from './outcome-form';
import { Drawer, type DrawerKind } from './drawer';
......@@ -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">
<IdentityCard
patient={patient}
features={persona.features}
onOpenPersona={() => setDrawerOpen('persona')}
onOpenImage={() =>
showToast('slate', '影像调阅', '跳转宿主页面')
}
......@@ -322,20 +323,6 @@ export function PlanDetailApp({
visibleReasons={visibleReasons}
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
patient={patient}
persona={persona}
......@@ -350,20 +337,7 @@ export function PlanDetailApp({
<RecallHistoryCard history={recallHistory} fmtRel={fmtRel} />
)}
{returnVisits.length > 0 && <ReturnVisitsCard visits={returnVisits} />}
<SidebarCard
title="治疗链"
meta={`${chains.length} 条`}
action={
<button
onClick={() => setDrawerOpen('chain-detail')}
className="text-[10.5px] text-teal-700 hover:underline"
>
详情 →
</button>
}
>
<ChainSidebar chains={chains} />
</SidebarCard>
{/* 治疗链卡片已隐藏(链已弃用;chains 仍传 drawer 备用)*/}
</aside>
}
centerPane={
......@@ -950,10 +924,14 @@ function RecycleCountdown({ recycleAt }: { recycleAt: Date | null }) {
// ──────────────────────────────────────────
function IdentityCard({
patient,
features,
onOpenPersona,
onOpenImage,
onOpenProfile,
}: {
patient: typeof mockPatient;
features: typeof mockPersona.features;
onOpenPersona: () => void;
onOpenImage: () => void;
onOpenProfile: () => void;
}) {
......@@ -1091,6 +1069,18 @@ function IdentityCard({
</span>
)}
</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>
</section>
......@@ -1652,14 +1642,13 @@ function PersonaTagCloud({ features }: { features: typeof mockPersona.features }
const T = tone(f.tone);
const short = shortPersonaValueLabel(f.value);
return (
<PersonaFeatureHover key={f.key} featureKey={f.key}>
<PersonaFeatureHover key={f.key} featureKey={f.key} value={f.value}>
<span
className={cn(
'inline-flex items-center gap-1 px-1.5 py-0.5 rounded ring-1 cursor-help max-w-full',
T.bg,
T.ring,
)}
title={f.value}
>
<span className={cn('flex-none text-[10px] font-semibold', T.text)}>{f.label}</span>
{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