style: 任务FX工作区操作区从右下角浮层移入左侧任务栏底部

- TaskFloatingActions 重命名为 TaskActionsPanel,挂到对话列表底部
- 不再 absolute 覆盖右侧聊天输入框;编辑/下一步/清除记录改为操作卡片
- 左侧栏样式增强(标题/会话数/选中态高亮)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
AdministratorandClaude Opus 4.8 committed 2026-06-17 17:37:35 +08:00
1 parent a2bb8c80a2
commit 0dca637645
1 file changed
+76 -41
@@ -2,7 +2,7 @@
* 任务深链(rwfx / 任务FX)的「任务工作区」:一个任务挂多个对话(1:N)。 * 任务深链(rwfx / 任务FX)的「任务工作区」:一个任务挂多个对话(1:N)。
* *
* 左侧 = 该任务下的对话列表(按 metadata.taskId 过滤)+「新建对话」; * 左侧 = 该任务下的对话列表(按 metadata.taskId 过滤)+「新建对话」;
* 右下角 = 常驻浮层红色按钮(编辑 / 下一步 / 清除任务记录),absolute 定位,时刻可见、不被内容挤走; * 右下角 = 常驻浮动操作条(编辑 / 下一步 / 清除任务记录),absolute 定位,时刻可见、不被内容挤走;
* 右侧 = <Outlet/> 渲染选中的对话(AgentChatPage,新建对话顶部带目的树闸门面板)。 * 右侧 = <Outlet/> 渲染选中的对话(AgentChatPage,新建对话顶部带目的树闸门面板)。
* *
* - 进入 chats 索引:有历史 → 跳最新;无历史 → 跳 new(=新建,走闸门)。 * - 进入 chats 索引:有历史 → 跳最新;无历史 → 跳 new(=新建,走闸门)。
@@ -61,44 +61,69 @@ function TaskConversationsList({ taskId, chatsBase }: { taskId: string; chatsBas
const currentThreadId = useMemo(() => parseThreadId(pathname, chatsBase), [pathname, chatsBase]); const currentThreadId = useMemo(() => parseThreadId(pathname, chatsBase), [pathname, chatsBase]);
return ( return (
<div className="border-border/70 bg-card/40 flex w-60 shrink-0 flex-col border-r"> <div className="bg-background/85 border-border/70 flex w-64 shrink-0 flex-col border-r shadow-[12px_0_30px_-30px_rgba(15,23,42,0.45)] backdrop-blur-xl dark:border-[#1b477e]/70 dark:bg-[#061b35]/82">
<div className="border-border/70 border-b p-2"> <div className="border-border/70 border-b p-3 dark:border-[#1b477e]/70">
<Button size="sm" className="w-full" onClick={() => navigate(`${chatsBase}/new`)}> <div className="mb-2 flex items-center justify-between gap-2">
<div className="min-w-0">
<div className="text-foreground text-sm font-semibold">任务对话</div>
<div className="text-muted-foreground mt-0.5 text-xs tabular-nums">
{isLoading ? "同步中" : `${threads.length} 个会话`}
</div>
</div>
</div>
<Button
size="sm"
className="h-9 w-full bg-[var(--color-main)] text-white shadow-sm hover:bg-[var(--brand-950)]"
onClick={() => navigate(`${chatsBase}/new`)}
>
<PlusSquare className="h-4 w-4" /> 新建对话 <PlusSquare className="h-4 w-4" /> 新建对话
</Button> </Button>
</div> </div>
<div className="min-h-0 flex-1 overflow-y-auto p-1.5"> <div className="min-h-0 flex-1 overflow-y-auto p-2">
{isLoading ? ( {isLoading ? (
<div className="text-muted-foreground p-2 text-sm">加载中…</div> <div className="text-muted-foreground rounded-md border border-dashed p-3 text-center text-sm dark:border-[#1b477e]/60">
加载中…
</div>
) : threads.length === 0 ? ( ) : threads.length === 0 ? (
<div className="text-muted-foreground p-2 text-sm">暂无对话</div> <div className="text-muted-foreground rounded-md border border-dashed p-3 text-center text-sm dark:border-[#1b477e]/60">
暂无对话
</div>
) : ( ) : (
threads.map((thread) => ( threads.map((thread) => (
<button <button
key={thread.thread_id} key={thread.thread_id}
onClick={() => navigate(`${chatsBase}/${thread.thread_id}`)} onClick={() => navigate(`${chatsBase}/${thread.thread_id}`)}
className={cn( className={cn(
"hover:bg-accent flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-sm", "group relative flex w-full items-center gap-2 rounded-md px-2.5 py-2 text-left text-sm transition-colors",
thread.thread_id === currentThreadId && "bg-accent font-medium", "hover:bg-[var(--bg-color)] dark:hover:bg-[#0d2a4d]/85",
thread.thread_id === currentThreadId &&
"bg-[var(--bg-color)] font-medium text-[var(--color-main)] shadow-sm dark:bg-[#0d2a4d] dark:text-[#9fe3ff]",
)} )}
title={desensitize(titleOfThread(thread, "新对话"))} title={desensitize(titleOfThread(thread, "新对话"))}
> >
<MessageSquare className="h-3.5 w-3.5 shrink-0" /> <span
className={cn(
"absolute inset-y-2 left-0 w-0.5 rounded-full bg-transparent transition-colors",
thread.thread_id === currentThreadId && "bg-[var(--color-main)] dark:bg-[#64cdf5]",
)}
/>
<MessageSquare className="text-muted-foreground group-hover:text-foreground h-3.5 w-3.5 shrink-0" />
<span className="truncate">{desensitize(titleOfThread(thread, "新对话"))}</span> <span className="truncate">{desensitize(titleOfThread(thread, "新对话"))}</span>
</button> </button>
)) ))
)} )}
</div> </div>
<TaskActionsPanel taskId={taskId} />
</div> </div>
); );
} }
/** /**
* 右下角常驻红色操作浮层:编辑 / 下一步 / 清除任务记录。 * 左侧任务栏底部操作区:编辑 / 下一步 / 清除任务记录。
* absolute 定位在工作区右侧面板内,z-40,时刻可见、不被聊天内容挤走。 * 跟随任务对话面板布局,不再覆盖右侧聊天输入框。
*/ */
function TaskFloatingActions({ taskId }: { taskId: string }) { function TaskActionsPanel({ taskId }: { taskId: string }) {
const [clearOpen, setClearOpen] = useState(false); const [clearOpen, setClearOpen] = useState(false);
const [clearing, setClearing] = useState(false); const [clearing, setClearing] = useState(false);
@@ -123,32 +148,43 @@ function TaskFloatingActions({ taskId }: { taskId: string }) {
return ( return (
<> <>
<div className="pointer-events-none absolute bottom-4 right-4 z-40 flex flex-col items-end gap-2"> <div className="border-border/70 shrink-0 border-t p-3 dark:border-[#1b477e]/70">
<Button <div className="rounded-md border border-[var(--card-border)] bg-background/80 p-2 shadow-sm dark:border-[#2b8ffe]/45 dark:bg-[#0b2546]/72">
size="sm" <div className="text-muted-foreground mb-2 px-1 text-xs font-medium">
variant="destructive" 任务操作
className="pointer-events-auto shadow-lg" </div>
onClick={() => openExternal(buildTreemapEditUrl(taskId), "编辑")} <div className="grid grid-cols-2 gap-2">
> <Button
<ExternalLink className="h-3.5 w-3.5" /> 编辑 size="sm"
</Button> variant="outline"
<Button className="h-8 border-[var(--card-border)] bg-background/80 text-[var(--color-main)] hover:bg-[var(--bg-color)] hover:text-[var(--color-main)] dark:border-[#2b8ffe]/50 dark:bg-[#0b2546]/80 dark:text-[#bde9ff] dark:hover:bg-[#10375f]"
size="sm" onClick={() => openExternal(buildTreemapEditUrl(taskId), "编辑")}
variant="destructive" disabled={!taskId}
className="pointer-events-auto shadow-lg" title="编辑任务"
onClick={() => openExternal(buildTreemapNextUrl(taskId), "下一步")} >
> <ExternalLink className="h-3.5 w-3.5" /> 编辑
<ArrowRight className="h-3.5 w-3.5" /> 下一步 </Button>
</Button> <Button
<Button size="sm"
size="sm" className="h-8 bg-[var(--color-main)] text-white shadow-[0_8px_18px_-12px_var(--color-main)] hover:bg-[var(--brand-950)]"
variant="destructive" onClick={() => openExternal(buildTreemapNextUrl(taskId), "下一步")}
className="pointer-events-auto shadow-lg" disabled={!taskId}
onClick={() => setClearOpen(true)} title="进入下一步"
disabled={!taskId} >
> 下一步 <ArrowRight className="h-3.5 w-3.5" />
<Trash2 className="h-3.5 w-3.5" /> 清除任务记录 </Button>
</Button> <Button
size="sm"
variant="ghost"
className="col-span-2 h-8 text-muted-foreground hover:bg-destructive/10 hover:text-destructive dark:text-[#ccd2da] dark:hover:bg-[#5c0011]/35"
onClick={() => setClearOpen(true)}
disabled={!taskId}
title="清除任务记录"
>
<Trash2 className="h-3.5 w-3.5" /> 清除记录
</Button>
</div>
</div>
</div> </div>
<Dialog open={clearOpen} onOpenChange={(o) => !clearing && setClearOpen(o)}> <Dialog open={clearOpen} onOpenChange={(o) => !clearing && setClearOpen(o)}>
@@ -173,7 +209,7 @@ function TaskFloatingActions({ taskId }: { taskId: string }) {
); );
} }
/** 任务工作区布局:左列表 + 右 Outlet + 右下角常驻红色操作浮层。 */ /** 任务工作区布局:左列表 + 右 Outlet。 */
export function TaskFxWorkspace({ taskBase }: { taskBase: string }) { export function TaskFxWorkspace({ taskBase }: { taskBase: string }) {
const { taskId, chatsBase } = useChatsBase(taskBase); const { taskId, chatsBase } = useChatsBase(taskBase);
// 预拉配置,保证「编辑/下一步」的同步 URL 构造能读到缓存。 // 预拉配置,保证「编辑/下一步」的同步 URL 构造能读到缓存。
@@ -185,7 +221,6 @@ export function TaskFxWorkspace({ taskBase }: { taskBase: string }) {
<TaskConversationsList taskId={taskId} chatsBase={chatsBase} /> <TaskConversationsList taskId={taskId} chatsBase={chatsBase} />
<div className="relative min-h-0 min-w-0 flex-1"> <div className="relative min-h-0 min-w-0 flex-1">
<Outlet /> <Outlet />
<TaskFloatingActions taskId={taskId} />
</div> </div>
</div> </div>
); );