From 24df36a9b400eaa02bac1aee2d2179f8fabb91b4 Mon Sep 17 00:00:00 2001 From: Rick Ye Date: Tue, 8 Sep 2026 17:33:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=A1=E6=89=B9=E5=8D=A1=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E6=8A=BD=E5=B1=89=E5=BC=8F=E2=80=94=E2=80=94=E8=BE=93?= =?UTF-8?q?=E5=85=A5=E6=A1=86=E4=B8=8A=E6=96=B9=E6=BB=91=E5=87=BA=EF=BC=8C?= =?UTF-8?q?=E4=B8=80=E6=AC=A1=E4=B8=80=E5=BC=A0=EF=BC=8Cradio=20=E4=BF=9D?= =?UTF-8?q?=E7=95=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../renderer/src/components/ApprovalCard.tsx | 16 +- .../renderer/src/components/BottomSlot.tsx | 143 ++++++++++-------- 2 files changed, 87 insertions(+), 72 deletions(-) diff --git a/apps/desktop/src/renderer/src/components/ApprovalCard.tsx b/apps/desktop/src/renderer/src/components/ApprovalCard.tsx index c4158c3..1ee6abb 100644 --- a/apps/desktop/src/renderer/src/components/ApprovalCard.tsx +++ b/apps/desktop/src/renderer/src/components/ApprovalCard.tsx @@ -90,17 +90,23 @@ function Ico({ path, size = 14, sw = 2 }: { path: ReactNode; size?: number; sw?: const OPTION_APPROVE = 0 +export type ApprovalOutcome = 'approved' | 'rejected' + export function ApprovalCard({ request, + outcome, onResolve }: { request: ApprovalRequest + /** 外部已结算(卡片重挂后恢复结果态):给定时直接呈现结果胶囊,交互冻结 */ + outcome?: ApprovalOutcome onResolve: (id: string, approved: boolean) => void }): ReactNode { const [picked, setPicked] = useState(null) const [sent, setSent] = useState(null) const submitTimer = useRef | null>(null) const settled = useRef(false) + const settledNote = sent ?? (outcome === 'approved' ? '已允许执行' : outcome === 'rejected' ? '已拒绝' : null) useEffect( () => () => { @@ -110,7 +116,7 @@ export function ApprovalCard({ ) const settle = (approved: boolean, note: string): void => { - if (settled.current) return + if (settledNote) return settled.current = true setSent(note) if (submitTimer.current) clearTimeout(submitTimer.current) @@ -118,7 +124,7 @@ export function ApprovalCard({ } const pick = (index: number): void => { - if (sent) return + if (settledNote) return setPicked(index) // radio 自动前进:480ms 后提交(官方 single-choice 行为) if (submitTimer.current) clearTimeout(submitTimer.current) @@ -127,8 +133,8 @@ export function ApprovalCard({ }, 480) } - if (sent) { - const ok = sent === '已允许执行' + if (settledNote) { + const ok = settledNote === '已允许执行' return (
)} - {sent} + {settledNote}
) diff --git a/apps/desktop/src/renderer/src/components/BottomSlot.tsx b/apps/desktop/src/renderer/src/components/BottomSlot.tsx index bde63ce..711032d 100644 --- a/apps/desktop/src/renderer/src/components/BottomSlot.tsx +++ b/apps/desktop/src/renderer/src/components/BottomSlot.tsx @@ -1,41 +1,18 @@ import { useEffect, useRef, useState, type ReactNode } from 'react' import type { ApprovalRequest } from '@shared/types' -import { ApprovalCard } from './ApprovalCard' +import { ApprovalCard, type ApprovalOutcome } from './ApprovalCard' /** - * 底部插槽(抽屉):审批卡出现时从 Chatbox 位置升起替换输入框, - * 结算后输入框滑回。两行用 grid-template-rows 0fr↔1fr 过渡做真实高度动画, - * 交叉淡入淡出;Composer 始终挂载(草稿不丢)。已结算的卡片以结算胶囊态 - * 多停留约一秒再撤出,避免「啪」一下消失。 + * 底部插槽(审批抽屉):审批请求出现时,卡片从输入框上方(版面中部)像抽屉 + * 一样滑出,输入框保留可见可输入(Composer 始终挂载,草稿不丢)。 + * 一次只展示一张:队列中的后续请求在前一张结算后接入;结算后卡片以结果 + * 胶囊态停留约一秒,再下滑淡出收起。升起/收起均为 CSS 过渡。 */ -const ROWS_TRANSITION = 'grid-template-rows 400ms cubic-bezier(0.22, 1, 0.36, 1)' -const FADE_TRANSITION = 'opacity 220ms ease, transform 400ms cubic-bezier(0.22, 1, 0.36, 1)' +type Phase = 'pending' | 'settled' | 'closing' | 'hidden' -/** 跟踪从 requests 里消失的审批:缓存原件让卡片以结算态多停留一拍 */ -function useLeavingApprovals(requests: ApprovalRequest[]): ApprovalRequest[] { - const seenRef = useRef(new Map()) - for (const request of requests) seenRef.current.set(request.id, request) - const [leaving, setLeaving] = useState([]) - - useEffect(() => { - const gone: ApprovalRequest[] = [] - for (const [id, request] of seenRef.current) { - if (!requests.some((r) => r.id === id)) gone.push(request) - } - if (gone.length === 0) return - for (const g of gone) seenRef.current.delete(g.id) - setLeaving((cur) => [...cur.filter((l) => !gone.some((g) => g.id === l.id)), ...gone]) - const timers = gone.map((g) => - setTimeout(() => { - setLeaving((cur) => cur.filter((l) => l.id !== g.id)) - }, 1000) - ) - return () => timers.forEach(clearTimeout) - }, [requests]) - - return leaving -} +const SETTLED_MS = 900 +const CLOSE_MS = 240 export function BottomSlot({ approvals, @@ -46,48 +23,80 @@ export function BottomSlot({ onResolve: (id: string, approved: boolean) => void composer: ReactNode }): ReactNode { - const leaving = useLeavingApprovals(approvals) - const showApproval = approvals.length > 0 || leaving.length > 0 + const [shown, setShown] = useState(null) + const [phase, setPhase] = useState('hidden') + const outcomesRef = useRef(new Map()) + const timerRef = useRef(null) + + useEffect( + () => () => { + if (timerRef.current) window.clearTimeout(timerRef.current) + }, + [] + ) + + useEffect(() => { + const first = approvals[0] + if (first) { + // 新请求接入(含上一张结算中的队列换牌):立即切换为待决卡片 + if (timerRef.current) { + window.clearTimeout(timerRef.current) + timerRef.current = null + } + setShown((cur) => (cur?.id === first.id ? cur : first)) + setPhase('pending') + return + } + if (phase === 'pending' && shown) { + // 待决卡片刚结算且没有后续请求:胶囊态停留一拍,再下滑淡出收起 + setPhase('settled') + timerRef.current = window.setTimeout(() => { + setPhase('closing') + timerRef.current = window.setTimeout(() => { + timerRef.current = null + setShown(null) + setPhase('hidden') + }, CLOSE_MS) + }, SETTLED_MS) + } + }, [approvals, phase, shown]) + + const handleResolve = (id: string, approved: boolean): void => { + outcomesRef.current.set(id, approved ? 'approved' : 'rejected') + onResolve(id, approved) + } + + const visible = shown !== null && phase !== 'hidden' return ( -
- {/* Chatbox 行:审批升起时收起(高度 0 + 下沉淡出) */} -
-
+
+ {/* 审批抽屉:悬浮在输入框上方(版面中部),从输入框后缘滑出 */} +
+ {shown && (
- {composer} +
-
-
- - {/* 审批抽屉行:从输入框位置升起(高度 1fr + 上浮淡入) */} -
-
-
-
- {[...approvals, ...leaving.filter((l) => !approvals.some((a) => a.id === l.id))].map((request) => ( -
- -
- ))} -
-
-
+ )}
+ {composer} +
) }