'use client';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
AlertCircle,
CalendarDays,
CheckCircle2,
Inbox,
Loader2,
Mail,
MailOpen,
RefreshCw,
Save,
Send,
Settings2,
Unplug,
} from 'lucide-react';
import {
connectOutlook,
disconnectOutlook,
getOutlookEvents,
getOutlookMessageDetail,
getOutlookMessages,
getOutlookOverview,
getOutlookStatus,
testOutlookConnection,
} from '@/lib/api';
import type {
OutlookConnectionPayload,
OutlookConnectionTestResult,
OutlookEventListResponse,
OutlookEventSummary,
OutlookMessageDetail,
OutlookMessageListResponse,
OutlookMessageSummary,
OutlookPageInfo,
OutlookStatus,
} from '@/types';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Skeleton } from '@/components/ui/skeleton';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Separator } from '@/components/ui/separator';
import { Switch } from '@/components/ui/switch';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import type { AppLocale } from '@/lib/i18n/core';
import { pickAppText } from '@/lib/i18n/core';
import { useAppI18n } from '@/lib/i18n/provider';
type OutlookFormState = OutlookConnectionPayload;
type OutlookView = 'inbox' | 'sent' | 'calendar' | 'settings';
type OutlookMailboxView = 'inbox' | 'sent';
const MAILBOX_PAGE_SIZE = 20;
const CALENDAR_PAGE_SIZE = 100;
const EMPTY_FORM: OutlookFormState = {
email: '',
password: '',
username: '',
domain: '',
service_endpoint: '',
server: '',
autodiscover: false,
default_timezone: 'Asia/Shanghai',
};
function toFormState(status: OutlookStatus | null): OutlookFormState {
const defaults = status?.defaults.fields;
const saved = status?.saved;
return {
email: saved?.email || '',
password: '',
username: saved?.username || '',
domain: saved?.domain || defaults?.domain || '',
service_endpoint: saved?.service_endpoint || defaults?.service_endpoint || '',
server: saved?.server || defaults?.server || '',
autodiscover: saved?.autodiscover ?? defaults?.autodiscover ?? false,
default_timezone: saved?.default_timezone || defaults?.default_timezone || 'Asia/Shanghai',
};
}
function formatDateTime(value?: string | null, locale: AppLocale = 'zh-CN'): string {
if (!value) return '-';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
}).format(date);
}
function toLocalDateKey(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
function formatDateKey(value?: string | null): string | null {
if (!value) return null;
const date = new Date(value);
if (Number.isNaN(date.getTime())) return null;
return toLocalDateKey(date);
}
function formatDayLabel(value: Date, locale: AppLocale = 'zh-CN'): string {
return new Intl.DateTimeFormat(locale, {
month: '2-digit',
day: '2-digit',
weekday: 'short',
}).format(value);
}
function formatTime(value?: string | null, locale: AppLocale = 'zh-CN'): string {
if (!value) return '-';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return new Intl.DateTimeFormat(locale, {
hour: '2-digit',
minute: '2-digit',
}).format(date);
}
function buildCalendarRange(anchorKey: string): { startTime: string; endTime: string } {
const anchor = new Date(`${anchorKey}T00:00:00`);
const start = Number.isNaN(anchor.getTime()) ? new Date() : anchor;
start.setHours(0, 0, 0, 0);
const end = new Date(start);
end.setDate(end.getDate() + 7);
return {
startTime: start.toISOString(),
endTime: end.toISOString(),
};
}
function mailboxLabel(
mailbox?:
| {
emailAddress?: {
name?: string | null;
address?: string | null;
} | null;
}
| null
): string {
const name = mailbox?.emailAddress?.name?.trim();
const address = mailbox?.emailAddress?.address?.trim();
if (name && address) return `${name} <${address}>`;
return address || name || '-';
}
function statusVariant(connected: boolean): 'default' | 'secondary' {
return connected ? 'default' : 'secondary';
}
function sanitizeEmailHtml(html: string): string {
if (typeof window === 'undefined') return html;
const parser = new DOMParser();
const documentRef = parser.parseFromString(html, 'text/html');
const blockedSelectors = [
'script',
'style',
'iframe',
'object',
'embed',
'form',
'input',
'button',
'textarea',
'select',
'meta',
'link',
'base',
'img',
];
const removableFormattingAttrs = new Set([
'style',
'class',
'id',
'bgcolor',
'color',
'face',
'width',
'height',
'cellpadding',
'cellspacing',
'align',
]);
for (const selector of blockedSelectors) {
documentRef.querySelectorAll(selector).forEach((node) => node.remove());
}
documentRef.querySelectorAll('*').forEach((element) => {
for (const attr of Array.from(element.attributes)) {
const name = attr.name.toLowerCase();
const value = attr.value.trim();
if (removableFormattingAttrs.has(name)) {
element.removeAttribute(attr.name);
continue;
}
if (name.startsWith('on')) {
element.removeAttribute(attr.name);
continue;
}
if (name === 'srcdoc') {
element.removeAttribute(attr.name);
continue;
}
if ((name === 'href' || name === 'src') && value) {
const normalized = value.toLowerCase();
if (
normalized.startsWith('javascript:')
|| normalized.startsWith('data:')
|| normalized.startsWith('file:')
) {
element.removeAttribute(attr.name);
}
}
}
if (element.tagName.toLowerCase() === 'a') {
element.setAttribute('target', '_blank');
element.setAttribute('rel', 'noreferrer noopener');
}
});
return documentRef.body.innerHTML;
}
function buildEmailPreviewDocument(html: string, locale: AppLocale = 'zh-CN'): string {
return `
${sanitizeEmailHtml(html)}
`;
}
function renderPlainText(content: string): React.ReactNode[] {
const urlPattern = /(https?:\/\/[^\s<]+|mailto:[^\s<]+)/gi;
const lines = content.split(/\r?\n/);
const nodes: React.ReactNode[] = [];
lines.forEach((line, lineIndex) => {
const parts = line.split(urlPattern);
parts.forEach((part, partIndex) => {
if (!part) return;
if (urlPattern.test(part)) {
nodes.push(
{part}
);
} else {
nodes.push({part} );
}
urlPattern.lastIndex = 0;
});
if (lineIndex < lines.length - 1) {
nodes.push( );
}
});
return nodes;
}
export default function OutlookPage() {
const { locale } = useAppI18n();
const t = (zh: string, en: string) => pickAppText(locale, zh, en);
const [status, setStatus] = useState(null);
const [form, setForm] = useState(EMPTY_FORM);
const [formDirty, setFormDirty] = useState(false);
const [testResult, setTestResult] = useState(null);
const [overview, setOverview] = useState> | null>(null);
const [statusLoading, setStatusLoading] = useState(true);
const [overviewLoading, setOverviewLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [testing, setTesting] = useState(false);
const [saving, setSaving] = useState(false);
const [disconnecting, setDisconnecting] = useState(false);
const [error, setError] = useState(null);
const [selectedMessageRef, setSelectedMessageRef] = useState<{
id: string;
changekey?: string | null;
} | null>(null);
const [selectedMessage, setSelectedMessage] = useState(null);
const [messageLoading, setMessageLoading] = useState(false);
const [selectedEvent, setSelectedEvent] = useState(null);
const [activeView, setActiveView] = useState('settings');
const [inboxPage, setInboxPage] = useState(null);
const [sentPage, setSentPage] = useState(null);
const [calendarPage, setCalendarPage] = useState(null);
const [calendarAnchorKey, setCalendarAnchorKey] = useState(toLocalDateKey(new Date()));
const [mailboxLoading, setMailboxLoading] = useState>({
inbox: false,
sent: false,
});
const [calendarLoading, setCalendarLoading] = useState(false);
const applyStatus = useCallback((nextStatus: OutlookStatus, forceFormSync = false) => {
setStatus(nextStatus);
if (forceFormSync || !formDirty) {
setForm(toFormState(nextStatus));
setFormDirty(false);
}
}, [formDirty]);
const loadOverview = useCallback(async (preserveExisting = false) => {
setOverviewLoading(true);
try {
const nextOverview = await getOutlookOverview();
setOverview(nextOverview);
setError(null);
} catch (err: any) {
if (!preserveExisting) {
setOverview(null);
}
setError(err.message || t('加载 Outlook 概览失败', 'Failed to load the Outlook overview'));
} finally {
setOverviewLoading(false);
}
}, [t]);
const loadMailboxPage = useCallback(async (view: OutlookMailboxView, skip = 0) => {
setMailboxLoading((current) => ({ ...current, [view]: true }));
try {
const nextPage = await getOutlookMessages(view === 'inbox' ? 'inbox' : 'sentitems', {
top: MAILBOX_PAGE_SIZE,
skip,
});
if (view === 'inbox') {
setInboxPage(nextPage);
} else {
setSentPage(nextPage);
}
setError(null);
} catch (err: any) {
setError(
err.message
|| t(
`加载${view === 'inbox' ? '收件箱' : '发件箱'}失败`,
`Failed to load the ${view === 'inbox' ? 'inbox' : 'sent mailbox'}`
)
);
} finally {
setMailboxLoading((current) => ({ ...current, [view]: false }));
}
}, [t]);
const loadCalendarPage = useCallback(async (anchorKey: string) => {
setCalendarLoading(true);
try {
const range = buildCalendarRange(anchorKey);
const nextPage = await getOutlookEvents({
...range,
top: CALENDAR_PAGE_SIZE,
skip: 0,
});
setCalendarPage(nextPage);
setError(null);
} catch (err: any) {
setError(err.message || t('加载日程失败', 'Failed to load calendar events'));
} finally {
setCalendarLoading(false);
}
}, [t]);
const loadStatus = useCallback(async (
background = false,
options?: {
forceFormSync?: boolean;
preserveOverview?: boolean;
}
) => {
if (background) {
setRefreshing(true);
} else {
setStatusLoading(true);
}
setError(null);
try {
const nextStatus = await getOutlookStatus();
applyStatus(nextStatus, options?.forceFormSync);
if (!background) {
setStatusLoading(false);
}
if (nextStatus.configured) {
await loadOverview(options?.preserveOverview ?? background);
} else {
setOverview(null);
setOverviewLoading(false);
}
} catch (err: any) {
setError(err.message || t('加载 Outlook 集成状态失败', 'Failed to load Outlook integration status'));
setOverviewLoading(false);
} finally {
if (background) {
setRefreshing(false);
} else {
setStatusLoading(false);
}
}
}, [applyStatus, loadOverview, t]);
useEffect(() => {
void loadStatus();
}, [loadStatus]);
useEffect(() => {
if (!selectedMessageRef) {
setSelectedMessage(null);
return;
}
let cancelled = false;
setMessageLoading(true);
getOutlookMessageDetail(selectedMessageRef.id, selectedMessageRef.changekey)
.then((message) => {
if (!cancelled) {
setSelectedMessage(message);
}
})
.catch((err: any) => {
if (!cancelled) {
setError(err.message || t('加载邮件详情失败', 'Failed to load message details'));
}
})
.finally(() => {
if (!cancelled) {
setMessageLoading(false);
}
});
return () => {
cancelled = true;
};
}, [selectedMessageRef, t]);
const canTest = useMemo(
() => Boolean(
form.email.trim()
&& form.password
&& (form.autodiscover || (form.service_endpoint || '').trim() || (form.server || '').trim())
),
[form.autodiscover, form.email, form.password, form.server, form.service_endpoint]
);
const isConfigured = Boolean(status?.configured);
const isConnected = Boolean(status?.connected);
const inboxCount = overview?.recentInbox.length ?? 0;
const sentCount = overview?.recentSent.length ?? 0;
const eventCount = overview?.todayEvents.length ?? 0;
const overviewWarnings = overview?.warnings || [];
const testWarnings = testResult?.warnings || [];
const statusPending = statusLoading && !status;
const overviewPending = overviewLoading && !overview;
const availableViews = useMemo(() => {
if (!isConfigured) {
return [
{
id: 'settings' as const,
label: t('设置', 'Settings'),
hint: t('配置 Outlook 连接', 'Configure the Outlook connection'),
icon: Settings2,
count: null,
},
];
}
return [
{
id: 'inbox' as const,
label: t('收件箱', 'Inbox'),
hint: t('最近接收邮件', 'Recently received mail'),
icon: Inbox,
count: null,
},
{
id: 'sent' as const,
label: t('发件箱', 'Sent'),
hint: t('最近发送记录', 'Recently sent messages'),
icon: Send,
count: null,
},
{
id: 'calendar' as const,
label: t('日程', 'Calendar'),
hint: t('未来 7 天', 'Next 7 days'),
icon: CalendarDays,
count: overviewPending ? null : eventCount,
},
{
id: 'settings' as const,
label: t('设置', 'Settings'),
hint: t('连接与状态', 'Connection and status'),
icon: Settings2,
count: null,
},
];
}, [eventCount, inboxCount, isConfigured, overviewPending, sentCount, t]);
useEffect(() => {
if (!availableViews.some((view) => view.id === activeView)) {
setActiveView(availableViews[0].id);
}
}, [activeView, availableViews]);
useEffect(() => {
if (!isConfigured) {
return;
}
if (activeView === 'inbox' && !inboxPage && !mailboxLoading.inbox) {
void loadMailboxPage('inbox', 0);
}
if (activeView === 'sent' && !sentPage && !mailboxLoading.sent) {
void loadMailboxPage('sent', 0);
}
if (activeView === 'calendar' && !calendarPage && !calendarLoading) {
void loadCalendarPage(calendarAnchorKey);
}
}, [
activeView,
calendarAnchorKey,
calendarLoading,
calendarPage,
inboxPage,
isConfigured,
loadCalendarPage,
loadMailboxPage,
mailboxLoading.inbox,
mailboxLoading.sent,
sentPage,
]);
const updateField = (key: K, value: OutlookFormState[K]) => {
setFormDirty(true);
setForm((current) => ({ ...current, [key]: value }));
};
const handleTest = async () => {
setTesting(true);
setError(null);
try {
const result = await testOutlookConnection(form);
setTestResult(result);
} catch (err: any) {
setError(err.message || t('测试连接失败', 'Failed to test the connection'));
setTestResult(null);
} finally {
setTesting(false);
}
};
const handleConnect = async () => {
setSaving(true);
setError(null);
try {
await connectOutlook(form);
setForm((current) => ({ ...current, password: '' }));
setFormDirty(false);
setTestResult(null);
setInboxPage(null);
setSentPage(null);
setCalendarPage(null);
setCalendarAnchorKey(toLocalDateKey(new Date()));
await loadStatus(true, { forceFormSync: true });
setActiveView('inbox');
} catch (err: any) {
setError(err.message || t('保存 Outlook 配置失败', 'Failed to save Outlook settings'));
} finally {
setSaving(false);
}
};
const handleDisconnect = async () => {
setDisconnecting(true);
setError(null);
try {
await disconnectOutlook();
setOverview(null);
setTestResult(null);
setSelectedMessageRef(null);
setSelectedEvent(null);
setInboxPage(null);
setSentPage(null);
setCalendarPage(null);
setCalendarAnchorKey(toLocalDateKey(new Date()));
setActiveView('settings');
setFormDirty(false);
await loadStatus(true, { forceFormSync: true });
} catch (err: any) {
setError(err.message || t('断开 Outlook 连接失败', 'Failed to disconnect Outlook'));
} finally {
setDisconnecting(false);
}
};
const refreshOverview = async () => {
await loadStatus(true, { preserveOverview: true });
if (activeView === 'inbox') {
await loadMailboxPage('inbox', inboxPage?.page.skip ?? 0);
} else if (activeView === 'sent') {
await loadMailboxPage('sent', sentPage?.page.skip ?? 0);
} else if (activeView === 'calendar') {
await loadCalendarPage(calendarAnchorKey);
}
};
return (
Outlook
{statusPending ? (
<>
>
) : (
<>
{isConnected ? t('已连通', 'Connected') : isConfigured ? t('已配置', 'Configured') : t('未配置', 'Not configured')}
{status?.mcp_registered ? t('MCP 已注册', 'MCP registered') : t('MCP 未注册', 'MCP not registered')}
{status?.provider || 'ews'}
{t('邮箱', 'Mailbox')} {overview?.mailbox || status?.saved?.email || '-'}
{t('时区', 'Timezone')} {status?.saved?.default_timezone || overview?.timezone || form.default_timezone}
{t('最近刷新', 'Last refreshed')} {formatDateTime((overview?.meta?.last_overview_refresh_at || status?.meta?.last_overview_refresh_at) as string | undefined, locale)}
>
)}
{isConfigured ? (
<>
>
) : null}
void refreshOverview()}>
{t('刷新', 'Refresh')}
{error && (
)}
{!error && overviewWarnings.length > 0 && (
{overviewWarnings.map((warning, index) => (
))}
)}
setActiveView(value as OutlookView)} className="space-y-6">
{availableViews.map((view) => {
const Icon = view.icon;
return (
{view.label}
{typeof view.count === 'number' ? (
{t(`${view.count} 条`, `${view.count} items`)}
) : null}
);
})}
}
items={inboxPage?.value || []}
page={inboxPage?.page || null}
loading={mailboxLoading.inbox || (activeView === 'inbox' && !inboxPage)}
emptyLabel={t('还没有读取到收件箱邮件', 'No inbox messages have been loaded yet')}
locale={locale}
onOpen={(item) => setSelectedMessageRef(item.id ? { id: item.id, changekey: item.changekey } : null)}
onRefresh={() => void loadMailboxPage('inbox', inboxPage?.page.skip ?? 0)}
refreshing={mailboxLoading.inbox}
onPreviousPage={() => void loadMailboxPage('inbox', Math.max(0, (inboxPage?.page.skip ?? 0) - MAILBOX_PAGE_SIZE))}
onNextPage={() => {
const nextSkip = inboxPage?.page.next_skip;
if (typeof nextSkip === 'number') {
void loadMailboxPage('inbox', nextSkip);
}
}}
/>
}
items={sentPage?.value || []}
page={sentPage?.page || null}
loading={mailboxLoading.sent || (activeView === 'sent' && !sentPage)}
emptyLabel={t('还没有读取到已发送邮件', 'No sent messages have been loaded yet')}
locale={locale}
onOpen={(item) => setSelectedMessageRef(item.id ? { id: item.id, changekey: item.changekey } : null)}
onRefresh={() => void loadMailboxPage('sent', sentPage?.page.skip ?? 0)}
refreshing={mailboxLoading.sent}
onPreviousPage={() => void loadMailboxPage('sent', Math.max(0, (sentPage?.page.skip ?? 0) - MAILBOX_PAGE_SIZE))}
onNextPage={() => {
const nextSkip = sentPage?.page.next_skip;
if (typeof nextSkip === 'number') {
void loadMailboxPage('sent', nextSkip);
}
}}
/>
setSelectedEvent(item)}
onRefresh={() => void loadCalendarPage(calendarAnchorKey)}
refreshing={calendarLoading}
onPreviousWeek={() => {
const next = new Date(`${calendarAnchorKey}T00:00:00`);
next.setDate(next.getDate() - 7);
setCalendarAnchorKey(toLocalDateKey(next));
setCalendarPage(null);
}}
onNextWeek={() => {
const next = new Date(`${calendarAnchorKey}T00:00:00`);
next.setDate(next.getDate() + 7);
setCalendarAnchorKey(toLocalDateKey(next));
setCalendarPage(null);
}}
onCurrentWeek={() => {
const nextKey = toLocalDateKey(new Date());
setCalendarAnchorKey(nextKey);
setCalendarPage(null);
}}
/>
{t('连接设置', 'Connection settings')}
updateField('email', event.target.value)}
placeholder="you@boardware.com"
/>
updateField('username', event.target.value)}
placeholder={t('留空时默认取邮箱前缀', 'Leave blank to default to the email prefix')}
/>
updateField('password', event.target.value)}
placeholder={t('请输入邮箱密码', 'Enter the mailbox password')}
/>
updateField('domain', event.target.value)}
placeholder="boardware.com.mo"
/>
updateField('service_endpoint', event.target.value)}
placeholder="https://mail.boardware.com.mo/EWS/Exchange.asmx"
disabled={form.autodiscover}
/>
updateField('server', event.target.value)}
placeholder="mail.boardware.com.mo"
disabled={form.autodiscover}
/>
updateField('default_timezone', event.target.value)}
placeholder="Asia/Shanghai"
/>
Autodiscover
{t('开启后优先使用 Exchange 自动发现,不再强依赖手填 EWS URL。', 'When enabled, Exchange autodiscover is preferred so the EWS URL does not need to be entered manually.')}
updateField('autodiscover', checked)}
/>
{testing ? : }
{t('测试连接', 'Test connection')}
{saving ? : }
{t('保存并启用', 'Save and enable')}
{disconnecting ? : }
{t('断开连接', 'Disconnect')}
{testResult && (
{t('测试成功', 'Test succeeded')}
{testResult.mailbox}
{t('用户名', 'Username')}: {testResult.resolved_username}
{testWarnings.length > 0 && (
{testWarnings.map((warning, index) => (
))}
)}
)}
{t('连接状态', 'Connection status')}
{statusPending ? (
<>
>
) : (
<>
{isConnected ? t('已连通', 'Connected') : isConfigured ? t('已配置', 'Configured') : t('未配置', 'Not configured')}
{status?.mcp_registered ? t('MCP 已注册', 'MCP registered') : t('MCP 未注册', 'MCP not registered')}
{status?.provider || 'ews'}
>
)}
{status?.error && (
{status.error}
)}
{t('当前存储位置', 'Current storage mode')}
{status?.storage_mode === 'authz'
? t(
'当前为 AuthZ 模式。Outlook 凭据保存在 AuthZ Service,由外置 Outlook MCP 按 backend 身份读取。',
'AuthZ mode is active. Outlook credentials are stored in the AuthZ service and read by the external Outlook MCP using the backend identity.'
)
: <>{t('当前为 workspace 模式。Outlook 状态文件会写入当前 workspace 的', 'Workspace mode is active. Outlook state files are written to')} state/bw_outlook_mcp.>}
!open && setSelectedMessageRef(null)}>
{selectedMessage?.subject || t('邮件详情', 'Message details')}
{selectedMessage?.receivedDateTime ? formatDateTime(selectedMessage.receivedDateTime, locale) : t('正在加载', 'Loading')}
{messageLoading ? (
) : selectedMessage ? (
{selectedMessage.isRead ? t('已读', 'Read') : t('未读', 'Unread')}
{t('正文', 'Body')}
{selectedMessage.body?.contentType?.toLowerCase() === 'html' ? (
) : (
{renderPlainText(selectedMessage.body?.content || selectedMessage.bodyPreview || '')}
)}
) : (
{t('未加载到邮件详情。', 'Message details were not loaded.')}
)}
!open && setSelectedEvent(null)}>
{selectedEvent?.subject || t('日程详情', 'Event details')}
{selectedEvent
? `${formatDateTime(selectedEvent.start?.dateTime, locale)} - ${formatDateTime(selectedEvent.end?.dateTime, locale)}`
: t('日程详情', 'Event details')}
{selectedEvent && (
{t('说明', 'Notes')}
{selectedEvent.bodyPreview || t('没有更多说明。', 'No additional notes.')}
)}
);
}
function Field({
label,
required = false,
children,
}: {
label: string;
required?: boolean;
children: React.ReactNode;
}) {
return (
{label}
{required ? * : null}
{children}
);
}
function InfoRow({ label, value, loading = false }: { label: string; value: string; loading?: boolean }) {
return (
{label}
{loading ? (
) : (
{value}
)}
);
}
function MiniStat({ label, value }: { label: string; value: string }) {
return (
);
}
function TopStat({ label, value, loading = false }: { label: string; value: string; loading?: boolean }) {
return (
{label}
{loading ? (
) : (
{value}
)}
);
}
function MessageCard({
title,
icon,
items,
page,
locale,
loading = false,
emptyLabel,
onOpen,
onRefresh,
refreshing,
onPreviousPage,
onNextPage,
}: {
title: string;
icon: React.ReactNode;
items: OutlookMessageSummary[];
page: OutlookPageInfo | null;
locale: AppLocale;
loading?: boolean;
emptyLabel: string;
onOpen: (item: OutlookMessageSummary) => void;
onRefresh: () => void;
refreshing: boolean;
onPreviousPage: () => void;
onNextPage: () => void;
}) {
const t = (zh: string, en: string) => pickAppText(locale, zh, en);
const currentPage = page ? Math.floor(page.skip / Math.max(page.top, 1)) + 1 : 1;
const pageLabel = page ? t(`第 ${currentPage} 页 · 本页 ${page.returned} 封`, `Page ${currentPage} · ${page.returned} messages`) : t('正在读取邮件…', 'Loading messages...');
return (
{icon}
{title}
{loading ? t('正在读取邮件…', 'Loading messages...') : pageLabel}
{t('上一页', 'Previous')}
{t('下一页', 'Next')}
{loading ? (
{Array.from({ length: 4 }).map((_, index) => (
))}
) : items.length === 0 ? (
{emptyLabel}
) : (
{items.map((item) => (
item.id && onOpen(item)}
className="w-full rounded-2xl border bg-card p-4 text-left transition-colors hover:bg-muted/40"
>
{item.subject || t('(无主题)', '(No subject)')}
{mailboxLabel(item.from)}
{item.bodyPreview || t('没有预览内容。', 'No preview available.')}
{item.isRead ? t('已读', 'Read') : t('未读', 'Unread')}
{formatDateTime(item.receivedDateTime, locale)}
))}
)}
);
}
function EventCard({
items,
startDate,
locale,
loading = false,
onOpen,
onRefresh,
refreshing,
onPreviousWeek,
onNextWeek,
onCurrentWeek,
}: {
items: OutlookEventSummary[];
startDate?: string | null;
locale: AppLocale;
loading?: boolean;
onOpen: (item: OutlookEventSummary) => void;
onRefresh: () => void;
refreshing: boolean;
onPreviousWeek: () => void;
onNextWeek: () => void;
onCurrentWeek: () => void;
}) {
const t = (zh: string, en: string) => pickAppText(locale, zh, en);
const initialAnchor = startDate ? new Date(startDate) : new Date();
const anchor = Number.isNaN(initialAnchor.getTime()) ? new Date() : initialAnchor;
const weekDays = Array.from({ length: 7 }, (_, index) => {
const next = new Date(anchor);
next.setDate(anchor.getDate() + index);
return next;
});
const eventsByDay = weekDays.map((day) => {
const key = toLocalDateKey(day);
return {
key,
label: formatDayLabel(day, locale),
items: items
.filter((item) => formatDateKey(item.start?.dateTime) === key)
.sort((left, right) => {
const leftTime = new Date(left.start?.dateTime || '').getTime();
const rightTime = new Date(right.start?.dateTime || '').getTime();
return leftTime - rightTime;
}),
};
});
return (
{t('日程安排', 'Schedule')}
{formatDayLabel(weekDays[0], locale)} - {formatDayLabel(weekDays[weekDays.length - 1], locale)}
{t('上一周', 'Previous week')}
{t('本周', 'This week')}
{t('下一周', 'Next week')}
{loading ? (
{Array.from({ length: 6 }).map((_, index) => (
))}
) : (
{eventsByDay.map((day) => (
{day.label}
{t(`${day.items.length} 条安排`, `${day.items.length} events`)}
{day.items.length === 0 ? (
{t('暂无安排', 'No events')}
) : (
{day.items.map((item) => (
onOpen(item)}
className="w-full rounded-xl border bg-background p-3 text-left transition-colors hover:bg-muted/40"
>
{item.subject || t('(无主题)', '(No subject)')}
{formatTime(item.start?.dateTime, locale)} - {formatTime(item.end?.dateTime, locale)}
{item.location?.displayName || t('未设置地点', 'No location set')}
))}
)}
))}
)}
);
}