diff --git a/apps/web/src/index.css b/apps/web/src/index.css index b40abde..8c22cf5 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -83,6 +83,36 @@ } @layer components { + .mail-shell-grid { + display: grid; + grid-template-columns: clamp(236px, 17vw, 344px) minmax(0, 1fr); + } + + .mail-content-grid { + display: grid; + grid-template-columns: minmax(420px, 528px) minmax(0, 1fr); + } + + .mail-list-pane, + .mail-detail-pane { + min-height: 0; + height: 100%; + } + + .mail-list-pane { + border-right: 1px solid hsl(var(--border)); + } + + @media (max-width: 1179px) { + .mail-content-grid { + grid-template-columns: minmax(0, 1fr); + } + + .mail-list-pane { + border-right: 0; + } + } + [data-sidebar="content"] { gap: 0.75rem; } diff --git a/apps/web/src/pages/mail.tsx b/apps/web/src/pages/mail.tsx index 42b457c..92c2549 100644 --- a/apps/web/src/pages/mail.tsx +++ b/apps/web/src/pages/mail.tsx @@ -30,7 +30,6 @@ import { ScrollArea } from "@/components/ui/scroll-area" import { Separator } from "@/components/ui/separator" import { Skeleton } from "@/components/ui/skeleton" import { Avatar, AvatarFallback } from "@/components/ui/avatar" -import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable" import { ConfirmDialog } from "@/components/confirm-dialog" import { Sidebar, @@ -89,7 +88,8 @@ const filterLabels: Record = { const emptyAdvancedSearch: AdvancedMailSearch = { from: "", to: "", subject: "", startDate: "", endDate: "", hasAttachments: false, unread: false, starred: false } const emptyAdvancedSearchDraft: AdvancedMailSearchDraft = { ...emptyAdvancedSearch } const mailboxSelectionStorageVersion = "2" -const mailCompactBreakpoint = 1024 +const mailCompactBreakpoint = 768 +const mailDetailBreakpoint = 1180 function useMaxViewportWidth(maxWidth: number) { const [matches, setMatches] = React.useState(false) @@ -133,6 +133,7 @@ export function MailPage() { const [displayMode] = useDisplayMode() const isMobile = useIsMobile() const isNarrowMailViewport = useMaxViewportWidth(mailCompactBreakpoint) + const isTwoPaneMailViewport = useMaxViewportWidth(mailDetailBreakpoint) const compactMailLayout = isMobile || isNarrowMailViewport || displayMode === "compact" const [refreshing, setRefreshing] = React.useState(false) const [autoRefreshing, setAutoRefreshing] = React.useState(false) @@ -1085,6 +1086,7 @@ export function MailPage() { selectedMailboxId={selectedMailboxId} selectedMailbox={selectedMailbox} fallbackAddress={selectedMailbox?.address || me.data?.user.email || ""} + unreadCount={mailboxUnreadCount} onSelect={switchMailbox} /> {!sidebarCollapsed && !isAllMailboxSelected && ( @@ -1416,8 +1418,8 @@ export function MailPage() { language={language} /> ) : ( - - +
+
@@ -1480,11 +1482,16 @@ export function MailPage() {
- {selectedCountOnPage > 0 && canOrganizeMail && }
+ {selectedCountOnPage > 0 && canOrganizeMail && ( +
+ 已选 {selectedCountOnPage} 封 + +
+ )} {(mailView === "external" ? externalMessages.isLoading : messages.isLoading) && } {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 })} canOrganize={canOrganizeMail} />)} @@ -1498,11 +1505,10 @@ export function MailPage() { )}
- - +
- -
+
+
{!selectedId && (
@@ -1514,6 +1520,12 @@ export function MailPage() { {detail.isLoading &&
} {selected &&
+ {isTwoPaneMailViewport && ( + + )}

{selected.subject}

@@ -1540,9 +1552,9 @@ export function MailPage() {
} -
- - + +
+ ) return ( @@ -1575,17 +1587,14 @@ export function MailPage() {
{contentView}
) : ( - - +
+ +
+ {contentView} +
+
)} @@ -2247,25 +2256,38 @@ function datetimeLocalToISO(value: string) { type BulkAction = "read" | "unread" | "star" | "unstar" | "archive" | "trash" | "spam" | "delete" -function BulkActionMenu({ pending, onAction }: { pending: boolean; onAction: (action: BulkAction) => void }) { +function BulkActionToolbar({ pending, onAction }: { pending: boolean; onAction: (action: BulkAction) => void }) { + const buttonClass = "h-7 w-7 rounded-md text-muted-foreground hover:bg-accent hover:text-foreground" return ( - - - - - - onAction("read")}>标为已读 - onAction("unread")}>标为未读 - onAction("star")}>添加星标 - onAction("unstar")}>取消星标 - onAction("archive")}>归档 - onAction("trash")}>移入回收站 - onAction("spam")}>移入垃圾邮件 - onAction("delete")} className="text-destructive">删除 - - +
+ + + + + + + + + + + onAction("unstar")}>取消星标 + onAction("spam")}>移入垃圾邮件 + onAction("delete")} className="text-destructive">彻底删除 + + +
) } @@ -2620,7 +2642,7 @@ function CompactMailView({ {selectedIds.length > 0 ? ( <> 已选 {selectedIds.length} 封 - {canOrganize && } + {canOrganize && } ) : (
{messages.length} / {total} 封
@@ -3038,10 +3060,11 @@ function AccountHeader({ collapsed, name, email, darkMode, language, onToggleThe ) } -function MailboxSwitcher({ collapsed, mailboxes, selectedMailboxId, selectedMailbox, fallbackAddress, onSelect }: { collapsed: boolean; mailboxes: Mailbox[]; selectedMailboxId: string; selectedMailbox?: Mailbox; fallbackAddress?: string; onSelect: (mailboxId: string) => void }) { +function MailboxSwitcher({ collapsed, mailboxes, selectedMailboxId, selectedMailbox, fallbackAddress, unreadCount, onSelect }: { collapsed: boolean; mailboxes: Mailbox[]; selectedMailboxId: string; selectedMailbox?: Mailbox; fallbackAddress?: string; unreadCount: number; onSelect: (mailboxId: string) => void }) { const [mailboxQuery, setMailboxQuery] = React.useState("") const isAllSelected = selectedMailboxId === "all" const displayAddress = isAllSelected ? "全部邮箱" : selectedMailbox?.address || fallbackAddress || "选择邮箱" + const displayUnreadCount = Math.min(unreadCount, 99) const normalizedQuery = mailboxQuery.trim().toLowerCase() const showAllMailboxOption = !normalizedQuery || "全部邮箱".includes(normalizedQuery) || "all".includes(normalizedQuery) const filteredMailboxes = React.useMemo(() => { @@ -3059,6 +3082,11 @@ function MailboxSwitcher({ collapsed, mailboxes, selectedMailboxId, selectedMail {!collapsed && ( <> {displayAddress} + {isAllSelected && unreadCount > 0 && ( + + {unreadCount > 99 ? "99+" : displayUnreadCount} + + )} )} @@ -3082,6 +3110,11 @@ function MailboxSwitcher({ collapsed, mailboxes, selectedMailboxId, selectedMail onSelect("all")} className={cn("h-8 gap-2 rounded-sm px-2 text-[13px] font-normal", isAllSelected && "bg-accent text-accent-foreground")}> 全部邮箱 + {unreadCount > 0 && ( + + {unreadCount > 99 ? "99+" : displayUnreadCount} + + )} )} {filteredMailboxes.map((mailbox) => (