diff --git a/apps/web/src/components/protected-layout.tsx b/apps/web/src/components/protected-layout.tsx index 46c8c03..5468321 100644 --- a/apps/web/src/components/protected-layout.tsx +++ b/apps/web/src/components/protected-layout.tsx @@ -21,6 +21,7 @@ import { SidebarProvider, SidebarRail, SidebarTrigger, + useSidebar, } from "@/components/ui/sidebar" const adminSections = [ @@ -80,16 +81,7 @@ function ProtectedContent() { - {adminSections.map((item) => ( - - - - {item.icon} - {item.label} - - - - ))} + @@ -126,8 +118,11 @@ function ProtectedContent() {
-
- +
+ +
+ {isAdminRoute ? adminSections.find((item) => item.key === adminSection)?.label || "系统管理" : "LanQin Email"} +
@@ -135,3 +130,20 @@ function ProtectedContent() { ) } + +function AdminSectionItems({ activeSection }: { activeSection: string }) { + const { isMobile, setOpenMobile } = useSidebar() + function closeMobile() { + if (isMobile) setOpenMobile(false) + } + return adminSections.map((item) => ( + + + + {item.icon} + {item.label} + + + + )) +} diff --git a/apps/web/src/pages/admin.tsx b/apps/web/src/pages/admin.tsx index cb23b0b..5d25f5c 100644 --- a/apps/web/src/pages/admin.tsx +++ b/apps/web/src/pages/admin.tsx @@ -53,20 +53,12 @@ export function AdminPage() { const section: Section = rawSection && sectionKeys.includes(rawSection) ? rawSection : "overview" return ( - -
+ +

{sectionLabels[section]}

-
- {sectionKeys.map((key) => ( - - ))} -
- {section === "overview" && (
} label="用户" value={overview.data?.users || 0} /> @@ -203,24 +195,45 @@ function UsersSection({ users }: { users: AdminUser[] }) {
- - 用户角色邮箱状态创建时间 - - {filteredUsers.map((user) => ( - - -
{user.displayName}
-
{user.email}
-
- {user.role === "admin" ? "管理员" : "普通用户"} - - {user.disabled ? "停用" : "正常"} - {new Date(user.createdAt).toLocaleDateString()} - setPendingConfirm({ title: "删除用户?", description: `将删除 ${user.email} 及其关联数据。`, confirmText: "删除用户", onConfirm: () => remove.mutate(user.id) })} /> -
- ))} -
-
+
+ {filteredUsers.map((user) => ( +
+
+
+
{user.displayName}
+
{user.email}
+
+ setPendingConfirm({ title: "删除用户?", description: `将删除 ${user.email} 及其关联数据。`, confirmText: "删除用户", onConfirm: () => remove.mutate(user.id) })} /> +
+
+ {user.role === "admin" ? "管理员" : "普通用户"} + {user.disabled ? "停用" : "正常"} + {new Date(user.createdAt).toLocaleDateString()} +
+
+
+ ))} +
+
+ + 用户角色邮箱状态创建时间 + + {filteredUsers.map((user) => ( + + +
{user.displayName}
+
{user.email}
+
+ {user.role === "admin" ? "管理员" : "普通用户"} + + {user.disabled ? "停用" : "正常"} + {new Date(user.createdAt).toLocaleDateString()} + setPendingConfirm({ title: "删除用户?", description: `将删除 ${user.email} 及其关联数据。`, confirmText: "删除用户", onConfirm: () => remove.mutate(user.id) })} /> +
+ ))} +
+
+
{filteredUsers.length === 0 && } { if (!open) setPendingConfirm(null) }} onConfirm={() => pendingConfirm?.onConfirm()} /> @@ -293,21 +306,41 @@ function MailboxesSection({ mailboxes, users, domains }: { mailboxes: MailboxTyp
- - 地址归属用户名称配额状态 - - {mailboxes.map((mailbox) => ( - - {mailbox.address} - {mailbox.userEmail || mailbox.userId} - {mailbox.displayName} - {mailbox.quotaMb} MB - {mailbox.status === "active" ? "启用" : "停用"} - setPendingConfirm({ title: "删除邮箱?", description: `将删除 ${mailbox.address} 和其中邮件。`, confirmText: "删除邮箱", onConfirm: () => remove.mutate(mailbox.id) })} /> - - ))} - -
+
+ {mailboxes.map((mailbox) => ( +
+
+
+
{mailbox.address}
+
{mailbox.userEmail || mailbox.userId}
+
+ setPendingConfirm({ title: "删除邮箱?", description: `将删除 ${mailbox.address} 和其中邮件。`, confirmText: "删除邮箱", onConfirm: () => remove.mutate(mailbox.id) })} /> +
+
+ {mailbox.status === "active" ? "启用" : "停用"} + {mailbox.quotaMb} MB + {mailbox.displayName || "未命名"} +
+
+ ))} +
+
+ + 地址归属用户名称配额状态 + + {mailboxes.map((mailbox) => ( + + {mailbox.address} + {mailbox.userEmail || mailbox.userId} + {mailbox.displayName} + {mailbox.quotaMb} MB + {mailbox.status === "active" ? "启用" : "停用"} + setPendingConfirm({ title: "删除邮箱?", description: `将删除 ${mailbox.address} 和其中邮件。`, confirmText: "删除邮箱", onConfirm: () => remove.mutate(mailbox.id) })} /> + + ))} + +
+
{mailboxes.length === 0 && }
{ if (!open) setPendingConfirm(null) }} onConfirm={() => pendingConfirm?.onConfirm()} /> @@ -330,20 +363,39 @@ function AliasesSection({ aliases, domains }: { aliases: Alias[]; domains: Domai - - 来源目标域名状态 - - {aliases.map((alias) => ( - - {alias.source} - {alias.destination} - {domains.find((d) => d.id === alias.domainId)?.name || alias.domainId} - {alias.enabled ? "启用" : "停用"} - update.mutate({ id: alias.id, payload: { source: alias.source, destination: alias.destination, enabled: !alias.enabled } })} onDelete={() => setPendingConfirm({ title: "删除别名?", description: `${alias.source} 将不再转发到 ${alias.destination}。`, confirmText: "删除别名", onConfirm: () => remove.mutate(alias.id) })} /> - - ))} - -
+
+ {aliases.map((alias) => ( +
+
+
+
{alias.source}
+
{alias.destination}
+
+ update.mutate({ id: alias.id, payload: { source: alias.source, destination: alias.destination, enabled: !alias.enabled } })} onDelete={() => setPendingConfirm({ title: "删除别名?", description: `${alias.source} 将不再转发到 ${alias.destination}。`, confirmText: "删除别名", onConfirm: () => remove.mutate(alias.id) })} /> +
+
+ {alias.enabled ? "启用" : "停用"} + {domains.find((d) => d.id === alias.domainId)?.name || alias.domainId} +
+
+ ))} +
+
+ + 来源目标域名状态 + + {aliases.map((alias) => ( + + {alias.source} + {alias.destination} + {domains.find((d) => d.id === alias.domainId)?.name || alias.domainId} + {alias.enabled ? "启用" : "停用"} + update.mutate({ id: alias.id, payload: { source: alias.source, destination: alias.destination, enabled: !alias.enabled } })} onDelete={() => setPendingConfirm({ title: "删除别名?", description: `${alias.source} 将不再转发到 ${alias.destination}。`, confirmText: "删除别名", onConfirm: () => remove.mutate(alias.id) })} /> + + ))} + +
+
{aliases.length === 0 && }
{ if (!open) setPendingConfirm(null) }} onConfirm={() => pendingConfirm?.onConfirm()} /> @@ -407,38 +459,62 @@ function AdminMessagesSection({ mailboxes }: { mailboxes: MailboxType[] }) { - - - - 邮件 - 邮箱 - 发件人 - 收件人 - 文件夹 - 时间 - - - - - {items.map((message) => ( - - +
+ {items.map((message) => ( +
+
+
{message.subject}
-
{message.snippet}
- - -
{message.mailboxAddress || message.recipientAddress || "-"}
- {message.ownerEmail &&
{message.ownerEmail}
} -
- {adminSenderDisplayName(message)} - {message.recipientAddress || message.to.join(", ")} - {folderName(message.folder)} - {formatDate(message.receivedAt)} - +
{message.snippet}
+
+ +
+
+
邮箱:{message.mailboxAddress || message.recipientAddress || "-"}
+
发件人:{adminSenderDisplayName(message)}
+
收件人:{message.recipientAddress || message.to.join(", ")}
+
+
+ {folderName(message.folder)} + {formatDate(message.receivedAt)} +
+
+ ))} +
+
+
+ + + 邮件 + 邮箱 + 发件人 + 收件人 + 文件夹 + 时间 + - ))} - -
+ + + {items.map((message) => ( + + +
{message.subject}
+
{message.snippet}
+
+ +
{message.mailboxAddress || message.recipientAddress || "-"}
+ {message.ownerEmail &&
{message.ownerEmail}
} +
+ {adminSenderDisplayName(message)} + {message.recipientAddress || message.to.join(", ")} + {folderName(message.folder)} + {formatDate(message.receivedAt)} + +
+ ))} +
+ + {messages.isLoading && } {!messages.isLoading && items.length === 0 && } {!messages.isLoading && messages.hasNextPage && ( @@ -828,9 +904,9 @@ function adminSenderTitle(message: MailMessage) { } function Stat({ icon, label, value }: { icon: React.ReactNode; label: string; value: React.ReactNode }) { - return
{icon}
{value}
{label}
+ return
{icon}
{value}
{label}
} -function InfoBox({ label, value }: { label: string; value: React.ReactNode }) { return
{value}
{label}
} +function InfoBox({ label, value }: { label: string; value: React.ReactNode }) { return
{value}
{label}
} function Empty({ text }: { text: string }) { return
{text}
} function DomainBadgeRow({ domain }: { domain: Domain }) { return
{domain.name}{domain.dnsStatus === "ok" ? "正常" : domain.dnsStatus}
} function invalidateAdmin(qc: ReturnType) { qc.invalidateQueries({ queryKey: ["admin"] }); qc.invalidateQueries({ queryKey: ["mailboxes"] }); qc.invalidateQueries({ queryKey: ["me"] }) } diff --git a/apps/web/src/pages/mail.tsx b/apps/web/src/pages/mail.tsx index c8e7ad6..283b966 100644 --- a/apps/web/src/pages/mail.tsx +++ b/apps/web/src/pages/mail.tsx @@ -22,6 +22,7 @@ import { Input } from "@/components/ui/input" import { Badge } from "@/components/ui/badge" import { Checkbox } from "@/components/ui/checkbox" import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { Sheet, SheetContent, SheetTitle, SheetTrigger } from "@/components/ui/sheet" import { Label } from "@/components/ui/label" import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" @@ -44,6 +45,7 @@ import { SidebarProvider, } from "@/components/ui/sidebar" import { useMe } from "@/hooks/use-me" +import { useIsMobile } from "@/hooks/use-mobile" import { useToast } from "@/hooks/use-toast" const folderIcons: Record = { inbox: , sent: , drafts: , archive: , spam: , trash: } @@ -93,12 +95,14 @@ export function MailPage() { const [selectedMailboxId, setSelectedMailboxId] = React.useState(() => localStorage.getItem("lanqin:selected-mailbox") || "") const [darkMode, setDarkMode] = React.useState(getInitialTheme) const [displayMode] = useDisplayMode() + const isMobile = useIsMobile() const [refreshing, setRefreshing] = React.useState(false) const [autoRefreshing, setAutoRefreshing] = React.useState(false) const [lastAutoRefreshAt, setLastAutoRefreshAt] = React.useState(null) const [bulkPending, setBulkPending] = React.useState(false) const [pendingConfirm, setPendingConfirm] = React.useState(null) const [cancelingScheduledId, setCancelingScheduledId] = React.useState("") + const [mobileSidebarOpen, setMobileSidebarOpen] = React.useState(false) const sidebarPanelRef = React.useRef(null) const themeMountedRef = React.useRef(false) const mailNotifyStateRef = React.useRef>({}) @@ -470,30 +474,36 @@ export function MailPage() { setSelectedLabelId("") setSelectedId(null) setMailFilter("all") + setMobileSidebarOpen(false) } function openFolder(nextFolder: string) { setFolder(nextFolder) setMailView("folder") setSelectedLabelId("") setSelectedId(null) + setMailFilter("all") + setMobileSidebarOpen(false) } function openStarred() { setMailView("starred") setSelectedLabelId("") setSelectedId(null) setMailFilter("all") + setMobileSidebarOpen(false) } function openScheduled() { setMailView("scheduled") setSelectedLabelId("") setSelectedId(null) setMailFilter("all") + setMobileSidebarOpen(false) } function openLabel(labelId: string) { setSelectedLabelId(labelId) setMailView("label") setSelectedId(null) setMailFilter("all") + setMobileSidebarOpen(false) } function openMessage(messageId: string | null) { if (!messageId) { @@ -527,6 +537,89 @@ export function MailPage() { function openSettings() { navigate("/profile") } + const sidebarContent = ( + + + setDarkMode((value) => !value)} + onSettings={openSettings} + /> +
+ + {!sidebarCollapsed && ( + + )} +
+ +
+ + + {!sidebarCollapsed && 邮件夹} + + + {mailMenuItems.map((item) => ( + + item.type === "starred" ? openStarred() : item.type === "scheduled" ? openScheduled() : openFolder(item.folderName)} + > + {item.icon} + {!sidebarCollapsed && {item.label}} + {!sidebarCollapsed && item.count > 0 && {item.count}} + + + ))} + + {folders.isLoading && } + + + + {!sidebarCollapsed && 标签} + + + {labelItems.map((label) => ( + + openLabel(label.id)}> + + {!sidebarCollapsed && {label.name}} + {!sidebarCollapsed && !!label.messageCount && {label.messageCount}} + + + ))} + {!sidebarCollapsed && !labels.isLoading && labelItems.length === 0 &&
暂无标签
} + + createLabel.mutate(name)} /> + +
+ {labels.isLoading && } +
+
+
+ {!isMobile && ( +
+ +
+ )} +
+ ) function toggleSidebar() { if (sidebarCollapsed) { sidebarPanelRef.current?.expand(14) @@ -537,93 +630,158 @@ export function MailPage() { } } - return ( -
- - - setSidebarCollapsed(true)} onExpand={() => setSidebarCollapsed(false)}> - - - setDarkMode((value) => !value)} - onSettings={openSettings} - /> -
- - {!sidebarCollapsed && ( - - )} -
- -
- - - {!sidebarCollapsed && 邮件夹} - - - {mailMenuItems.map((item) => ( - - item.type === "starred" ? openStarred() : item.type === "scheduled" ? openScheduled() : openFolder(item.folderName)} - > - {item.icon} - {!sidebarCollapsed && {item.label}} - {!sidebarCollapsed && item.count > 0 && {item.count}} - - - ))} - - {folders.isLoading && } - - - - {!sidebarCollapsed && 标签} - - - {labelItems.map((label) => ( - - openLabel(label.id)}> - - {!sidebarCollapsed && {label.name}} - {!sidebarCollapsed && !!label.messageCount && {label.messageCount}} - - - ))} - {!sidebarCollapsed && !labels.isLoading && labelItems.length === 0 &&
暂无标签
} - - createLabel.mutate(name)} /> - -
- {labels.isLoading && } -
-
-
-
- + const contentView = !mailboxList.isLoading && !hasMailboxes ? ( + + ) : mailView === "scheduled" ? ( + cancelScheduledSend.mutate(item)} + /> + ) : isMobile || displayMode === "compact" ? ( + : undefined} + messages={visibleMessages} + total={allMessages.length} + selectedIds={compactSelectedIds} + allSelected={compactAllSelected} + someSelected={compactSomeSelected} + loading={messages.isLoading} + hasMore={hasMoreMessages} + loadingMore={messages.isFetchingNextPage} + onLoadMore={() => messages.fetchNextPage()} + emptyMessage={emptyMessage} + selectedId={selectedId} + selected={selected} + detailLoading={detail.isLoading} + labels={labelItems} + labelPending={addLabel.isPending || removeLabel.isPending} + onSelect={openMessage} + onSelectAll={toggleCompactSelectAll} + onToggleSelected={toggleCompactSelect} + scheduledDraftIds={scheduledDraftIds} + onCloseReader={() => setSelectedId(null)} + onStar={(message) => star.mutate({ id: message.id, starred: !message.isStarred })} + onReply={openReply} + onForward={openForward} + onArchive={(message) => move.mutate({ id: message.id, folder: message.folder === "Archive" ? "Inbox" : "Archive" })} + onDelete={confirmDeleteMessage} + onToggleRead={(message) => markRead.mutate({ id: message.id, read: !message.isRead })} + onAddLabel={(message, label) => addLabel.mutate({ id: message.id, label })} + onRemoveLabel={(message, labelId) => removeLabel.mutate({ id: message.id, labelId })} + bulkPending={bulkPending} + onBulkAction={runBulkAction} + /> + ) : ( + + +
+
+
+ toggleCompactSelectAll(value === true)} /> +
+
{mailView === "label" && selectedLabel && }{viewTitle}
+
{selectedCountOnPage > 0 ? `已选 ${selectedCountOnPage} 封` : `${visibleMessages.length} / ${allMessages.length} 封邮件`}
+
+
+ {selectedCountOnPage > 0 && } +
+ + {messages.isLoading && } + {visibleMessages.map((m) => toggleCompactSelect(m.id, checked)} onClick={() => openMessage(m.id)} onStar={() => star.mutate({ id: m.id, starred: !m.isStarred })} />)} + {!messages.isLoading && visibleMessages.length === 0 &&
{emptyMessage}
} + {!messages.isLoading && hasMoreMessages && ( +
+ +
+ )} +
+
+
+ + + +
+ {!selectedId &&
选择一封邮件阅读
} + {detail.isLoading &&
} + {selected &&
+
+
+

{selected.subject}

+
+ + + {selected.folder === "Archive" ? ( + + ) : ( + + )} +
- +
+
{senderDisplayName(selected)} 发给 {selected.to.join(", ")} · {formatDateTime(selected.receivedAt)}
+ addLabel.mutate({ id: selected.id, label })} + onRemove={(labelId) => removeLabel.mutate({ id: selected.id, labelId })} + pending={addLabel.isPending || removeLabel.isPending} + /> +
+ +
+
${selected.bodyText || ""}`) }} /> + {selected.attachments && selected.attachments.length > 0 &&
附件
{selected.attachments.map((a) => {a.filename}{formatBytes(a.sizeBytes)})}
} +
+ +
} +
+
+
+ ) + + return ( +
+ + {isMobile ? ( +
+ {!selectedId && ( +
+ + + + + + 邮箱导航 +
{sidebarContent}
+
+
+ +
{viewTitle}
+ +
+ + setQuery(e.target.value)} placeholder={mailView === "scheduled" ? "搜索待发送" : "搜索邮件"} className="h-10 pl-9" /> +
+
+ )} +
{contentView}
+
+ ) : ( + + setSidebarCollapsed(true)} onExpand={() => setSidebarCollapsed(false)}> + {sidebarContent} -
@@ -659,127 +817,11 @@ export function MailPage() { setQuery(e.target.value)} placeholder={mailView === "scheduled" ? "搜索待发送" : "搜索邮件"} className="pl-9" />
- - {!mailboxList.isLoading && !hasMailboxes ? ( - - ) : mailView === "scheduled" ? ( - cancelScheduledSend.mutate(item)} - /> - ) : displayMode === "compact" ? ( - : undefined} - messages={visibleMessages} - total={allMessages.length} - selectedIds={compactSelectedIds} - allSelected={compactAllSelected} - someSelected={compactSomeSelected} - loading={messages.isLoading} - hasMore={hasMoreMessages} - loadingMore={messages.isFetchingNextPage} - onLoadMore={() => messages.fetchNextPage()} - emptyMessage={emptyMessage} - selectedId={selectedId} - selected={selected} - detailLoading={detail.isLoading} - labels={labelItems} - labelPending={addLabel.isPending || removeLabel.isPending} - onSelect={openMessage} - onSelectAll={toggleCompactSelectAll} - onToggleSelected={toggleCompactSelect} - scheduledDraftIds={scheduledDraftIds} - onCloseReader={() => setSelectedId(null)} - onStar={(message) => star.mutate({ id: message.id, starred: !message.isStarred })} - onReply={openReply} - onForward={openForward} - onArchive={(message) => move.mutate({ id: message.id, folder: message.folder === "Archive" ? "Inbox" : "Archive" })} - onDelete={confirmDeleteMessage} - onToggleRead={(message) => markRead.mutate({ id: message.id, read: !message.isRead })} - onAddLabel={(message, label) => addLabel.mutate({ id: message.id, label })} - onRemoveLabel={(message, labelId) => removeLabel.mutate({ id: message.id, labelId })} - bulkPending={bulkPending} - onBulkAction={runBulkAction} - /> - ) : ( - - -
-
-
- toggleCompactSelectAll(value === true)} /> -
-
{mailView === "label" && selectedLabel && }{viewTitle}
-
{selectedCountOnPage > 0 ? `已选 ${selectedCountOnPage} 封` : `${visibleMessages.length} / ${allMessages.length} 封邮件`}
-
-
- {selectedCountOnPage > 0 && } -
- - {messages.isLoading && } - {visibleMessages.map((m) => toggleCompactSelect(m.id, checked)} onClick={() => openMessage(m.id)} onStar={() => star.mutate({ id: m.id, starred: !m.isStarred })} />)} - {!messages.isLoading && visibleMessages.length === 0 &&
{emptyMessage}
} - {!messages.isLoading && hasMoreMessages && ( -
- -
- )} -
-
-
- - - -
- {!selectedId &&
选择一封邮件阅读
} - {detail.isLoading &&
} - {selected &&
-
-
-

{selected.subject}

-
- - - {selected.folder === "Archive" ? ( - - ) : ( - - )} - -
-
-
{senderDisplayName(selected)} 发给 {selected.to.join(", ")} · {formatDateTime(selected.receivedAt)}
- addLabel.mutate({ id: selected.id, label })} - onRemove={(labelId) => removeLabel.mutate({ id: selected.id, labelId })} - pending={addLabel.isPending || removeLabel.isPending} - /> -
- -
-
${selected.bodyText || ""}`) }} /> - {selected.attachments && selected.attachments.length > 0 &&
附件
{selected.attachments.map((a) => {a.filename}{formatBytes(a.sizeBytes)})}
} -
- -
} -
-
-
- - )} + {contentView} + )} { setComposeOpen(open); if (!open) setComposeDraft(undefined) }} onSent={() => { setComposeOpen(false); setComposeDraft(undefined); qc.invalidateQueries({ queryKey: ["messages"] }); qc.invalidateQueries({ queryKey: ["folders"] }); qc.invalidateQueries({ queryKey: ["mail-stats"] }); qc.invalidateQueries({ queryKey: ["labels"] }); qc.invalidateQueries({ queryKey: ["scheduled-sends"] }) }} /> @@ -1044,15 +1086,18 @@ function CompactMailView({ return (
-
-
+
+
onSelectAll(value === true)} /> -
{icon}{title}
+
+ {icon} + {title} +
-
+
{selectedIds.length > 0 ? ( <> - 已选 {selectedIds.length} 封 + 已选 {selectedIds.length} 封 ) : ( @@ -1113,7 +1158,43 @@ function CompactMessageDetail({ }) { return (
-
+
+
+ +
{selected?.subject || "邮件详情"}
+ + + {selected && ( + + + + + + {selected.folder === "Drafts" ? ( + onSelect(selected.id)}>编辑草稿 + ) : ( + <> + onReply(selected)}>回复 + onForward(selected)}>转发 + onArchive(selected)}>{selected.folder === "Archive" ? "取消归档" : "归档"} + onToggleRead(selected)}>{selected.isRead ? "标为未读" : "标为已读"} + onStar(selected)}>{selected.isStarred ? "取消星标" : "添加星标"} + + )} + onDelete(selected)} className="text-destructive">删除 + + + )} +
+
{selected?.folder === "Drafts" ? ( @@ -1134,19 +1215,20 @@ function CompactMessageDetail({
+
{loading &&
} {!loading && !selected &&
邮件不存在
} {selected && ( -
+
-
-

{selected.subject}

+
+

{selected.subject}

-
+
{accountInitial(senderDisplayName(selected), selected.from)}
@@ -1154,7 +1236,7 @@ function CompactMessageDetail({
收件人 {selected.to.join(", ")}
-
{formatDateTime(selected.receivedAt)}
+
{formatDateTime(selected.receivedAt)}
-
-
${selected.bodyText || ""}`) }} /> - {selected.attachments && selected.attachments.length > 0 &&
附件
{selected.attachments.map((a) => {a.filename}{formatBytes(a.sizeBytes)})}
} +
+
${escapeHtml(selected.bodyText || "")}`) }} /> + {selected.attachments && selected.attachments.length > 0 &&
附件
{selected.attachments.map((a) => {a.filename}{formatBytes(a.sizeBytes)})}
}
@@ -1177,21 +1259,40 @@ function CompactMessageDetail({ function CompactMessageRow({ message, active, checked, scheduled, onCheckedChange, onClick, onStar }: { message: MailMessage; active: boolean; checked: boolean; scheduled?: boolean; onCheckedChange: (checked: boolean) => void; onClick: () => void; onStar: () => void }) { const visibleLabels = (message.labels || []).slice(0, 2) + const hiddenLabelCount = Math.max((message.labels?.length || 0) - visibleLabels.length, 0) const senderName = senderDisplayName(message) return ( -
- onCheckedChange(value === true)} onClick={(event) => event.stopPropagation()} /> - -
{senderName}
-
- {message.subject} - {message.snippet} - {scheduled && 已定时} - {visibleLabels.map((label) => )} - {message.hasAttachments && } +
+
+ onCheckedChange(value === true)} onClick={(event) => event.stopPropagation()} className="mt-0.5 shrink-0 sm:mt-0" /> + {message.isRead ? ( + + ) : ( + + )} +
+
+
{senderName}
+
+ {formatDate(message.receivedAt)} + +
+
+
+ {message.subject} + {message.snippet} + {scheduled && 已定时} + {visibleLabels.map((label) => )} + {hiddenLabelCount > 0 && +{hiddenLabelCount}} + {message.hasAttachments && } +
+
{message.snippet}
+
-
{formatDate(message.receivedAt)}
-
@@ -1666,13 +1767,13 @@ function ComposeDialog({ mailbox, open, draft, onOpenChange, onSent }: { mailbox return ( event.preventDefault()} onPointerDownOutside={(event) => event.preventDefault()} > -
- - + + + {draftId ? "编辑草稿" : "写信"} {draftStatus === "saving" ? "正在保存草稿..." : draftStatus === "saved" && lastSavedAt ? `草稿已保存 ${lastSavedAt.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}` : draftStatus === "error" ? "草稿保存失败" : ""} @@ -1721,10 +1822,10 @@ function ComposeDialog({ mailbox, open, draft, onOpenChange, onSent }: { mailbox onRemoveFile={(index) => { setAttachmentsTouched(true); setFiles((current) => current.filter((_, itemIndex) => itemIndex !== index)) }} />
- - - - + + + + @@ -1744,8 +1845,8 @@ function ComposeDialog({ mailbox, open, draft, onOpenChange, onSent }: { mailbox function ComposeField({ label, children, action }: { label: string; children: React.ReactNode; action?: React.ReactNode }) { return ( -
- +
+
{children} {action} @@ -1983,7 +2084,7 @@ function MailBodyComposer({ defaultValue, defaultHtml, files, signatureText, onC content: composerInitialHtml(defaultValue, defaultHtml), editorProps: { attributes: { - class: "mail-html min-h-[280px] overflow-y-auto px-6 py-5 text-base leading-7 outline-none", + class: "mail-html min-h-[260px] overflow-y-auto px-4 py-4 text-base leading-7 outline-none sm:min-h-[280px] sm:px-6 sm:py-5", "aria-label": "正文", }, handlePaste(view, event) { @@ -2105,9 +2206,9 @@ function MailBodyComposer({ defaultValue, defaultHtml, files, signatureText, onC } return ( -
+
-
+
editor?.chain().focus().undo().run()}> editor?.chain().focus().redo().run()}> @@ -2151,7 +2252,7 @@ function MailBodyComposer({ defaultValue, defaultHtml, files, signatureText, onC
{formatOpen && ( -
+
editor?.chain().focus().unsetAllMarks().clearNodes().run()}> @@ -2234,7 +2335,7 @@ function MailBodyComposer({ defaultValue, defaultHtml, files, signatureText, onC )}
{files.length > 0 && ( -
+
{files.map((file, index) => ( diff --git a/apps/web/src/pages/profile.tsx b/apps/web/src/pages/profile.tsx index 9933acf..8c200f7 100644 --- a/apps/web/src/pages/profile.tsx +++ b/apps/web/src/pages/profile.tsx @@ -10,6 +10,7 @@ import { applyTheme, getInitialTheme } from "@/lib/theme" import { DisplayMode, useDisplayMode } from "@/lib/display-mode" import { useMe } from "@/hooks/use-me" import { useLogout } from "@/hooks/use-logout" +import { useIsMobile } from "@/hooks/use-mobile" import { validatePasswordConfirm } from "@/lib/validation" import { Button } from "@/components/ui/button" import { PasswordInput } from "@/components/ui/password-input" @@ -21,6 +22,7 @@ import { Avatar, AvatarFallback } from "@/components/ui/avatar" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Checkbox } from "@/components/ui/checkbox" import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { Sheet, SheetContent, SheetTitle, SheetTrigger } from "@/components/ui/sheet" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Separator } from "@/components/ui/separator" import { ScrollArea } from "@/components/ui/scroll-area" @@ -60,6 +62,8 @@ export function ProfilePage() { const [displayMode, setDisplayMode] = useDisplayMode() const [blockedMailboxId, setBlockedMailboxId] = React.useState("all") const [ruleDialogOpen, setRuleDialogOpen] = React.useState(false) + const [mobileSidebarOpen, setMobileSidebarOpen] = React.useState(false) + const isMobile = useIsMobile() const themeMountedRef = React.useRef(false) const rawTab = params.get("tab") as Tab | null @@ -187,52 +191,78 @@ export function ProfilePage() { const logout = useLogout() async function copy(text: string) { await navigator.clipboard.writeText(text); toast({ title: "已复制" }) } - function setTab(next: Tab) { setParams(next === "profile" ? {} : { tab: next }) } + function setTab(next: Tab) { setParams(next === "profile" ? {} : { tab: next }); setMobileSidebarOpen(false) } function toggleSidebar() { sidebarCollapsed ? (sidebarPanelRef.current?.expand(14), setSidebarCollapsed(false)) : (sidebarPanelRef.current?.collapse(), setSidebarCollapsed(true)) } if (me.isLoading) return
加载中...
if (me.isError || !user) return
登录状态已失效
+ const sidebarContent = ( + + + setDarkMode((v) => !v)} onBack={() => navigate("/")} /> + + + + {!sidebarCollapsed && 个人中心} + + {tabKeys.map((key) => setTab(key)}>{tabs[key].icon}{!sidebarCollapsed && {tabs[key].label}})} + + + +
+ + {!isMobile && ( + <> + + + + )} +
+
+ ) + return ( -
+
- - setSidebarCollapsed(true)} onExpand={() => setSidebarCollapsed(false)}> - - - setDarkMode((v) => !v)} onBack={() => navigate("/")} /> - - - - {!sidebarCollapsed && 个人中心} - - {tabKeys.map((key) => setTab(key)}>{tabs[key].icon}{!sidebarCollapsed && {tabs[key].label}})} - - - -
- - - -
-
-
- - -
-
-
{tabs[tab].label}
-
-
{renderTab()}
-
-
-
+ {isMobile ? ( +
+
+ + + + + + 个人中心导航 +
{sidebarContent}
+
+
+
{tabs[tab].label}
+ +
+
{renderTab()}
+
+ ) : ( + + setSidebarCollapsed(true)} onExpand={() => setSidebarCollapsed(false)}> + {sidebarContent} + + + +
+
+
{tabs[tab].label}
+
+
{renderTab()}
+
+
+
+ )}
) - function renderTab() { if (tab === "mailboxes") return { setMailboxId(id); navigate("/") }} onApply={(payload) => applyMailbox.mutateAsync(payload).then(() => undefined)} /> if (tab === "clients") return @@ -439,7 +469,7 @@ function ApplyMailboxDialog({ options, pending, onApply }: { options: MailboxApp return ( - + 申请邮箱
@@ -450,7 +480,7 @@ function ApplyMailboxDialog({ options, pending, onApply }: { options: MailboxApp - + @@ -620,7 +650,7 @@ function SignaturesSection({ items, mailboxes, loading, pending, onCreate, onUpd { if (!open) setEditing(null) }}> - + 编辑签名 {editing && ( { e.preventDefault(); const form = new FormData(e.currentTarget); form.set("mailboxId", editingMailboxId === "all" ? "" : editingMailboxId); form.set("isDefault", editingIsDefault ? "on" : ""); onUpdate(editing.id, form); setEditing(null) }}> @@ -637,7 +667,7 @@ function SignaturesSection({ items, mailboxes, loading, pending, onCreate, onUpd setEditing((current) => current ? { ...current, isDefault: value === true } : current)} /> 设为当前范围默认签名 - + @@ -796,12 +826,12 @@ function RuleDialog({ open, onOpenChange, mailboxes, labels, pending, onCreate } return ( - - - 新建规则 + + + 新建规则 - -
+ +
setName(event.target.value)} placeholder="我的规则" /> @@ -860,7 +890,7 @@ function RuleDialog({ open, onOpenChange, mailboxes, labels, pending, onCreate }
- +