♻️ refactor: move notification settings page to business override slot

This commit is contained in:
YuTengjing
2026-03-25 20:39:44 +08:00
parent 48819b5bbf
commit a08a67e6ee
6 changed files with 28 additions and 137 deletions

View File

@@ -1,6 +1,22 @@
import type { NotificationSettings } from '@lobechat/types';
export const DEFAULT_NOTIFICATION_SETTINGS: NotificationSettings = {
email: { enabled: true },
inbox: { enabled: true },
email: {
enabled: true,
items: {
generation: {
image_generation_completed: true,
video_generation_completed: true,
},
},
},
inbox: {
enabled: true,
items: {
generation: {
image_generation_completed: true,
video_generation_completed: true,
},
},
},
};

View File

@@ -0,0 +1,3 @@
const Notification = () => null;
export default Notification;

View File

@@ -1,5 +1,6 @@
import Billing from '@/business/client/BusinessSettingPages/Billing';
import Credits from '@/business/client/BusinessSettingPages/Credits';
import Notification from '@/business/client/BusinessSettingPages/Notification';
import Plans from '@/business/client/BusinessSettingPages/Plans';
import Referral from '@/business/client/BusinessSettingPages/Referral';
import Usage from '@/business/client/BusinessSettingPages/Usage';
@@ -12,7 +13,6 @@ import Appearance from '../appearance';
import Creds from '../creds';
import Hotkey from '../hotkey';
import Memory from '../memory';
import Notification from '../notification';
import Profile from '../profile';
import Provider from '../provider';
import Proxy from '../proxy';

View File

@@ -22,9 +22,12 @@ export const componentMap = {
[SettingsTabs.Memory]: dynamic(() => import('../memory'), {
loading: loading('Settings > Memory'),
}),
[SettingsTabs.Notification]: dynamic(() => import('../notification'), {
loading: loading('Settings > Notification'),
}),
[SettingsTabs.Notification]: dynamic(
() => import('@/business/client/BusinessSettingPages/Notification'),
{
loading: loading('Settings > Notification'),
},
),
[SettingsTabs.About]: dynamic(() => import('../about'), {
loading: loading('Settings > About'),
}),

View File

@@ -1,114 +0,0 @@
'use client';
import { getDefaultNotificationSettings, SCENARIO_REGISTRY } from '@cloud/const';
import { type FormGroupItemType } from '@lobehub/ui';
import { Form, Icon, Skeleton } from '@lobehub/ui';
import { Switch } from 'antd';
import isEqual from 'fast-deep-equal';
import { Loader2Icon } from 'lucide-react';
import { memo, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FORM_STYLE } from '@/const/layoutTokens';
import { useUserStore } from '@/store/user';
import { settingsSelectors } from '@/store/user/selectors';
const defaults = getDefaultNotificationSettings();
/** Collect notification types grouped by category for a given channel */
const getCategoryTypesForChannel = (channel: string): { category: string; types: string[] }[] => {
const result: { category: string; types: string[] }[] = [];
for (const [category, categoryTypes] of Object.entries(SCENARIO_REGISTRY)) {
const types: string[] = [];
for (const [type, config] of Object.entries(categoryTypes)) {
if (config.channels.includes(channel)) types.push(type);
}
if (types.length > 0) result.push({ category, types });
}
return result;
};
const NotificationForm = memo(() => {
const { t } = useTranslation('setting');
const { t: tNotification } = useTranslation('notification');
const [form] = Form.useForm();
const { notification } = useUserStore(settingsSelectors.currentSettings, isEqual);
const [setSettings, isUserStateInit] = useUserStore((s) => [s.setSettings, s.isUserStateInit]);
const [loadingChannel, setLoadingChannel] = useState<string | null>(null);
const initialValues = useMemo(() => {
const result: Record<
string,
{ enabled: boolean; items: Record<string, Record<string, boolean>> }
> = {};
for (const [ch, def] of Object.entries(defaults)) {
const userChannel = notification?.[ch as keyof typeof notification];
const items: Record<string, Record<string, boolean>> = {};
for (const [category, typeDefaults] of Object.entries(def.items)) {
items[category] = { ...typeDefaults, ...userChannel?.items?.[category] };
}
result[ch] = {
enabled: userChannel?.enabled ?? def.enabled,
items,
};
}
return result;
}, [notification]);
if (!isUserStateInit) return <Skeleton active paragraph={{ rows: 3 }} title={false} />;
const buildChannelGroup = (channel: string): FormGroupItemType => {
const categoryTypes = getCategoryTypesForChannel(channel);
const isInbox = channel === 'inbox';
return {
children: [
{
children: <Switch />,
desc: isInbox ? t('notification.inbox.desc') : t('notification.email.desc'),
label: t('notification.enabled'),
layout: 'horizontal',
minWidth: undefined,
name: [channel, 'enabled'],
valuePropName: 'checked',
},
...categoryTypes.flatMap(({ category, types }) =>
types.map((type) => ({
children: <Switch />,
label: tNotification(`${type}_title`),
layout: 'horizontal' as const,
minWidth: undefined,
name: [channel, 'items', category, type],
valuePropName: 'checked',
})),
),
],
extra: loadingChannel === channel && (
<Icon spin icon={Loader2Icon} size={16} style={{ opacity: 0.5 }} />
),
title: isInbox ? t('notification.inbox.title') : t('notification.email.title'),
};
};
const channelGroups = Object.keys(defaults).map(buildChannelGroup);
return (
<Form
collapsible={false}
form={form}
initialValues={initialValues}
items={channelGroups}
itemsType={'group'}
variant={'filled'}
onValuesChange={async (changedValues) => {
const channel = Object.keys(changedValues)[0];
setLoadingChannel(channel);
await setSettings({ notification: changedValues });
setLoadingChannel(null);
}}
{...FORM_STYLE}
/>
);
});
export default NotificationForm;

View File

@@ -1,17 +0,0 @@
import { useTranslation } from 'react-i18next';
import SettingHeader from '@/routes/(main)/settings/features/SettingHeader';
import NotificationForm from './features/NotificationForm';
const Page = () => {
const { t } = useTranslation('setting');
return (
<>
<SettingHeader title={t('tab.notification')} />
<NotificationForm />
</>
);
};
export default Page;