From c92140c9cc80aa64459a8340bf08ba17a7914325 Mon Sep 17 00:00:00 2001
From: zxyszx <299979470+zxyszx@users.noreply.github.com>
Date: Wed, 12 Aug 2026 05:34:49 +0800
Subject: [PATCH] release: prepare v1.2.30
---
.github/release-notes/v1.2.30.md | 9 +++++
VERSION | 2 +-
apps/web/src/pages/admin.tsx | 17 +++++++---
apps/web/src/pages/mail.tsx | 56 +++++++++++++++++++++++---------
4 files changed, 63 insertions(+), 21 deletions(-)
create mode 100644 .github/release-notes/v1.2.30.md
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 && onStar(selected)}>{selected.isStarred ? "取消星标" : "添加星标"}}
>
)}
- {selected && canOrganize && onDelete(selected)}>删除}
+ {selected && canOrganize && onDelete(selected)}>{selected.folder === "Trash" ? "永久删除" : "移入已删除"}}
previousMessage && onSelect(previousMessage.id)}>上一封
@@ -3782,7 +3808,7 @@ function MessageRow({
{ event.stopPropagation(); onArchive() }}>
-
{ event.stopPropagation(); onTrash() }}>
+ { event.stopPropagation(); onTrash() }}>
{ event.stopPropagation(); onToggleRead() }}>