gitdataai/src/app/settings/NotificationsPage.tsx

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>
);
}