From 3d3a251af292e9dc0f10a1b8cb99ebcca8a6b4fd Mon Sep 17 00:00:00 2001 From: zxyszx <299979470+zxyszx@users.noreply.github.com> Date: Wed, 12 Aug 2026 01:15:42 +0800 Subject: [PATCH] release: prepare v1.2.28 --- .github/release-notes/v1.2.28.md | 8 +++ VERSION | 2 +- apps/web/src/components/ui/dialog.tsx | 2 +- apps/web/src/pages/admin.tsx | 12 ++-- apps/web/src/pages/mail.tsx | 23 ++++--- apps/web/src/pages/profile.tsx | 95 ++++++++++++++------------- 6 files changed, 82 insertions(+), 60 deletions(-) create mode 100644 .github/release-notes/v1.2.28.md diff --git a/.github/release-notes/v1.2.28.md b/.github/release-notes/v1.2.28.md new file mode 100644 index 0000000..c9e6c05 --- /dev/null +++ b/.github/release-notes/v1.2.28.md @@ -0,0 +1,8 @@ +- 收信规则页新增规则名称搜索,支持按字母或完整名称快速筛选,并提供清空搜索和空结果提示。 +- 优化规则列表的桌面端与手机端布局,规则名称、适用邮箱、条件和动作可完整换行显示,操作按钮不再挤压内容。 +- 调整新建规则工具栏与表单对齐,搜索框与规则列表等宽,“所有条件”选择器与下方条件字段保持同一左边界。 +- 邮箱侧栏的“文件夹”和“标签”支持点击展开或收起,桌面端和手机侧栏保持一致交互。 +- 降低所有对话框的背景遮罩深度,手机端和桌面端分别使用更轻的灰色层级,避免打开弹窗后页面过黑。 +- 重排“被拦截邮件”页面,统一标题、统计和新增入口,长邮箱地址与拦截原因会自动换行,不再超出边框。 +- 重排“邮件清理”页面的统计卡片和清理操作,修复窄屏文字截断、按钮挤压和横向溢出。 +- 统一前后台文字按钮样式,移除“新建规则”、“新增拦截”、权限配置、账号、域名、邮箱和转发等命令前多余的加号图标。 diff --git a/VERSION b/VERSION index 3b781da..f6b839d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.27 +1.2.28 diff --git a/apps/web/src/components/ui/dialog.tsx b/apps/web/src/components/ui/dialog.tsx index 9dbeaa0..e5e73e7 100644 --- a/apps/web/src/components/ui/dialog.tsx +++ b/apps/web/src/components/ui/dialog.tsx @@ -19,7 +19,7 @@ const DialogOverlay = React.forwardRef< - + ) return ( @@ -2015,7 +2015,7 @@ function CreateUserDialog(_props: { permissionGroups: PermissionGroup[] }) { }) return ( - + 创建账号
{ event.preventDefault(); create.mutate(new FormData(event.currentTarget)) }}> @@ -2103,7 +2103,7 @@ function EditMailboxDialog({ mailbox, users, open, onOpenChange }: { mailbox: Ma function CreateDomainDialog() { const qc = useQueryClient(); const { toast } = useToast(); const [open, setOpen] = React.useState(false) const mut = useMutation({ mutationFn: (form: FormData) => api.createDomain(String(form.get("name"))), onSuccess: () => { invalidateAdmin(qc); setOpen(false); toast({ title: "域名已创建" }) }, onError: (e) => toast({ title: "创建失败", description: e.message }) }) - return 添加域名 { e.preventDefault(); mut.mutate(new FormData(e.currentTarget)) }}>
+ return 添加域名
{ e.preventDefault(); mut.mutate(new FormData(e.currentTarget)) }}>
} function CreateMailboxDialog({ domains, users }: { domains: Domain[]; users: AdminUser[] }) { @@ -2133,7 +2133,7 @@ function CreateMailboxDialog({ domains, users }: { domains: Domain[]; users: Adm }) return ( - + 创建邮箱
{ e.preventDefault(); mut.mutate(new FormData(e.currentTarget)) }}> @@ -2163,7 +2163,7 @@ function CreateAliasDialog({ domains }: { domains: Domain[] }) { const qc = useQueryClient(); const { toast } = useToast(); const [open, setOpen] = React.useState(false); const [domainId, setDomainId] = React.useState("") React.useEffect(() => { if (!domainId && domains[0]) setDomainId(domains[0].id) }, [domains, domainId]) const mut = useMutation({ mutationFn: (form: FormData) => api.createAlias({ domainId, source: String(form.get("source")), destination: String(form.get("destination")), enabled: true }), onSuccess: () => { invalidateAdmin(qc); setOpen(false); toast({ title: "转发已创建" }) }, onError: (e) => toast({ title: "创建失败", description: e.message }) }) - return 创建邮件转发 { e.preventDefault(); mut.mutate(new FormData(e.currentTarget)) }}>
+ return 创建邮件转发
{ e.preventDefault(); mut.mutate(new FormData(e.currentTarget)) }}>
} function DNSPanel({ domain, embedded = false }: { domain?: Domain; embedded?: boolean }) { diff --git a/apps/web/src/pages/mail.tsx b/apps/web/src/pages/mail.tsx index d935686..65bd190 100644 --- a/apps/web/src/pages/mail.tsx +++ b/apps/web/src/pages/mail.tsx @@ -202,6 +202,8 @@ export function MailPage() { const [selectedMailboxId, setSelectedMailboxId] = React.useState("all") const [selectedExternalAccountId, setSelectedExternalAccountId] = React.useState("") const [expandedExternalAccountIds, setExpandedExternalAccountIds] = React.useState([]) + const [foldersExpanded, setFoldersExpanded] = React.useState(true) + const [labelsExpanded, setLabelsExpanded] = React.useState(true) const [externalFolder, setExternalFolder] = React.useState("INBOX") const [darkMode, setDarkMode] = React.useState(getInitialTheme) const [language, setLanguage] = useLanguage() @@ -1438,7 +1440,10 @@ export function MailPage() { {(customMailMenuItems.length > 0 || canManageFolders) && {!sidebarCollapsed && (
- 文件夹 + {canManageFolders && (
)} - + {foldersExpanded && {customMailMenuItems.map((item) => ( )} - + }
} {(canReadMail || canManageLabels) && {!sidebarCollapsed && (
- 标签 + {canManageCurrentMailboxLabels && (
)} - + {labelsExpanded && {canReadMail && labelItems.map((label) => { const dotColor = labelDotColor(label) @@ -1558,7 +1566,7 @@ export function MailPage() { {labels.isLoading && } {!sidebarCollapsed && labels.isError && { void labels.refetch() }} />} - + } } {canCreate && ( )} {customFolder && (canReorder || canDelete) && ( @@ -3386,7 +3394,6 @@ function NewLabelButton({ collapsed, pending, onCreate, editing, onEditingChange } return ( setEditingState(true)}> - 新建标签 ) diff --git a/apps/web/src/pages/profile.tsx b/apps/web/src/pages/profile.tsx index 1132d3c..ccff76d 100644 --- a/apps/web/src/pages/profile.tsx +++ b/apps/web/src/pages/profile.tsx @@ -693,8 +693,8 @@ function SettingsCard({ title, subtitle, action, children, className, contentCla
-

{title}

- {subtitle &&

{subtitle}

} +

{title}

+ {subtitle &&

{subtitle}

}
{action &&
{action}
}
@@ -1626,7 +1626,7 @@ function ExternalImapDialog({ account, mailboxId, disabled, pending, onSubmit }: return ( {account ? "编辑外部 IMAP" : "添加外部 IMAP"} @@ -2221,16 +2221,13 @@ function CleanupSection({ mailbox, stats, showStats, pending, onCleanup }: { mai }) } return ( -
+
{showStats && } - - 清理当前邮箱 - + } title="归档已读收件箱" disabled={!mailbox || pending} onClick={() => confirmCleanup("archive-read-inbox", "归档已读收件箱?")} /> } title="清空垃圾邮件" disabled={!mailbox || pending} onClick={() => confirmCleanup("empty-spam", "清空垃圾邮件?", true)} /> } title="清空回收站" disabled={!mailbox || pending} onClick={() => confirmCleanup("empty-trash", "清空回收站?", true)} /> - - + { if (!open) setPendingConfirm(null) }} onConfirm={() => pendingConfirm?.onConfirm()} />
) @@ -2261,6 +2258,9 @@ const ruleActionLabels: Record = { archive: "移 function RulesSection({ items, mailboxes, labels, verifiedEmails, open, onOpenChange, onCreate, onUpdate, onToggle, onMove, onApply, onDelete, pending }: { items: MailRule[]; mailboxes: Mailbox[]; labels: MailLabel[]; verifiedEmails: string[]; open: boolean; onOpenChange: (open: boolean) => void; onCreate: (payload: RuleCreatePayload) => void; onUpdate: (id: string, payload: RuleCreatePayload) => void; onToggle: (item: MailRule) => void; onMove: (id: string, direction: "up" | "down") => void; onApply: (id: string) => void; onDelete: (id: string) => void; pending: boolean }) { const [editingRule, setEditingRule] = React.useState(null) + const [query, setQuery] = React.useState("") + const normalizedQuery = query.trim().toLocaleLowerCase() + const filteredItems = items.map((item, index) => ({ item, index })).filter(({ item }) => !normalizedQuery || item.name.toLocaleLowerCase().includes(normalizedQuery)) function setDialogOpen(next: boolean) { if (!next) setEditingRule(null) @@ -2269,12 +2269,18 @@ function RulesSection({ items, mailboxes, labels, verifiedEmails, open, onOpenCh return (
-
- +
+
+ + setQuery(event.target.value)} placeholder="搜索规则名称" aria-label="搜索规则" className="h-10 pl-9 pr-10" /> + {query && } +
+
- {items.map((item, index) => mailbox.id === item.mailboxId)?.address || "指定邮箱" : "全部邮箱"} pending={pending} onEdit={() => { setEditingRule(item); onOpenChange(true) }} onToggle={() => onToggle(item)} onMove={(direction) => onMove(item.id, direction)} onApply={() => onApply(item.id)} onDelete={onDelete} />)} + {filteredItems.map(({ item, index }) => mailbox.id === item.mailboxId)?.address || "指定邮箱" : "全部邮箱"} pending={pending} onEdit={() => { setEditingRule(item); onOpenChange(true) }} onToggle={() => onToggle(item)} onMove={(direction) => onMove(item.id, direction)} onApply={() => onApply(item.id)} onDelete={onDelete} />)} {items.length === 0 && } text="暂无收件规则" description="新建规则后,可自动标记、移动或转发符合条件的邮件。" className="min-h-[180px] border-solid bg-card" />} + {items.length > 0 && filteredItems.length === 0 && } text="没有找到规则" description={`没有名称包含“${query.trim()}”的规则。`} className="min-h-[180px] border-solid bg-card" />}
editingRule ? onUpdate(editingRule.id, payload) : onCreate(payload)} />
@@ -2349,10 +2355,10 @@ function RuleDialog({ open, onOpenChange, mailboxes, labels, verifiedEmails, pen
-
- 当新邮件到达时,满足以下 +
+
当新邮件到达时,满足以下
@@ -2487,26 +2493,26 @@ function RuleListItem({ item, index, count, mailboxLabel, pending, onEdit, onTog const conditionText = ruleConditionSummary(item.conditions, item.fromContains, item.subjectContains) const actionText = item.actions.map(ruleActionSummary).filter(Boolean).join(";") || "无动作" return ( -
-
-
-

{item.name}

+
+
+
+

{item.name}

{item.enabled ? "已启用" : "已停用"}
-
-

适用: {mailboxLabel}

-

条件: {conditionText}

-

动作: {actionText}

+
+

适用:{mailboxLabel}

+

条件:{conditionText}

+

动作:{actionText}

-
- - +
+ + - - - - + + + +
{ onDelete(item.id); setConfirmOpen(false) }} />
@@ -2577,17 +2583,18 @@ function BlockedSection({ items, mailboxes, mailboxId, spamCount, onMailboxChang setDialogOpen(false) } return ( -
-
-
共 {spamCount} 封垃圾邮件 · {items.length} 条发件人拦截规则
- -
- +
+ setDialogOpen(true)}>新增拦截} + contentClassName="space-y-2" + > {items.map((item) => ( -
+
-
{item.email}
-
{item.mailboxId ? mailboxes.find((m) => m.id === item.mailboxId)?.address : "全部邮箱"}{item.reason ? ` · ${item.reason}` : ""}
+
{item.email}
+
{item.mailboxId ? mailboxes.find((m) => m.id === item.mailboxId)?.address : "全部邮箱"}{item.reason ? ` · ${item.reason}` : ""}
@@ -2816,13 +2823,13 @@ function StatsSummary({ stats }: { stats?: MailStats }) { { label: stats?.quotaBytes ? `容量 ${Math.min(stats.quotaUsedPct || 0, 999).toFixed(1)}%` : "容量", value: quotaLabel, icon: , tone: "bg-slate-100 text-slate-700" }, ] return ( -
+
{cards.map((card) => ( - - + +
{card.icon}
-
{card.value}
+
{card.value}
{card.label}
@@ -2832,7 +2839,7 @@ function StatsSummary({ stats }: { stats?: MailStats }) { ) } -function CleanupButton({ icon, title, disabled, onClick }: { icon: React.ReactNode; title: string; disabled: boolean; onClick: () => void }) { return } +function CleanupButton({ icon, title, disabled, onClick }: { icon: React.ReactNode; title: string; disabled: boolean; onClick: () => void }) { return } function MailboxSelect({ value, mailboxes, onChange }: { value: string; mailboxes: Mailbox[]; onChange: (value: string) => void }) { return } function Field({ label, children }: { label: string; children: React.ReactNode }) { return
{children}
} function EmptyState({ text, description, icon, action, className }: { text: string; description?: string; icon?: React.ReactNode; action?: React.ReactNode; className?: string }) {