336 lines
10 KiB
TypeScript
336 lines
10 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import {
|
|
getNotificationPreferences,
|
|
updateNotificationPreferences,
|
|
} from "@/client/api";
|
|
import type { NotificationPreferencesResponse } from "@/client/model";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Loader2 } from "lucide-react";
|
|
import { useSettingsDataCache } from "@/components/settings/SettingsDataCache";
|
|
import { NOTIFICATIONS_PAGE } from "@/css/app/styles";
|
|
|
|
const DIGEST_MODES = [
|
|
{ value: "instant", label: "即时" },
|
|
{ value: "daily", label: "每日摘要" },
|
|
{ value: "weekly", label: "每周摘要" },
|
|
{ value: "off", label: "关闭" },
|
|
];
|
|
|
|
const ToggleRow = ({
|
|
label,
|
|
desc,
|
|
checked,
|
|
onChange,
|
|
}: {
|
|
label: string;
|
|
desc: string;
|
|
checked: boolean;
|
|
onChange: (v: boolean) => void;
|
|
}) => (
|
|
<div className={NOTIFICATIONS_PAGE.toggleRow}>
|
|
<div className="flex-1 pr-4">
|
|
<p className={NOTIFICATIONS_PAGE.toggleLabel} style={{ color: "var(--text-primary)" }}>
|
|
{label}
|
|
</p>
|
|
<p className={NOTIFICATIONS_PAGE.toggleLabelDesc} style={{ color: "var(--text-muted)" }}>
|
|
{desc}
|
|
</p>
|
|
</div>
|
|
<Switch checked={checked} onCheckedChange={onChange} />
|
|
</div>
|
|
);
|
|
|
|
export function NotificationsPage() {
|
|
const { notificationPrefs: cachedPrefs, setNotificationPrefs: setCachedPrefs } = useSettingsDataCache();
|
|
const [, setPrefs] =
|
|
useState<NotificationPreferencesResponse | null>(cachedPrefs);
|
|
const [loading, setLoading] = useState(!cachedPrefs);
|
|
const [saving, setSaving] = useState(false);
|
|
const [form, setForm] = useState({
|
|
email_enabled: cachedPrefs?.email_enabled ?? true,
|
|
in_app_enabled: cachedPrefs?.in_app_enabled ?? true,
|
|
push_enabled: cachedPrefs?.push_enabled ?? false,
|
|
digest_mode: cachedPrefs?.digest_mode ?? "instant",
|
|
dnd_enabled: cachedPrefs?.dnd_enabled ?? false,
|
|
marketing_enabled: cachedPrefs?.marketing_enabled ?? false,
|
|
security_enabled: cachedPrefs?.security_enabled ?? true,
|
|
product_enabled: cachedPrefs?.product_enabled ?? true,
|
|
});
|
|
const [message, setMessage] = useState<{
|
|
type: "success" | "error";
|
|
text: string;
|
|
} | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (cachedPrefs) return;
|
|
(async () => {
|
|
try {
|
|
const res = await getNotificationPreferences();
|
|
const d = res.data.data!;
|
|
setPrefs(d);
|
|
setCachedPrefs(d);
|
|
setForm({
|
|
email_enabled: d.email_enabled,
|
|
in_app_enabled: d.in_app_enabled,
|
|
push_enabled: d.push_enabled,
|
|
digest_mode: d.digest_mode,
|
|
dnd_enabled: d.dnd_enabled,
|
|
marketing_enabled: d.marketing_enabled,
|
|
security_enabled: d.security_enabled,
|
|
product_enabled: d.product_enabled,
|
|
});
|
|
} catch {
|
|
setMessage({ type: "error", text: "加载通知偏好失败" });
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
})();
|
|
}, [cachedPrefs, setCachedPrefs]);
|
|
|
|
const handleSave = async () => {
|
|
try {
|
|
setSaving(true);
|
|
setMessage(null);
|
|
await updateNotificationPreferences({
|
|
email_enabled: form.email_enabled,
|
|
in_app_enabled: form.in_app_enabled,
|
|
push_enabled: form.push_enabled,
|
|
digest_mode: form.digest_mode,
|
|
dnd_enabled: form.dnd_enabled,
|
|
marketing_enabled: form.marketing_enabled,
|
|
security_enabled: form.security_enabled,
|
|
product_enabled: form.product_enabled,
|
|
});
|
|
setMessage({ type: "success", text: "通知设置已保存" });
|
|
} catch {
|
|
setMessage({ type: "error", text: "保存失败,请重试" });
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className={NOTIFICATIONS_PAGE.loadingState}>
|
|
<Loader2
|
|
className="w-6 h-6 animate-spin"
|
|
style={{ color: "var(--text-muted)" }}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<h1
|
|
className={NOTIFICATIONS_PAGE.pageHeader}
|
|
style={{ color: "var(--text-primary)" }}
|
|
>
|
|
通知
|
|
</h1>
|
|
<p
|
|
className={NOTIFICATIONS_PAGE.pageSubtitle}
|
|
style={{ color: "var(--text-muted)" }}
|
|
>
|
|
管理你的通知偏好
|
|
</p>
|
|
|
|
<div className={NOTIFICATIONS_PAGE.sectionGroup}>
|
|
{/* Notification Channels */}
|
|
<div>
|
|
<h3
|
|
className={NOTIFICATIONS_PAGE.sectionTitle}
|
|
style={{ color: "var(--text-muted)" }}
|
|
>
|
|
通知渠道
|
|
</h3>
|
|
<div
|
|
className={NOTIFICATIONS_PAGE.toggleContainer}
|
|
style={{ border: "1px solid var(--border-subtle)" }}
|
|
>
|
|
<ToggleRow
|
|
label="邮件通知"
|
|
desc="通过邮件接收通知"
|
|
checked={form.email_enabled}
|
|
onChange={(v) =>
|
|
setForm((f) => ({ ...f, email_enabled: v }))
|
|
}
|
|
/>
|
|
<div
|
|
className={NOTIFICATIONS_PAGE.toggleDivider}
|
|
style={{ borderTop: "1px solid var(--border-subtle)" }}
|
|
/>
|
|
<ToggleRow
|
|
label="应用内通知"
|
|
desc="在应用内接收通知提醒"
|
|
checked={form.in_app_enabled}
|
|
onChange={(v) =>
|
|
setForm((f) => ({ ...f, in_app_enabled: v }))
|
|
}
|
|
/>
|
|
<div
|
|
className={NOTIFICATIONS_PAGE.toggleDivider}
|
|
style={{ borderTop: "1px solid var(--border-subtle)" }}
|
|
/>
|
|
<ToggleRow
|
|
label="推送通知"
|
|
desc="通过浏览器推送接收通知"
|
|
checked={form.push_enabled}
|
|
onChange={(v) =>
|
|
setForm((f) => ({ ...f, push_enabled: v }))
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Digest Mode */}
|
|
<div>
|
|
<Label
|
|
className={NOTIFICATIONS_PAGE.sectionTitle}
|
|
style={{ color: "var(--text-muted)" }}
|
|
>
|
|
摘要模式
|
|
</Label>
|
|
<Select
|
|
value={form.digest_mode}
|
|
onValueChange={(v) =>
|
|
setForm((f) => ({ ...f, digest_mode: v }))
|
|
}
|
|
>
|
|
<SelectTrigger
|
|
className={NOTIFICATIONS_PAGE.selectTrigger}
|
|
style={{
|
|
backgroundColor: "var(--surface-elevated)",
|
|
borderColor: "var(--border-default)",
|
|
color: "var(--text-primary)",
|
|
}}
|
|
>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent
|
|
style={{
|
|
backgroundColor: "var(--surface-elevated)",
|
|
borderColor: "var(--border-default)",
|
|
color: "var(--text-primary)",
|
|
}}
|
|
>
|
|
{DIGEST_MODES.map((m) => (
|
|
<SelectItem key={m.value} value={m.value}>
|
|
{m.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Notification Types */}
|
|
<div>
|
|
<h3
|
|
className={NOTIFICATIONS_PAGE.sectionTitle}
|
|
style={{ color: "var(--text-muted)" }}
|
|
>
|
|
通知类型
|
|
</h3>
|
|
<div
|
|
className={NOTIFICATIONS_PAGE.toggleContainer}
|
|
style={{ border: "1px solid var(--border-subtle)" }}
|
|
>
|
|
<ToggleRow
|
|
label="安全通知"
|
|
desc="账户安全相关的通知"
|
|
checked={form.security_enabled}
|
|
onChange={(v) =>
|
|
setForm((f) => ({ ...f, security_enabled: v }))
|
|
}
|
|
/>
|
|
<div
|
|
className={NOTIFICATIONS_PAGE.toggleDivider}
|
|
style={{ borderTop: "1px solid var(--border-subtle)" }}
|
|
/>
|
|
<ToggleRow
|
|
label="产品更新"
|
|
desc="新功能和产品更新通知"
|
|
checked={form.product_enabled}
|
|
onChange={(v) =>
|
|
setForm((f) => ({ ...f, product_enabled: v }))
|
|
}
|
|
/>
|
|
<div
|
|
className={NOTIFICATIONS_PAGE.toggleDivider}
|
|
style={{ borderTop: "1px solid var(--border-subtle)" }}
|
|
/>
|
|
<ToggleRow
|
|
label="营销邮件"
|
|
desc="促销活动和优惠信息"
|
|
checked={form.marketing_enabled}
|
|
onChange={(v) =>
|
|
setForm((f) => ({ ...f, marketing_enabled: v }))
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* DND */}
|
|
<div>
|
|
<h3
|
|
className={NOTIFICATIONS_PAGE.sectionTitle}
|
|
style={{ color: "var(--text-muted)" }}
|
|
>
|
|
免打扰
|
|
</h3>
|
|
<div
|
|
className={NOTIFICATIONS_PAGE.toggleContainer}
|
|
style={{ border: "1px solid var(--border-subtle)" }}
|
|
>
|
|
<ToggleRow
|
|
label="启用免打扰"
|
|
desc="在指定时间段内不接收通知"
|
|
checked={form.dnd_enabled}
|
|
onChange={(v) =>
|
|
setForm((f) => ({ ...f, dnd_enabled: v }))
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{message && (
|
|
<div
|
|
className={NOTIFICATIONS_PAGE.message}
|
|
style={{
|
|
color:
|
|
message.type === "success"
|
|
? "var(--success)"
|
|
: "var(--destructive)",
|
|
}}
|
|
>
|
|
{message.text}
|
|
</div>
|
|
)}
|
|
|
|
<div className={NOTIFICATIONS_PAGE.buttonRow}>
|
|
<Button
|
|
onClick={handleSave}
|
|
disabled={saving}
|
|
size="sm"
|
|
style={{
|
|
backgroundColor: "var(--accent)",
|
|
color: "var(--accent-fg)",
|
|
}}
|
|
>
|
|
{saving && <Loader2 className="w-3.5 h-3.5 mr-2 animate-spin" />}
|
|
保存更改
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |