From 361daf8693880fd3c9d0c6c2fca33592f379a9a4 Mon Sep 17 00:00:00 2001 From: LanQin_ Date: Thu, 25 Jun 2026 15:50:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(mail):=20=E4=B8=BA=E4=BE=A7=E8=BE=B9?= =?UTF-8?q?=E6=A0=8F=E6=B7=BB=E5=8A=A0=E5=8F=B3=E9=94=AE=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 支持对侧边栏条目打开、刷新和新建文件夹。 - 为自定义文件夹增加置顶、上移、下移、置底操作。 --- apps/web/src/pages/mail.tsx | 121 +++++++++++++++++++++++++++++++++++- 1 file changed, 120 insertions(+), 1 deletion(-) diff --git a/apps/web/src/pages/mail.tsx b/apps/web/src/pages/mail.tsx index 0f0400a..c94b51d 100644 --- a/apps/web/src/pages/mail.tsx +++ b/apps/web/src/pages/mail.tsx @@ -67,6 +67,7 @@ type PendingConfirm = { title: string; description?: string; confirmText: string type MailNotificationState = { latestId: string; latestReceivedAt: string } type ComposeSendIntent = { title: string; description: string; confirmText: string; onConfirm: () => void } type MessageContextMenuState = { message: MailMessage; x: number; y: number } +type SidebarContextMenuState = { item: MailMenuItem; x: number; y: number } type FolderDropTarget = { key: string; edge: "before" | "after" | "end" } type MailMenuItem = | { type: "starred"; key: string; label: string; icon: React.ReactNode; count: number; order: number } @@ -113,6 +114,7 @@ export function MailPage() { const [labelEditMode, setLabelEditMode] = React.useState(false) const [newLabelEditing, setNewLabelEditing] = React.useState(false) const [messageContextMenu, setMessageContextMenu] = React.useState(null) + const [sidebarContextMenu, setSidebarContextMenu] = React.useState(null) const [folderDialogOpen, setFolderDialogOpen] = React.useState(false) const [draggingFolderId, setDraggingFolderId] = React.useState("") const [folderDropTarget, setFolderDropTarget] = React.useState(null) @@ -676,6 +678,20 @@ export function MailPage() { function closeMessageContextMenu() { setMessageContextMenu(null) } + function openSidebarContextMenu(event: React.MouseEvent, item: MailMenuItem) { + event.preventDefault() + event.stopPropagation() + setSidebarContextMenu({ item, x: event.clientX, y: event.clientY }) + } + function closeSidebarContextMenu() { + setSidebarContextMenu(null) + } + function activateSidebarItem(item: MailMenuItem) { + if (item.type === "starred") openStarred() + else if (item.type === "scheduled") openScheduled() + else if (item.type === "sendQueue") openSendQueue() + else openFolder(item.folderName) + } function reorderCustomFolder(draggedId: string, target: FolderDropTarget) { if (!canOrganizeMail || reorderFolders.isPending) return const foldersByID = new Map((folders.data?.items || []).map((item) => [item.id, item])) @@ -704,6 +720,23 @@ export function MailPage() { const nextFolders = assignCustomFolderOrders(nextMenu) reorderFolders.mutate(nextFolders) } + function moveSidebarFolder(item: MailMenuItem, action: "top" | "up" | "down" | "bottom") { + if (item.type !== "folder" || !item.custom) return + const currentIndex = mailMenuItems.findIndex((entry) => entry.key === item.key) + if (currentIndex < 0) return + if (action === "top") { + reorderCustomFolder(item.folderId, { key: mailMenuItems[0]?.key || "__end__", edge: "before" }) + return + } + if (action === "bottom") { + reorderCustomFolder(item.folderId, { key: "__end__", edge: "end" }) + return + } + const targetIndex = action === "up" ? currentIndex - 1 : currentIndex + 1 + const target = mailMenuItems[targetIndex] + if (!target) return + reorderCustomFolder(item.folderId, { key: target.key, edge: action === "up" ? "before" : "after" }) + } function handleFolderDragStart(event: React.DragEvent, item: MailMenuItem) { if (item.type !== "folder" || !item.custom || sidebarCollapsed || !canOrganizeMail) return event.dataTransfer.effectAllowed = "move" @@ -875,6 +908,7 @@ export function MailPage() { onDragLeave={() => { if (folderDropTarget?.key === item.key) setFolderDropTarget(null) }} onDrop={(event) => handleFolderDrop(event, item)} onDragEnd={clearFolderDragState} + onContextMenu={(event) => openSidebarContextMenu(event, item)} > item.type === "starred" ? openStarred() : item.type === "scheduled" ? openScheduled() : item.type === "sendQueue" ? openSendQueue() : openFolder(item.folderName)} + onClick={() => activateSidebarItem(item)} > {item.icon} {!sidebarCollapsed && {item.label}} @@ -1241,6 +1275,28 @@ export function MailPage() { active ? removeLabel.mutate({ id: message.id, labelId: label.id }) : addLabel.mutate({ id: message.id, label }) }} /> + { + closeSidebarContextMenu() + activateSidebarItem(item) + }} + onRefresh={() => { + closeSidebarContextMenu() + void refreshMailData() + }} + onCreateFolder={() => { + closeSidebarContextMenu() + setFolderDialogOpen(true) + }} + onMove={(item, action) => { + closeSidebarContextMenu() + moveSidebarFolder(item, action) + }} + /> void; onOpen: (item: MailMenuItem) => void; onRefresh: () => void; onCreateFolder: () => void; onMove: (item: MailMenuItem, action: "top" | "up" | "down" | "bottom") => void }) { + React.useEffect(() => { + if (!state) return + const close = () => onClose() + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") onClose() + } + window.addEventListener("pointerdown", close) + window.addEventListener("resize", close) + window.addEventListener("scroll", close, true) + window.addEventListener("keydown", onKeyDown) + return () => { + window.removeEventListener("pointerdown", close) + window.removeEventListener("resize", close) + window.removeEventListener("scroll", close, true) + window.removeEventListener("keydown", onKeyDown) + } + }, [state, onClose]) + + if (!state) return null + const item = state.item + const customFolder = item.type === "folder" && item.custom + const position = contextMenuPosition(state.x, state.y) + return ( +
event.stopPropagation()} + onContextMenu={(event) => event.preventDefault()} + role="menu" + > + + + {canOrganize && ( + + )} + {canOrganize && customFolder && ( + <> +
+ + + + + + )} +
+ ) +} + function MessageContextMenu({ state, labels, folders, canSend, canOrganize, canManageLabels, labelPending, onClose, onAction, onMoveToFolder, onToggleLabel }: { state: MessageContextMenuState | null; labels: MailLabel[]; folders: MailFolder[]; canSend: boolean; canOrganize: boolean; canManageLabels: boolean; labelPending: boolean; onClose: () => void; onAction: (action: "open" | "reply" | "forward" | "read" | "star" | "archive" | "trash" | "spam" | "delete", message: MailMessage) => void; onMoveToFolder: (message: MailMessage, folderName: string) => void; onToggleLabel: (message: MailMessage, label: MailLabel) => void }) { React.useEffect(() => { if (!state) return