吸收数字员工通知订阅策略
This commit is contained in:
@@ -1,13 +1,16 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Bell, Check, Mail, MessageSquare, X } from 'lucide-react';
|
||||
import { Bell, Check, Mail, MessageSquare, Settings2, X } from 'lucide-react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { RoleProvider } from '@/contexts/RoleContext';
|
||||
import {
|
||||
dismissNotification,
|
||||
getNotificationPreferences,
|
||||
getNotifications,
|
||||
getUnreadNotificationCount,
|
||||
markNotificationRead,
|
||||
patchNotificationPreferences,
|
||||
replyToNotification,
|
||||
type NotificationPreferences,
|
||||
type UserNotification,
|
||||
} from '@/lib/api';
|
||||
import CommandDeck from './CommandDeck';
|
||||
@@ -26,6 +29,28 @@ const TABS: { id: DigitalTab; label: string }[] = [
|
||||
];
|
||||
const DIGITAL_EMPLOYEE_CLOSE_MESSAGE = 'qimingclaw:digital-close';
|
||||
|
||||
const NOTIFICATION_LEVEL_OPTIONS: Array<{ id: UserNotification['level']; label: string }> = [
|
||||
{ id: 'info', label: '消息' },
|
||||
{ id: 'warn', label: '提醒' },
|
||||
{ id: 'error', label: '异常' },
|
||||
{ id: 'action', label: '待处理' },
|
||||
];
|
||||
|
||||
const NOTIFICATION_KIND_OPTIONS: Array<{ id: UserNotification['kind']; label: string }> = [
|
||||
{ id: 'task_finished', label: '任务完成' },
|
||||
{ id: 'task_failed', label: '任务异常' },
|
||||
{ id: 'need_approval', label: '审批' },
|
||||
{ id: 'need_input', label: '输入' },
|
||||
{ id: 'task_progress', label: '进度' },
|
||||
];
|
||||
|
||||
const DEFAULT_NOTIFICATION_PREFERENCES: NotificationPreferences = {
|
||||
enabled: true,
|
||||
muted_kinds: [],
|
||||
muted_levels: [],
|
||||
updated_at: null,
|
||||
};
|
||||
|
||||
function isDigitalTab(value: string | null): value is DigitalTab {
|
||||
return Boolean(value && TABS.some(tab => tab.id === value));
|
||||
}
|
||||
@@ -104,6 +129,8 @@ export default function DigitalEmployeeShell() {
|
||||
const [notifications, setNotifications] = useState<UserNotification[]>([]);
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
const [notificationsLoading, setNotificationsLoading] = useState(false);
|
||||
const [notificationSettingsOpen, setNotificationSettingsOpen] = useState(false);
|
||||
const [notificationPreferences, setNotificationPreferences] = useState<NotificationPreferences>(DEFAULT_NOTIFICATION_PREFERENCES);
|
||||
const [replyDraftById, setReplyDraftById] = useState<Record<string, string>>({});
|
||||
|
||||
const refreshNotifications = useCallback(async () => {
|
||||
@@ -115,9 +142,13 @@ export default function DigitalEmployeeShell() {
|
||||
]);
|
||||
setNotifications(items);
|
||||
setUnreadCount(count);
|
||||
void getNotificationPreferences()
|
||||
.then(setNotificationPreferences)
|
||||
.catch(() => setNotificationPreferences(DEFAULT_NOTIFICATION_PREFERENCES));
|
||||
} catch {
|
||||
setNotifications([]);
|
||||
setUnreadCount(0);
|
||||
setNotificationPreferences(DEFAULT_NOTIFICATION_PREFERENCES);
|
||||
} finally {
|
||||
setNotificationsLoading(false);
|
||||
}
|
||||
@@ -164,6 +195,26 @@ export default function DigitalEmployeeShell() {
|
||||
await refreshNotifications();
|
||||
}, [refreshNotifications, replyDraftById]);
|
||||
|
||||
const updateNotificationPreferences = useCallback(async (patch: Partial<NotificationPreferences>) => {
|
||||
const next = await patchNotificationPreferences(patch);
|
||||
setNotificationPreferences(next);
|
||||
await refreshNotifications();
|
||||
}, [refreshNotifications]);
|
||||
|
||||
const toggleMutedLevel = useCallback((level: UserNotification['level']) => {
|
||||
const muted = new Set(notificationPreferences.muted_levels);
|
||||
if (muted.has(level)) muted.delete(level);
|
||||
else muted.add(level);
|
||||
void updateNotificationPreferences({ muted_levels: Array.from(muted) });
|
||||
}, [notificationPreferences.muted_levels, updateNotificationPreferences]);
|
||||
|
||||
const toggleMutedKind = useCallback((kind: UserNotification['kind']) => {
|
||||
const muted = new Set(notificationPreferences.muted_kinds);
|
||||
if (muted.has(kind)) muted.delete(kind);
|
||||
else muted.add(kind);
|
||||
void updateNotificationPreferences({ muted_kinds: Array.from(muted) });
|
||||
}, [notificationPreferences.muted_kinds, updateNotificationPreferences]);
|
||||
|
||||
return (
|
||||
<RoleProvider>
|
||||
<div className="de-shell">
|
||||
@@ -206,14 +257,67 @@ export default function DigitalEmployeeShell() {
|
||||
{notificationsOpen && (
|
||||
<div className="de-notification-popover">
|
||||
<div className="de-notification-head">
|
||||
<div>
|
||||
<div className="de-notification-head-title">
|
||||
<strong>通知</strong>
|
||||
<span>{unreadCount > 0 ? `${unreadCount} 条未读` : '暂无未读'}</span>
|
||||
</div>
|
||||
<button type="button" className="de-header-icon de-notification-close" title="关闭" onClick={() => setNotificationsOpen(false)}>
|
||||
<X size={14} />
|
||||
</button>
|
||||
<div className="de-notification-head-actions">
|
||||
<button
|
||||
type="button"
|
||||
className={`de-header-icon de-notification-close ${notificationSettingsOpen ? 'active' : ''}`}
|
||||
title="通知设置"
|
||||
onClick={() => setNotificationSettingsOpen((open) => !open)}
|
||||
>
|
||||
<Settings2 size={14} />
|
||||
</button>
|
||||
<button type="button" className="de-header-icon de-notification-close" title="关闭" onClick={() => setNotificationsOpen(false)}>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{notificationSettingsOpen && (
|
||||
<div className="de-notification-preferences">
|
||||
<label className="de-notification-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={notificationPreferences.enabled}
|
||||
onChange={(event) => { void updateNotificationPreferences({ enabled: event.target.checked }); }}
|
||||
/>
|
||||
<span>接收通知</span>
|
||||
</label>
|
||||
<div className="de-notification-preference-group">
|
||||
<span>静音等级</span>
|
||||
<div className="de-notification-segment-row">
|
||||
{NOTIFICATION_LEVEL_OPTIONS.map((option) => (
|
||||
<button
|
||||
key={option.id}
|
||||
type="button"
|
||||
className={notificationPreferences.muted_levels.includes(option.id) ? 'active' : ''}
|
||||
title={`${option.label}静音`}
|
||||
onClick={() => toggleMutedLevel(option.id)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="de-notification-preference-group">
|
||||
<span>静音类型</span>
|
||||
<div className="de-notification-toggle-grid">
|
||||
{NOTIFICATION_KIND_OPTIONS.map((option) => (
|
||||
<label key={option.id}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={notificationPreferences.muted_kinds.includes(option.id)}
|
||||
onChange={() => toggleMutedKind(option.id)}
|
||||
/>
|
||||
<span>{option.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="de-notification-list">
|
||||
{notificationsLoading && <div className="de-notification-empty">加载中</div>}
|
||||
{!notificationsLoading && notifications.length === 0 && <div className="de-notification-empty">暂无通知</div>}
|
||||
|
||||
Reference in New Issue
Block a user