Commit d0a9f5f4 by luoqi

fix(助手): 窗口 400 → 480,确认单汇总行改成可换行

① 窗口宽度 400 → 480(产品:「调宽一点」)。️ `openAt` 里的 480 与 className 的
   `w-[480px]` 是一对,Tailwind 任意值必须是字面量、插不进常量,改一处要改两处;
   顺带把注释里当年按 400 写的依据一并同步(下一个人不会再按 400 做决定)。

② 确认单汇总行**没做响应式**:`flex items-center` 不换行,窄窗里
   「共 N 人 · 拟分 X · 待分配 Y」+ 角标 + 时效下拉挤在一行 ——
   实测「已移除 1」被压成**竖排单字**、时效下拉顶出卡片边界。
   ⇒ 改 `flex-wrap` + `gap-y-1`;两个角标补 `flex-none whitespace-nowrap`
   (不锁就会被压到零宽、中文按单字折行,那正是竖排的成因);
   时效那组补 whitespace-nowrap。`ml-auto` 换行后仍按行推到右端,不受影响。
   同样的毛病在"已分配 + 撤销三个数"那一行,一并加了 wrap。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
parent c20339c4
...@@ -57,7 +57,7 @@ import { cn, formatGender } from '@/lib/utils'; ...@@ -57,7 +57,7 @@ import { cn, formatGender } from '@/lib/utils';
* 上面四件事都是对**已经算好的结果**做局部修正,不产生新的人群。 * 上面四件事都是对**已经算好的结果**做局部修正,不产生新的人群。
* *
* ── 布局 ──────────────────────────────────────────────── * ── 布局 ────────────────────────────────────────────────
* 助手窗只有 400px 宽(最大化后更宽),所以**不用 table**(横向撑爆),用卡片列表; * 助手窗只有 480px 宽(2026-08-11 从 400 调宽;最大化后更宽),所以**不用 table**(横向撑爆),用卡片列表;
* 患者明细按客服折叠,展开才看 —— 兼顾"尽明细"与"一眼可确认"。 * 患者明细按客服折叠,展开才看 —— 兼顾"尽明细"与"一眼可确认"。
*/ */
...@@ -94,7 +94,7 @@ const OP_ZH: Record<SheetEditOp['action'], string> = { ...@@ -94,7 +94,7 @@ const OP_ZH: Record<SheetEditOp['action'], string> = {
* 患者姓名 —— **点一下复制**(2026-08-06 产品加)。 * 患者姓名 —— **点一下复制**(2026-08-06 产品加)。
* *
* ⚠️ 主管审这批人时常要把名字贴到别处(问医生、查宿主档案、发消息), * ⚠️ 主管审这批人时常要把名字贴到别处(问医生、查宿主档案、发消息),
* 在一个 400px 的列表里手动选中一个中文名很难选准。 * 在一个 480px 的列表里手动选中一个中文名很难选准。
* ⚠️ `stopPropagation` 是必须的:这一行同时是**折叠开关的兄弟**且整行可拖, * ⚠️ `stopPropagation` 是必须的:这一行同时是**折叠开关的兄弟**且整行可拖,
* 不拦住会连带触发别的行为。 * 不拦住会连带触发别的行为。
* ⚠️ `draggable={false}`:不加的话按住名字拖会被浏览器当成拖文本,把整行的拖拽吃掉。 * ⚠️ `draggable={false}`:不加的话按住名字拖会被浏览器当成拖文本,把整行的拖拽吃掉。
...@@ -283,7 +283,7 @@ export function AssignmentConfirmSheet({ ...@@ -283,7 +283,7 @@ export function AssignmentConfirmSheet({
* 批次福利(T4)—— 挂在**批次**上不挂个人,是**话术勾子 + 归因标签**。 * 批次福利(T4)—— 挂在**批次**上不挂个人,是**话术勾子 + 归因标签**。
* v1 不核销、不接宿主卡券:落库进 `attributes.benefit.text`,生成话术时作为事实输入进 prompt。 * v1 不核销、不接宿主卡券:落库进 `attributes.benefit.text`,生成话术时作为事实输入进 prompt。
* ⚠️ 卡片上**只读**:文案由主管口述、助手写进来(set_benefit)—— * ⚠️ 卡片上**只读**:文案由主管口述、助手写进来(set_benefit)——
* 在 400px 的卡片里放一个自由文本框,主管十有八九会写成一段带承诺的话, * 在 480px 的卡片里放一个自由文本框,主管十有八九会写成一段带承诺的话,
* 而福利话术有硬护栏(不得追加条件/期限/承诺),那道闸在生成侧,不在这里。 * 而福利话术有硬护栏(不得追加条件/期限/承诺),那道闸在生成侧,不在这里。
*/ */
const [benefit, setBenefit] = useState<string>(''); const [benefit, setBenefit] = useState<string>('');
...@@ -959,8 +959,15 @@ export function AssignmentConfirmSheet({ ...@@ -959,8 +959,15 @@ export function AssignmentConfirmSheet({
onDrop={stopAutoScroll} onDrop={stopAutoScroll}
className="overflow-hidden border-brand-100 text-[12px] shadow-sm" className="overflow-hidden border-brand-100 text-[12px] shadow-sm"
> >
{/* ① 汇总 */} {/*
<div className="flex items-center gap-2 border-b bg-brand-50/50 px-3 py-2"> ① 汇总
🔴 **必须 `flex-wrap`**(2026-08-11 产品指出):窗口窄的时候这一行装不下
「共 N 人 · 拟分 X · 待分配 Y」+ 角标 + 时效下拉,不换行就只能挤 ——
实测「已移除 1」被压成**竖排单字**,时效下拉顶出卡片边界。
⚠️ 换行之后 `ml-auto` 仍然有效(它按**行**推到右端),时效自己落到第二行右侧。
⚠️ `gap-y-1` 不能省:换行后两行会贴在一起。
*/}
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 border-b bg-brand-50/50 px-3 py-2">
<Users className="h-3.5 w-3.5 flex-none text-brand-600" /> <Users className="h-3.5 w-3.5 flex-none text-brand-600" />
{/* {/*
⚠️ 有待分配时必须**先给总数**,再拆两半。 ⚠️ 有待分配时必须**先给总数**,再拆两半。
...@@ -970,7 +977,7 @@ export function AssignmentConfirmSheet({ ...@@ -970,7 +977,7 @@ export function AssignmentConfirmSheet({
*/} */}
<span className="font-semibold text-slate-800"> <span className="font-semibold text-slate-800">
{pendingList.length > 0 ? ( {pendingList.length > 0 ? (
// ⚠️ 每段各自 nowrap:400px 的面板里必然换行,不锁的话会断成「待分配 3 / 9」, // ⚠️ 每段各自 nowrap:面板里必然换行,不锁的话会断成「待分配 3 / 9」,
// 数字被劈成两行 —— 断要断在「·」上,不能断在数字中间。 // 数字被劈成两行 —— 断要断在「·」上,不能断在数字中间。
<> <>
<span className="whitespace-nowrap">共 {items.length + pendingList.length} 人</span> <span className="whitespace-nowrap">共 {items.length + pendingList.length} 人</span>
...@@ -987,13 +994,18 @@ export function AssignmentConfirmSheet({ ...@@ -987,13 +994,18 @@ export function AssignmentConfirmSheet({
</> </>
)} )}
</span> </span>
{/* ⚠️ 角标一律 `flex-none whitespace-nowrap`:不锁的话 flex 会把它们压到零宽,
中文再按**单字**折行(那正是「已移除 1」变成竖排的原因)。 */}
{dropped.size > 0 && ( {dropped.size > 0 && (
<Badge variant="secondary" className="text-[10px] font-normal"> <Badge variant="secondary" className="flex-none whitespace-nowrap text-[10px] font-normal">
已移除 {dropped.size} 已移除 {dropped.size}
</Badge> </Badge>
)} )}
{sheet.unplaced > 0 && ( {sheet.unplaced > 0 && (
<Badge variant="secondary" className="bg-amber-100 text-[10px] font-normal text-amber-700"> <Badge
variant="secondary"
className="flex-none whitespace-nowrap bg-amber-100 text-[10px] font-normal text-amber-700"
>
{sheet.unplaced} 人分不下去 {sheet.unplaced} 人分不下去
</Badge> </Badge>
)} )}
...@@ -1011,7 +1023,7 @@ export function AssignmentConfirmSheet({ ...@@ -1011,7 +1023,7 @@ export function AssignmentConfirmSheet({
放在底部微调区会让人以为它跟下面的逐条时效是一回事。 放在底部微调区会让人以为它跟下面的逐条时效是一回事。
⚠️ 文案是「N 天**后自动退回**」而不是光一个「时效」—— ⚠️ 文案是「N 天**后自动退回**」而不是光一个「时效」——
主管要知道到期会发生什么(单子回池),不然这个数对他没有意义。 */} 主管要知道到期会发生什么(单子回池),不然这个数对他没有意义。 */}
<div className="ml-auto flex flex-none items-center gap-1 text-[10.5px] text-slate-500"> <div className="ml-auto flex flex-none items-center gap-1 whitespace-nowrap text-[10.5px] text-slate-500">
<DaySelect value={expiresInDays} disabled={readOnly} onChange={setExpiresInDays} /> <DaySelect value={expiresInDays} disabled={readOnly} onChange={setExpiresInDays} />
<span>后自动退回</span> <span>后自动退回</span>
</div> </div>
...@@ -1423,7 +1435,8 @@ export function AssignmentConfirmSheet({ ...@@ -1423,7 +1435,8 @@ export function AssignmentConfirmSheet({
// 助手撤的:本地没有三个数,只说状态 —— 具体数字助手在消息里已经原话转述过 // 助手撤的:本地没有三个数,只说状态 —— 具体数字助手在消息里已经原话转述过
<span className="text-[11.5px] font-medium text-slate-500">这批已撤销</span> <span className="text-[11.5px] font-medium text-slate-500">这批已撤销</span>
) : done ? ( ) : done ? (
<div className="flex items-center gap-2"> // ⚠️ 同汇总行:撤销结果那三个数在窄窗里放不下,不 wrap 就会挤掉
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
<span className="flex items-center gap-1.5 text-[11.5px] font-medium text-emerald-700"> <span className="flex items-center gap-1.5 text-[11.5px] font-medium text-emerald-700">
<Check className="h-3.5 w-3.5" /> <Check className="h-3.5 w-3.5" />
已分配 已分配
......
...@@ -509,7 +509,7 @@ function MessageView({ ...@@ -509,7 +509,7 @@ function MessageView({
.reverse() .reverse()
.find((b): b is Extract<Block, { kind: 'tool' }> => b.kind === 'tool' && b.step.status === 'running'); .find((b): b is Extract<Block, { kind: 'tool' }> => b.kind === 'tool' && b.step.status === 'running');
const hintLabel = runningTool ? toolMeta(runningTool.step.tool).running : '生成中'; const hintLabel = runningTool ? toolMeta(runningTool.step.tool).running : '生成中';
// ⛔ 助手消息**不带左侧头像**(2026-08-03 走查):助手窗只有 400px, // ⛔ 助手消息**不带左侧头像**(2026-08-03 走查):助手窗只有 480px,
// 每条都挂一个 28px 的圆头像 + 间距 = 白白吃掉一成宽度,而"这段是助手说的" // 每条都挂一个 28px 的圆头像 + 间距 = 白白吃掉一成宽度,而"这段是助手说的"
// 靠气泡形态(用户消息右对齐深底、助手左对齐无底)已经分得清了。 // 靠气泡形态(用户消息右对齐深底、助手左对齐无底)已经分得清了。
/** /**
......
...@@ -34,7 +34,14 @@ export function AssistantWidget() { ...@@ -34,7 +34,14 @@ export function AssistantWidget() {
const [winRect, setWinRect] = useState<{ x: number; y: number; w: number; h: number } | null>(null); const [winRect, setWinRect] = useState<{ x: number; y: number; w: number; h: number } | null>(null);
const openAt = (fab: DOMRect) => { const openAt = (fab: DOMRect) => {
const W = Math.min(400, window.innerWidth - 16); /**
* ⚠️ **这个 480 与下面 className 里的 `w-[480px]` 是一对,改一处要改两处**
* (Tailwind 的任意值必须是静态字面量,没法从这里的常量插进去)。
* 🔴 400 → 480(2026-08-11 产品:「窗口调宽一点」):确认单在 400px 里排不开 ——
* 汇总行的「已移除 1」被挤成竖排单字,时效下拉顶出边界。
* ⛔ 别再往回收:那张卡是这个窗口的**主要内容**,窗口要迁就它。
*/
const W = Math.min(480, window.innerWidth - 16);
const H = Math.min(620, Math.round(window.innerHeight * 0.78)); const H = Math.min(620, Math.round(window.innerHeight * 0.78));
// 水平:钮在屏幕右半 → 窗口右缘对齐钮右缘;左半 → 左缘对齐 // 水平:钮在屏幕右半 → 窗口右缘对齐钮右缘;左半 → 左缘对齐
let x = fab.left + fab.width / 2 > window.innerWidth / 2 ? fab.right - W : fab.left; let x = fab.left + fab.width / 2 > window.innerWidth / 2 ? fab.right - W : fab.left;
...@@ -96,7 +103,8 @@ export function AssistantWidget() { ...@@ -96,7 +103,8 @@ export function AssistantWidget() {
'transition-[opacity,transform] duration-150', 'transition-[opacity,transform] duration-150',
maximized maximized
? 'inset-3' // 最大化:填满窗口(留 12px 边距),不写死宽高,随窗口自适应 ? 'inset-3' // 最大化:填满窗口(留 12px 边距),不写死宽高,随窗口自适应
: cn(!winRect && 'bottom-4 right-4', 'h-[620px] max-h-[78vh] w-[400px] max-w-[calc(100vw-2rem)]'), : // ⚠️ `w-[480px]` 与上面 openAt 里的 480 是一对,改一处要改两处
cn(!winRect && 'bottom-4 right-4', 'h-[620px] max-h-[78vh] w-[480px] max-w-[calc(100vw-2rem)]'),
open ? 'translate-y-0 opacity-100' : 'pointer-events-none invisible translate-y-2 opacity-0', open ? 'translate-y-0 opacity-100' : 'pointer-events-none invisible translate-y-2 opacity-0',
)} )}
> >
......
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