fix: guide admins to mailbox management

This commit is contained in:
zxyszx
2026-08-03 23:52:42 +08:00
parent 697ed236cc
commit 99b8b7dee8
3 changed files with 19 additions and 12 deletions
+1 -1
View File
@@ -1 +1 @@
1.2.4 1.2.5
+4
View File
@@ -69,6 +69,8 @@ const exactTranslations: Record<string, Translation> = {
"暂无标签": { "zh-TW": "暫無標籤", en: "No labels" }, "暂无标签": { "zh-TW": "暫無標籤", en: "No labels" },
"收起侧栏": { "zh-TW": "收合側欄", en: "Collapse sidebar" }, "收起侧栏": { "zh-TW": "收合側欄", en: "Collapse sidebar" },
"选择邮箱": { "zh-TW": "選擇信箱", en: "Select mailbox" }, "选择邮箱": { "zh-TW": "選擇信箱", en: "Select mailbox" },
"加载邮箱...": { "zh-TW": "載入信箱...", en: "Loading mailboxes..." },
"未创建邮箱": { "zh-TW": "尚未建立信箱", en: "No mailbox created" },
"没有可用邮箱": { "zh-TW": "沒有可用信箱", en: "No mailboxes available" }, "没有可用邮箱": { "zh-TW": "沒有可用信箱", en: "No mailboxes available" },
"邮箱地址已复制": { "zh-TW": "信箱地址已複製", en: "Mailbox address copied" }, "邮箱地址已复制": { "zh-TW": "信箱地址已複製", en: "Mailbox address copied" },
"打开导航": { "zh-TW": "開啟導覽", en: "Open navigation" }, "打开导航": { "zh-TW": "開啟導覽", en: "Open navigation" },
@@ -179,6 +181,8 @@ const exactTranslations: Record<string, Translation> = {
"还没有可用邮箱": { "zh-TW": "還沒有可用信箱", en: "No mailbox available" }, "还没有可用邮箱": { "zh-TW": "還沒有可用信箱", en: "No mailbox available" },
"请在个人中心申请邮箱,或联系管理员为当前账号分配邮箱。": { "zh-TW": "請在個人中心申請信箱,或聯絡管理員為目前帳號分配信箱。", en: "Apply for a mailbox in Profile, or contact an administrator to assign one to this account." }, "请在个人中心申请邮箱,或联系管理员为当前账号分配邮箱。": { "zh-TW": "請在個人中心申請信箱,或聯絡管理員為目前帳號分配信箱。", en: "Apply for a mailbox in Profile, or contact an administrator to assign one to this account." },
"前往个人中心": { "zh-TW": "前往個人中心", en: "Go to profile" }, "前往个人中心": { "zh-TW": "前往個人中心", en: "Go to profile" },
"请前往邮箱管理,为当前账号创建或分配邮箱。": { "zh-TW": "請前往信箱管理,為目前帳號建立或分配信箱。", en: "Open mailbox management to create or assign a mailbox to this account." },
"前往邮箱管理": { "zh-TW": "前往信箱管理", en: "Go to mailbox management" },
"无邮箱前台权限": { "zh-TW": "無信箱前台權限", en: "No mailbox access" }, "无邮箱前台权限": { "zh-TW": "無信箱前台權限", en: "No mailbox access" },
"当前账号未开启邮箱前台访问权限。": { "zh-TW": "目前帳號未開啟信箱前台存取權限。", en: "Mailbox access is not enabled for this account." }, "当前账号未开启邮箱前台访问权限。": { "zh-TW": "目前帳號未開啟信箱前台存取權限。", en: "Mailbox access is not enabled for this account." },
"无邮件查看权限": { "zh-TW": "無郵件檢視權限", en: "No mail read permission" }, "无邮件查看权限": { "zh-TW": "無郵件檢視權限", en: "No mail read permission" },
+14 -11
View File
@@ -11,7 +11,7 @@ import TextAlign from "@tiptap/extension-text-align"
import Placeholder from "@tiptap/extension-placeholder" import Placeholder from "@tiptap/extension-placeholder"
import { BackgroundColor, Color, FontFamily, FontSize, TextStyle } from "@tiptap/extension-text-style" import { BackgroundColor, Color, FontFamily, FontSize, TextStyle } from "@tiptap/extension-text-style"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { AlignCenter, AlignLeft, AlignRight, Archive, ArrowLeft, Ban, Bold, Calendar, Check, ChevronDown, Clock3, Code2, Copy, Download, Ellipsis, Eraser, Eye, FileText, Folder, Forward, Highlighter, History, Image, Inbox, IndentDecrease, IndentIncrease, Italic, Link, List, ListOrdered, Mail, MailCheck, MailQuestion, Moon, PanelLeftOpen, Paperclip, PencilLine, Plus, Quote, Redo2, RefreshCcw, Reply, RotateCcw, Search, Send, Settings, ShieldCheck, Signature, SlidersHorizontal, Smile, Star, Strikethrough, Sun, Trash2, Type, Underline, Undo2, Upload, X } from "lucide-react" import { AlignCenter, AlignLeft, AlignRight, Archive, ArrowLeft, Ban, Bold, Calendar, Check, ChevronDown, Clock3, Code2, Copy, Download, Ellipsis, Eraser, Eye, FileText, Folder, Forward, Highlighter, History, Image, Inbox, IndentDecrease, IndentIncrease, Italic, Link, List, ListOrdered, Mail, Mailbox as MailboxIcon, MailCheck, MailQuestion, Moon, PanelLeftOpen, Paperclip, PencilLine, Plus, Quote, Redo2, RefreshCcw, Reply, RotateCcw, Search, Send, Settings, ShieldCheck, Signature, SlidersHorizontal, Smile, Star, Strikethrough, Sun, Trash2, Type, Underline, Undo2, Upload, X } from "lucide-react"
import { api, ExternalImapAccount, ListResponse, Mailbox, MailFolder, MailLabel, MailMessage, MailSearchParams, SendPayload, DraftPayload, ScheduledSend, SendQueueItem, SendQueueAuditEvent, SendQueueStatus, PermissionLimits } from "@/lib/api" import { api, ExternalImapAccount, ListResponse, Mailbox, MailFolder, MailLabel, MailMessage, MailSearchParams, SendPayload, DraftPayload, ScheduledSend, SendQueueItem, SendQueueAuditEvent, SendQueueStatus, PermissionLimits } from "@/lib/api"
import { cn, decodeMimeHeader, formatBytes, formatDate, formatDateTime, generateLabelColor } from "@/lib/utils" import { cn, decodeMimeHeader, formatBytes, formatDate, formatDateTime, generateLabelColor } from "@/lib/utils"
import { applyTheme, getInitialTheme } from "@/lib/theme" import { applyTheme, getInitialTheme } from "@/lib/theme"
@@ -174,6 +174,7 @@ export function MailPage() {
const canDownloadAttachments = hasPermission(user, "mail.attachments.download") const canDownloadAttachments = hasPermission(user, "mail.attachments.download")
const canManageSignatures = hasPermission(user, "mail.signatures.manage") const canManageSignatures = hasPermission(user, "mail.signatures.manage")
const canViewUnknownMail = user?.role === "admin" const canViewUnknownMail = user?.role === "admin"
const canManageMailboxes = hasPermission(user, "admin.mailboxes.view")
const publicSettings = useQuery({ queryKey: ["public-settings"], queryFn: api.publicSettings }) const publicSettings = useQuery({ queryKey: ["public-settings"], queryFn: api.publicSettings })
const externalImapEnabled = publicSettings.data?.externalImapEnabled ?? false const externalImapEnabled = publicSettings.data?.externalImapEnabled ?? false
@@ -1186,9 +1187,9 @@ export function MailPage() {
<MailboxSwitcher <MailboxSwitcher
collapsed={sidebarCollapsed} collapsed={sidebarCollapsed}
mailboxes={mailboxList.data?.items || []} mailboxes={mailboxList.data?.items || []}
loading={mailboxList.isLoading}
selectedMailboxId={selectedMailboxId} selectedMailboxId={selectedMailboxId}
selectedMailbox={selectedMailbox} selectedMailbox={selectedMailbox}
fallbackAddress={selectedMailbox?.address || me.data?.user.email || ""}
unreadCount={mailboxUnreadCount} unreadCount={mailboxUnreadCount}
onSelect={switchMailbox} onSelect={switchMailbox}
/> />
@@ -1468,7 +1469,7 @@ export function MailPage() {
) : !canReadMail ? ( ) : !canReadMail ? (
<PermissionEmptyState title="无邮件查看权限" description="当前账号可以访问邮箱前台,但未开启邮件查看权限。" onOpenSettings={openSettings} /> <PermissionEmptyState title="无邮件查看权限" description="当前账号可以访问邮箱前台,但未开启邮件查看权限。" onOpenSettings={openSettings} />
) : !mailboxList.isLoading && !hasMailboxes && mailView !== "unknown" ? ( ) : !mailboxList.isLoading && !hasMailboxes && mailView !== "unknown" ? (
<NoMailboxState onOpenSettings={openSettings} /> <NoMailboxState canManageMailboxes={canManageMailboxes} onOpenSettings={openSettings} onManageMailboxes={() => navigate("/admin?section=mailboxes")} />
) : mailView === "scheduled" && canScheduleMail ? ( ) : mailView === "scheduled" && canScheduleMail ? (
<ScheduledSendView <ScheduledSendView
compact={compactMailLayout} compact={compactMailLayout}
@@ -2061,7 +2062,7 @@ function externalAccountSubtitle(account: ExternalImapAccount) {
return [name, account.host, mode].filter(Boolean).join(" · ") return [name, account.host, mode].filter(Boolean).join(" · ")
} }
function NoMailboxState({ onOpenSettings }: { onOpenSettings: () => void }) { function NoMailboxState({ canManageMailboxes, onOpenSettings, onManageMailboxes }: { canManageMailboxes: boolean; onOpenSettings: () => void; onManageMailboxes: () => 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">
<div className="w-full max-w-md rounded-lg border border-dashed p-8 text-center"> <div className="w-full max-w-md rounded-lg border border-dashed p-8 text-center">
@@ -2069,9 +2070,10 @@ function NoMailboxState({ onOpenSettings }: { onOpenSettings: () => void }) {
<Mail className="h-5 w-5 text-muted-foreground" /> <Mail className="h-5 w-5 text-muted-foreground" />
</div> </div>
<div className="text-lg font-semibold"></div> <div className="text-lg font-semibold"></div>
<div className="mt-2 text-sm text-muted-foreground"></div> <div className="mt-2 text-sm text-muted-foreground">{canManageMailboxes ? "请前往邮箱管理,为当前账号创建或分配邮箱。" : "请在个人中心申请邮箱,或联系管理员为当前账号分配邮箱。"}</div>
<Button className="mt-5" onClick={onOpenSettings}> <Button className="mt-5" onClick={canManageMailboxes ? onManageMailboxes : onOpenSettings}>
<Settings className="h-4 w-4" /> {canManageMailboxes ? <MailboxIcon className="h-4 w-4" /> : <Settings className="h-4 w-4" />}
{canManageMailboxes ? "前往邮箱管理" : "前往个人中心"}
</Button> </Button>
</div> </div>
</div> </div>
@@ -3215,10 +3217,11 @@ function UnreadBadge({ count, tone = "danger" }: { count?: number; tone?: "dange
) )
} }
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 }) { function MailboxSwitcher({ collapsed, mailboxes, loading, selectedMailboxId, selectedMailbox, unreadCount, onSelect }: { collapsed: boolean; mailboxes: Mailbox[]; loading: boolean; selectedMailboxId: string; selectedMailbox?: Mailbox; unreadCount: number; onSelect: (mailboxId: string) => void }) {
const [mailboxQuery, setMailboxQuery] = React.useState("") const [mailboxQuery, setMailboxQuery] = React.useState("")
const isAllSelected = selectedMailboxId === "all" const isAllSelected = selectedMailboxId === "all"
const displayAddress = isAllSelected ? "全部邮箱" : selectedMailbox?.address || fallbackAddress || "选择邮箱" const mailboxUnavailable = loading || mailboxes.length === 0
const displayAddress = loading ? "加载邮箱..." : mailboxes.length === 0 ? "未创建邮箱" : isAllSelected ? "全部邮箱" : selectedMailbox?.address || "选择邮箱"
const selectedUnreadCount = isAllSelected ? unreadCount : (selectedMailbox?.unreadCount ?? unreadCount) const selectedUnreadCount = isAllSelected ? unreadCount : (selectedMailbox?.unreadCount ?? unreadCount)
const normalizedQuery = mailboxQuery.trim().toLowerCase() const normalizedQuery = mailboxQuery.trim().toLowerCase()
const showAllMailboxOption = !normalizedQuery || "全部邮箱".includes(normalizedQuery) || "all".includes(normalizedQuery) const showAllMailboxOption = !normalizedQuery || "全部邮箱".includes(normalizedQuery) || "all".includes(normalizedQuery)
@@ -3232,13 +3235,13 @@ function MailboxSwitcher({ collapsed, mailboxes, selectedMailboxId, selectedMail
return ( return (
<DropdownMenu onOpenChange={(open) => { if (!open) setMailboxQuery("") }}> <DropdownMenu onOpenChange={(open) => { if (!open) setMailboxQuery("") }}>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="outline" className={cn("h-8 min-w-0 flex-1 justify-start gap-1.5 overflow-hidden rounded-md border-input bg-background px-2 text-left font-normal shadow-none hover:bg-background", collapsed && "w-8 flex-none justify-center px-0")} title={displayAddress}> <Button disabled={mailboxUnavailable} variant="outline" className={cn("h-8 min-w-0 flex-1 justify-start gap-1.5 overflow-hidden rounded-md border-input bg-background px-2 text-left font-normal shadow-none hover:bg-background", collapsed && "w-8 flex-none justify-center px-0")} title={displayAddress}>
<Mail className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> <Mail className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
{!collapsed && ( {!collapsed && (
<> <>
<span className="min-w-0 flex-1 truncate text-[13px] font-medium">{displayAddress}</span> <span className="min-w-0 flex-1 truncate text-[13px] font-medium">{displayAddress}</span>
<UnreadBadge count={selectedUnreadCount} /> <UnreadBadge count={selectedUnreadCount} />
<ChevronDown className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> {!mailboxUnavailable && <ChevronDown className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />}
</> </>
)} )}
</Button> </Button>