feat(mail): 优化外部邮箱侧边栏交互
Docker Release / Check web and api (push) Waiting to run
Docker Release / Resolve release tag (push) Blocked by required conditions
Docker Release / Build and publish all-in-one (push) Blocked by required conditions
Docker Release / Build and publish api (push) Blocked by required conditions
Docker Release / Build and publish web (push) Blocked by required conditions
Docker Release / Build and publish dovecot (push) Blocked by required conditions
Docker Release / Build and publish postfix (push) Blocked by required conditions
Docker Release / Build and publish rspamd (push) Blocked by required conditions
Docker Release / Create GitHub release (push) Blocked by required conditions

- 支持外部邮箱账号展开/收起,并在切换时自动保持当前账号展开。
- 增加外部邮箱显示主标题与副标题,提升账号信息可读性。
- 在外部 IMAP 关闭或账号列表变化时,清理已展开状态。
This commit is contained in:
LanQin
2026-06-25 22:50:32 +08:00
parent 707f11687d
commit bce259ca8a
+54 -6
View File
@@ -99,6 +99,7 @@ export function MailPage() {
const [mailFilter, setMailFilter] = React.useState<MailFilter>("all") const [mailFilter, setMailFilter] = React.useState<MailFilter>("all")
const [selectedMailboxId, setSelectedMailboxId] = React.useState(() => localStorage.getItem("lanqin:selected-mailbox") || "") const [selectedMailboxId, setSelectedMailboxId] = React.useState(() => localStorage.getItem("lanqin:selected-mailbox") || "")
const [selectedExternalAccountId, setSelectedExternalAccountId] = React.useState("") const [selectedExternalAccountId, setSelectedExternalAccountId] = React.useState("")
const [expandedExternalAccountIds, setExpandedExternalAccountIds] = React.useState<string[]>([])
const [externalFolder, setExternalFolder] = React.useState("INBOX") const [externalFolder, setExternalFolder] = React.useState("INBOX")
const [darkMode, setDarkMode] = React.useState(getInitialTheme) const [darkMode, setDarkMode] = React.useState(getInitialTheme)
const [displayMode] = useDisplayMode() const [displayMode] = useDisplayMode()
@@ -164,8 +165,16 @@ export function MailPage() {
React.useEffect(() => { React.useEffect(() => {
if (externalImapEnabled) return if (externalImapEnabled) return
setSelectedExternalAccountId("") setSelectedExternalAccountId("")
setExpandedExternalAccountIds([])
if (mailView === "external") setMailView("folder") if (mailView === "external") setMailView("folder")
}, [externalImapEnabled, mailView]) }, [externalImapEnabled, mailView])
React.useEffect(() => {
const ids = new Set((externalMailAccounts.data?.items || []).map((item) => item.id))
setExpandedExternalAccountIds((current) => {
const next = current.filter((id) => ids.has(id))
return next.length === current.length ? current : next
})
}, [externalMailAccounts.data?.items])
const inboxProbe = useQuery({ const inboxProbe = useQuery({
queryKey: ["mail-notifications", activeMailboxId], queryKey: ["mail-notifications", activeMailboxId],
queryFn: () => api.messages("Inbox", "", "", activeMailboxId), queryFn: () => api.messages("Inbox", "", "", activeMailboxId),
@@ -761,6 +770,7 @@ export function MailPage() {
} }
function openExternalFolder(account: ExternalImapAccount, folderName = "INBOX") { function openExternalFolder(account: ExternalImapAccount, folderName = "INBOX") {
setSelectedExternalAccountId(account.id) setSelectedExternalAccountId(account.id)
setExpandedExternalAccountIds([account.id])
setExternalFolder(folderName) setExternalFolder(folderName)
setMailView("external") setMailView("external")
setSelectedLabelId("") setSelectedLabelId("")
@@ -768,6 +778,21 @@ export function MailPage() {
setMailFilter("all") setMailFilter("all")
setMobileSidebarOpen(false) setMobileSidebarOpen(false)
} }
function toggleExternalAccount(account: ExternalImapAccount) {
if (sidebarCollapsed) {
openExternalFolder(account, "INBOX")
return
}
const expanded = expandedExternalAccountIds.includes(account.id)
if (expanded) {
setExpandedExternalAccountIds((items) => items.filter((id) => id !== account.id))
return
}
setExpandedExternalAccountIds([account.id])
if (selectedExternalAccountId !== account.id || mailView !== "external") {
openExternalFolder(account, "INBOX")
}
}
function reorderCustomFolder(draggedId: string, target: FolderDropTarget) { function reorderCustomFolder(draggedId: string, target: FolderDropTarget) {
if (!canOrganizeMail || reorderFolders.isPending) return if (!canOrganizeMail || reorderFolders.isPending) return
const foldersByID = new Map((folders.data?.items || []).map((item) => [item.id, item])) const foldersByID = new Map((folders.data?.items || []).map((item) => [item.id, item]))
@@ -1041,20 +1066,31 @@ export function MailPage() {
{!sidebarCollapsed && <SidebarGroupLabel></SidebarGroupLabel>} {!sidebarCollapsed && <SidebarGroupLabel></SidebarGroupLabel>}
<SidebarGroupContent> <SidebarGroupContent>
<SidebarMenu> <SidebarMenu>
{externalAccountItems.map((account) => ( {externalAccountItems.map((account) => {
const expanded = expandedExternalAccountIds.includes(account.id)
return (
<React.Fragment key={account.id}> <React.Fragment key={account.id}>
<SidebarMenuItem> <SidebarMenuItem>
<SidebarMenuButton <SidebarMenuButton
isActive={mailView === "external" && selectedExternalAccountId === account.id} isActive={mailView === "external" && selectedExternalAccountId === account.id}
size={sidebarCollapsed ? "default" : "lg"}
className={cn(sidebarCollapsed && "justify-center px-0")} className={cn(sidebarCollapsed && "justify-center px-0")}
onClick={() => openExternalFolder(account, "INBOX")} title={`${externalAccountLabel(account)}${account.name && account.name !== externalAccountLabel(account) ? ` · ${account.name}` : ""}`}
onClick={() => toggleExternalAccount(account)}
> >
<Mail className="h-4 w-4" /> <Mail className="h-4 w-4" />
{!sidebarCollapsed && <span>{account.name}</span>} {!sidebarCollapsed && (
{!sidebarCollapsed && <Badge variant="outline" className="ml-auto">{account.storageMode === "local" ? "同步" : "直连"}</Badge>} <>
<span className="min-w-0 flex-1">
<span className="block truncate">{externalAccountLabel(account)}</span>
<span className="block truncate text-xs font-normal text-muted-foreground">{externalAccountSubtitle(account)}</span>
</span>
<ChevronDown className={cn("h-4 w-4 text-muted-foreground transition-transform", expanded && "rotate-180")} />
</>
)}
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
{!sidebarCollapsed && mailView === "external" && selectedExternalAccountId === account.id && externalFolderItems.map((item) => ( {!sidebarCollapsed && expanded && externalFolderItems.map((item) => (
<SidebarMenuItem key={`${account.id}-${item.name}`}> <SidebarMenuItem key={`${account.id}-${item.name}`}>
<SidebarMenuButton <SidebarMenuButton
isActive={externalFolder === item.name} isActive={externalFolder === item.name}
@@ -1067,8 +1103,9 @@ export function MailPage() {
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
))} ))}
{!sidebarCollapsed && expanded && externalFolders.isLoading && <FolderSkeleton />}
</React.Fragment> </React.Fragment>
))} )})}
</SidebarMenu> </SidebarMenu>
</SidebarGroupContent> </SidebarGroupContent>
</SidebarGroup>} </SidebarGroup>}
@@ -1558,6 +1595,17 @@ function getEmptyMessage(mailView: MailView, folder: string, total: number) {
return "当前文件夹没有邮件" return "当前文件夹没有邮件"
} }
function externalAccountLabel(account: ExternalImapAccount) {
return account.oauthEmail || account.username || account.name || "外部邮箱"
}
function externalAccountSubtitle(account: ExternalImapAccount) {
const label = externalAccountLabel(account)
const mode = account.storageMode === "local" ? "同步" : "直连"
const name = account.name && account.name !== label ? account.name : ""
return [name, account.host, mode].filter(Boolean).join(" · ")
}
function NoMailboxState({ onOpenSettings }: { onOpenSettings: () => void }) { function NoMailboxState({ onOpenSettings }: { onOpenSettings: () => void }) {
return ( return (
<div className="grid min-h-0 flex-1 place-items-center p-6"> <div className="grid min-h-0 flex-1 place-items-center p-6">