diff --git a/.github/release-notes/v1.2.30.md b/.github/release-notes/v1.2.30.md new file mode 100644 index 0000000..71bae37 --- /dev/null +++ b/.github/release-notes/v1.2.30.md @@ -0,0 +1,9 @@ +- 修复邮箱侧栏切换目录后,紧凑列表顶部仍可能显示上一个目录名称的问题;收件箱、草稿箱、已发送、已归档、已删除、星标邮件、垃圾邮件、自定义文件夹和标签现在会与当前列表保持一致。 +- 切换邮箱目录时按当前视图重新生成列表区域,避免星标邮件等旧标题或旧操作状态残留到其他目录。 +- 修复“已删除”目录批量删除仍执行“移入已删除”的问题;现在会永久删除所选邮件,并在操作前显示不可恢复的二次确认。 +- 区分普通目录与“已删除”的删除语义:普通目录显示“移入已删除”,已删除目录显示“永久删除”。 +- 统一批量工具栏、邮件详情、右键菜单和列表快捷按钮的删除名称、确认文案与完成提示,避免显示“已移动”但实际执行删除等文字不一致。 +- 优化批量归档、移回收件箱、移入垃圾邮件和移入已删除后的提示,准确显示目标目录及处理数量。 +- 后台邮箱管理中将账号默认邮箱固定置顶,其余子邮箱继续按 A-Z / 0-9 排序,方便快速识别账号主邮箱。 +- 修正邮箱管理表头与账号行的网格结构,“权限管理”和“子邮箱”列标题及内容统一居中对齐。 +- 保留默认邮箱不可停用、不可删除保护;普通子邮箱继续支持启用、停用和二次确认删除。 diff --git a/VERSION b/VERSION index 550c9e9..d20e748 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.29 +1.2.30 diff --git a/apps/web/src/pages/admin.tsx b/apps/web/src/pages/admin.tsx index 003a7c4..aeb00c6 100644 --- a/apps/web/src/pages/admin.tsx +++ b/apps/web/src/pages/admin.tsx @@ -703,8 +703,12 @@ function MailboxesSection({ mailboxes, users, domains }: { mailboxes: MailboxTyp const remove = useMutation({ mutationFn: api.deleteMailbox, onSuccess: () => { setPendingConfirm(null); invalidateAdmin(qc); toast({ title: "邮箱已删除" }) }, onError: (e) => toast({ title: "删除失败", description: e.message }) }) const keyword = query.trim().toLowerCase() const knownOwnerIDs = new Set(users.map((item) => item.id)) + const compareMailboxes = (left: MailboxType, right: MailboxType) => { + if (left.primary !== right.primary) return left.primary ? -1 : 1 + return left.address.localeCompare(right.address, "en", { sensitivity: "base" }) + } const mailboxGroups: Array<{ owner?: AdminUser; mailboxes: MailboxType[] }> = [ - ...users.slice().sort(compareAdminUsers).map((owner) => ({ owner, mailboxes: mailboxes.filter((mailbox) => mailbox.userId === owner.id).sort((left, right) => left.address.localeCompare(right.address, "en")) })), + ...users.slice().sort(compareAdminUsers).map((owner) => ({ owner, mailboxes: mailboxes.filter((mailbox) => mailbox.userId === owner.id).sort(compareMailboxes) })), ...mailboxes.filter((mailbox) => !knownOwnerIDs.has(mailbox.userId)).map((mailbox) => ({ owner: undefined, mailboxes: [mailbox] })), ] .filter((group) => group.mailboxes.length > 0) @@ -727,8 +731,11 @@ function MailboxesSection({ mailboxes, users, domains }: { mailboxes: MailboxTyp setQuery(event.target.value)} placeholder="搜索账号或邮箱" className="pl-9" />
-
- 归属账号权限管理子邮箱 +
+
{mailboxGroups.map((group) => { const ownerID = group.owner?.id || group.mailboxes[0].userId @@ -741,8 +748,8 @@ function MailboxesSection({ mailboxes, users, domains }: { mailboxes: MailboxTyp
{group.owner ? accountPrimaryEmail(group.owner) : group.mailboxes[0].userEmail || "未知账号"}
{group.owner?.displayName || "账号信息不可用"}
- {group.owner ? : "-"} - {group.mailboxes.length} 个查看邮箱 + {group.owner ? : "-"} + {group.mailboxes.length} 个查看邮箱 {expanded && (
diff --git a/apps/web/src/pages/mail.tsx b/apps/web/src/pages/mail.tsx index 65bd190..f26e856 100644 --- a/apps/web/src/pages/mail.tsx +++ b/apps/web/src/pages/mail.tsx @@ -486,7 +486,16 @@ export function MailPage() { }, onSettled: () => { qc.invalidateQueries({ queryKey: ["labels"] }); qc.invalidateQueries({ queryKey: ["messages"] }) }, }) - const del = useMutation({ mutationFn: (id: string) => api.delete(id), onSuccess: async () => { setSelectedId(null); setPendingConfirm(null); await qc.invalidateQueries({ queryKey: ["messages"] }); await qc.invalidateQueries({ queryKey: ["folders"] }); await qc.invalidateQueries({ queryKey: ["mailboxes"] }); await qc.invalidateQueries({ queryKey: ["mail-stats"] }); await qc.invalidateQueries({ queryKey: ["labels"] }); toast({ title: "已删除" }) }, onError: (error) => toast({ title: "删除失败", description: error.message }) }) + const del = useMutation({ + mutationFn: ({ id }: { id: string; permanent: boolean }) => api.delete(id), + onSuccess: async (_, { permanent }) => { + setSelectedId(null) + setPendingConfirm(null) + await refreshMailData() + toast({ title: permanent ? "邮件已永久删除" : "邮件已移入已删除" }) + }, + onError: (error) => toast({ title: "删除失败", description: error.message }), + }) const move = useMutation({ mutationFn: ({ id, folder }: { id: string; folder: string }) => api.move(id, folder), onSuccess: async () => { setSelectedId(null); await qc.invalidateQueries({ queryKey: ["messages"] }); await qc.invalidateQueries({ queryKey: ["folders"] }); await qc.invalidateQueries({ queryKey: ["mailboxes"] }); await qc.invalidateQueries({ queryKey: ["mail-stats"] }); await qc.invalidateQueries({ queryKey: ["labels"] }); toast({ title: "已移动" }) }, onError: (error) => toast({ title: "移动失败", description: error.message }) }) const cancelScheduledSend = useMutation({ mutationFn: (item: ScheduledSend) => api.cancelScheduledSend(item.id), @@ -814,10 +823,19 @@ export function MailPage() { await Promise.all(ids.map((id) => api.star(id, starred))) } else if (action === "delete") { await Promise.all(ids.map((id) => api.delete(id))) + completionTitle = `已永久删除 ${ids.length} 封邮件` } else { const target = action === "archive" ? "Archive" : action === "inbox" ? "Inbox" : action === "trash" ? "Trash" : "Spam" const result = await api.bulkMove(ids, target) - completionTitle = result.ok ? result.message : "批量移动部分失败" + completionTitle = result.ok + ? target === "Trash" + ? `已将 ${result.moved} 封邮件移入已删除` + : target === "Spam" + ? `已将 ${result.moved} 封邮件移入垃圾邮件` + : target === "Inbox" + ? `已将 ${result.moved} 封邮件移回收件箱` + : `已归档 ${result.moved} 封邮件` + : "批量移动部分失败" completionDescription = result.ok ? undefined : result.message } if (selectedId && ids.includes(selectedId)) setSelectedId(null) @@ -849,11 +867,14 @@ export function MailPage() { } } function confirmDeleteMessage(message: MailMessage) { + const permanent = message.folder === "Trash" setPendingConfirm({ - title: "删除这封邮件?", - description: `邮件“${message.subject || "无主题"}”将被删除。`, - confirmText: "删除邮件", - onConfirm: () => del.mutate(message.id), + title: permanent ? "永久删除这封邮件?" : "将这封邮件移入已删除?", + description: permanent + ? `邮件“${message.subject || "无主题"}”将被永久删除,且无法恢复。` + : `邮件“${message.subject || "无主题"}”将移入已删除。`, + confirmText: permanent ? "永久删除" : "移入已删除", + onConfirm: () => del.mutate({ id: message.id, permanent }), }) } function openCompose(draft?: ComposeDraft) { @@ -1669,7 +1690,9 @@ export function MailPage() { { void refetchMessages() }} /> ) : compactMailLayout ? ( {selectedLabel.name} : undefined} messages={visibleMessages} total={allMessages.length} @@ -1778,12 +1801,12 @@ export function MailPage() { {selectedCountOnPage > 0 && canOrganizeMail && mailView !== "unknown" && (
已选 {selectedCountOnPage} 封 - +
)} {mailMessagesLoading && } - {visibleMessages.map((m) => toggleCompactSelect(m.id, checked)} onClick={() => openMessage(m.id)} onContextMenu={(event) => openMessageContextMenu(event, m)} onStar={() => star.mutate({ id: m.id, starred: !m.isStarred })} onArchive={() => move.mutate({ id: m.id, folder: m.folder === "Archive" ? "Inbox" : "Archive" })} onTrash={() => move.mutate({ id: m.id, folder: "Trash" })} onToggleRead={() => markRead.mutate({ id: m.id, read: !m.isRead })} canOrganize={canOrganizeMail && mailView !== "unknown"} />)} + {visibleMessages.map((m) => toggleCompactSelect(m.id, checked)} onClick={() => openMessage(m.id)} onContextMenu={(event) => openMessageContextMenu(event, m)} onStar={() => star.mutate({ id: m.id, starred: !m.isStarred })} onArchive={() => move.mutate({ id: m.id, folder: m.folder === "Archive" ? "Inbox" : "Archive" })} onTrash={() => m.folder === "Trash" ? confirmDeleteMessage(m) : move.mutate({ id: m.id, folder: "Trash" })} onToggleRead={() => markRead.mutate({ id: m.id, read: !m.isRead })} canOrganize={canOrganizeMail && mailView !== "unknown"} />)} {!mailMessagesLoading && visibleMessages.length === 0 &&
{emptyMessage}
} {!mailMessagesLoading && hasMoreMessages && (
@@ -1827,7 +1850,7 @@ export function MailPage() { ) : ( ))} - {mailView !== "external" && mailView !== "unknown" && canOrganizeMail && } + {mailView !== "external" && mailView !== "unknown" && canOrganizeMail && }
void; onMoveToFolder?: (folderName: string) => void }) { const buttonClass = "h-7 w-7 rounded-md text-muted-foreground hover:bg-accent hover:text-foreground" + const permanentlyDelete = currentFolder === "Trash" const archiveAction: BulkAction = currentFolder === "Archive" ? "inbox" : "archive" const archiveLabel = currentFolder === "Archive" ? "移回收件箱" : "归档" const movableFolders = folders.filter((folder) => folder.name !== currentFolder && folder.name !== "Drafts") @@ -2602,7 +2626,7 @@ function BulkActionToolbar({ pending, currentFolder, folders = [], readAction = )} -
@@ -2784,7 +2808,7 @@ function MessageContextMenu({ state, labels, folders, canSend, canOrganize, canM {canOrganize && ( <>
- {item("删除", , "delete", true)} + {item(message.folder === "Trash" ? "永久删除" : "移入已删除", , "delete", true)} )}
@@ -2882,6 +2906,7 @@ function CreateFolderDialog({ open, pending, onOpenChange, onCreate }: { open: b function CompactMailView({ title, + currentFolder, icon, messages, total, @@ -2925,6 +2950,7 @@ function CompactMailView({ tools, }: { title: string + currentFolder?: string icon?: React.ReactNode messages: MailMessage[] total: number @@ -3016,7 +3042,7 @@ function CompactMailView({ {selectedIds.length > 0 ? ( <> 已选 {selectedIds.length} 封 - {canOrganize && } + {canOrganize && } ) : (
@@ -3127,7 +3153,7 @@ function CompactMessageDetail({ {canOrganize && onStar(selected)}>{selected.isStarred ? "取消星标" : "添加星标"}} )} - {canOrganize && onDelete(selected)} className="text-destructive">删除} + {canOrganize && onDelete(selected)} className="text-destructive">{selected.folder === "Trash" ? "永久删除" : "移入已删除"}} )} @@ -3147,7 +3173,7 @@ function CompactMessageDetail({ {selected && canOrganize && } )} - {selected && canOrganize && } + {selected && canOrganize && }
@@ -3782,7 +3808,7 @@ function MessageRow({ -