mirror of
https://github.com/beilunyang/moemail.git
synced 2026-09-07 08:16:37 +08:00
fix(shared-email): Improve date handling and error resilience in shared email page
This commit is contained in:
@@ -11,8 +11,8 @@ import { EMAIL_CONFIG } from "@/config"
|
|||||||
interface Email {
|
interface Email {
|
||||||
id: string
|
id: string
|
||||||
address: string
|
address: string
|
||||||
createdAt: string
|
createdAt: Date
|
||||||
expiresAt: string
|
expiresAt: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Message {
|
interface Message {
|
||||||
@@ -20,8 +20,8 @@ interface Message {
|
|||||||
from_address?: string
|
from_address?: string
|
||||||
to_address?: string
|
to_address?: string
|
||||||
subject: string
|
subject: string
|
||||||
received_at?: string
|
received_at?: Date
|
||||||
sent_at?: string
|
sent_at?: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MessageDetail extends Message {
|
interface MessageDetail extends Message {
|
||||||
@@ -37,16 +37,16 @@ interface SharedEmailPageClientProps {
|
|||||||
token: string
|
token: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SharedEmailPageClient({
|
export function SharedEmailPageClient({
|
||||||
email,
|
email,
|
||||||
initialMessages,
|
initialMessages,
|
||||||
initialNextCursor,
|
initialNextCursor,
|
||||||
initialTotal,
|
initialTotal,
|
||||||
token
|
token
|
||||||
}: SharedEmailPageClientProps) {
|
}: SharedEmailPageClientProps) {
|
||||||
const t = useTranslations("emails")
|
const t = useTranslations("emails")
|
||||||
const tShared = useTranslations("emails.shared")
|
const tShared = useTranslations("emails.shared")
|
||||||
|
|
||||||
const [messages, setMessages] = useState<Message[]>(initialMessages)
|
const [messages, setMessages] = useState<Message[]>(initialMessages)
|
||||||
const [selectedMessage, setSelectedMessage] = useState<MessageDetail | null>(null)
|
const [selectedMessage, setSelectedMessage] = useState<MessageDetail | null>(null)
|
||||||
const [messageLoading, setMessageLoading] = useState(false)
|
const [messageLoading, setMessageLoading] = useState(false)
|
||||||
@@ -75,12 +75,12 @@ export function SharedEmailPageClient({
|
|||||||
|
|
||||||
const messagesResponse = await fetch(url)
|
const messagesResponse = await fetch(url)
|
||||||
if (messagesResponse.ok) {
|
if (messagesResponse.ok) {
|
||||||
const messagesData = await messagesResponse.json() as {
|
const messagesData = await messagesResponse.json() as {
|
||||||
messages: Message[]
|
messages: Message[]
|
||||||
nextCursor: string | null
|
nextCursor: string | null
|
||||||
total: number
|
total: number
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cursor) {
|
if (!cursor) {
|
||||||
// 刷新时:合并新消息和旧消息,避免重复
|
// 刷新时:合并新消息和旧消息,避免重复
|
||||||
const newMessages = messagesData.messages
|
const newMessages = messagesData.messages
|
||||||
@@ -144,7 +144,7 @@ export function SharedEmailPageClient({
|
|||||||
return () => {
|
return () => {
|
||||||
stopPolling()
|
stopPolling()
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [token])
|
}, [token])
|
||||||
|
|
||||||
const handleLoadMore = () => {
|
const handleLoadMore = () => {
|
||||||
@@ -156,13 +156,13 @@ export function SharedEmailPageClient({
|
|||||||
const fetchMessageDetail = async (messageId: string) => {
|
const fetchMessageDetail = async (messageId: string) => {
|
||||||
try {
|
try {
|
||||||
setMessageLoading(true)
|
setMessageLoading(true)
|
||||||
|
|
||||||
const response = await fetch(`/api/shared/${token}/messages/${messageId}`)
|
const response = await fetch(`/api/shared/${token}/messages/${messageId}`)
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Failed to load message")
|
throw new Error("Failed to load message")
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json() as { message: MessageDetail }
|
const data = await response.json() as { message: MessageDetail }
|
||||||
setSelectedMessage(data.message)
|
setSelectedMessage(data.message)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -175,23 +175,47 @@ export function SharedEmailPageClient({
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||||
<div className="container mx-auto p-4 max-w-7xl">
|
<div className="container mx-auto p-4 max-w-7xl">
|
||||||
<BrandHeader
|
<BrandHeader
|
||||||
title={email.address}
|
title={email.address}
|
||||||
subtitle={new Date(email.expiresAt).getFullYear() === 9999
|
subtitle={(() => {
|
||||||
? tShared("permanent")
|
try {
|
||||||
: `${tShared("expiresAt")}: ${new Date(email.expiresAt).toLocaleDateString()} ${new Date(email.expiresAt).toLocaleTimeString()}`}
|
const expiresDate = new Date(email.expiresAt)
|
||||||
|
if (isNaN(expiresDate.getTime())) return tShared("sharedMailbox")
|
||||||
|
return expiresDate.getFullYear() === 9999
|
||||||
|
? tShared("permanent")
|
||||||
|
: `${tShared("expiresAt")}: ${expiresDate.toLocaleDateString()} ${expiresDate.toLocaleTimeString()}`
|
||||||
|
} catch {
|
||||||
|
return tShared("sharedMailbox")
|
||||||
|
}
|
||||||
|
})()}
|
||||||
showCta={true}
|
showCta={true}
|
||||||
ctaText={tShared("createOwnEmail")}
|
ctaText={tShared("createOwnEmail")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 桌面端双栏布局 */}
|
{/* 桌面端双栏布局 */}
|
||||||
<div className="hidden lg:grid grid-cols-2 gap-4 h-[calc(100vh-280px)] mt-6">
|
<div className="hidden lg:grid grid-cols-2 gap-4 h-[calc(100vh-280px)] mt-6">
|
||||||
<div className="border-2 border-primary/20 bg-background rounded-lg overflow-hidden">
|
<div className="border-2 border-primary/20 bg-background rounded-lg overflow-hidden">
|
||||||
<SharedMessageList
|
<SharedMessageList
|
||||||
messages={messages.map(msg => ({
|
messages={messages.map(msg => ({
|
||||||
...msg,
|
...msg,
|
||||||
received_at: msg.received_at ? new Date(msg.received_at as string).getTime() : undefined,
|
received_at: (() => {
|
||||||
sent_at: msg.sent_at ? new Date(msg.sent_at as string).getTime() : undefined
|
if (!msg.received_at) return undefined
|
||||||
|
try {
|
||||||
|
const date = new Date(msg.received_at)
|
||||||
|
return isNaN(date.getTime()) ? undefined : date.getTime()
|
||||||
|
} catch {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
sent_at: (() => {
|
||||||
|
if (!msg.sent_at) return undefined
|
||||||
|
try {
|
||||||
|
const date = new Date(msg.sent_at)
|
||||||
|
return isNaN(date.getTime()) ? undefined : date.getTime()
|
||||||
|
} catch {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
})()
|
||||||
}))}
|
}))}
|
||||||
selectedMessageId={selectedMessage?.id}
|
selectedMessageId={selectedMessage?.id}
|
||||||
onMessageSelect={fetchMessageDetail}
|
onMessageSelect={fetchMessageDetail}
|
||||||
@@ -216,8 +240,24 @@ export function SharedEmailPageClient({
|
|||||||
<SharedMessageDetail
|
<SharedMessageDetail
|
||||||
message={selectedMessage ? {
|
message={selectedMessage ? {
|
||||||
...selectedMessage,
|
...selectedMessage,
|
||||||
received_at: selectedMessage.received_at ? new Date(selectedMessage.received_at as string).getTime() : undefined,
|
received_at: (() => {
|
||||||
sent_at: selectedMessage.sent_at ? new Date(selectedMessage.sent_at as string).getTime() : undefined
|
if (!selectedMessage.received_at) return undefined
|
||||||
|
try {
|
||||||
|
const date = new Date(selectedMessage.received_at)
|
||||||
|
return isNaN(date.getTime()) ? undefined : date.getTime()
|
||||||
|
} catch {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
sent_at: (() => {
|
||||||
|
if (!selectedMessage.sent_at) return undefined
|
||||||
|
try {
|
||||||
|
const date = new Date(selectedMessage.sent_at)
|
||||||
|
return isNaN(date.getTime()) ? undefined : date.getTime()
|
||||||
|
} catch {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
})()
|
||||||
} : null}
|
} : null}
|
||||||
loading={messageLoading}
|
loading={messageLoading}
|
||||||
t={{
|
t={{
|
||||||
@@ -243,8 +283,24 @@ export function SharedEmailPageClient({
|
|||||||
<SharedMessageList
|
<SharedMessageList
|
||||||
messages={messages.map(msg => ({
|
messages={messages.map(msg => ({
|
||||||
...msg,
|
...msg,
|
||||||
received_at: msg.received_at ? new Date(msg.received_at as string).getTime() : undefined,
|
received_at: (() => {
|
||||||
sent_at: msg.sent_at ? new Date(msg.sent_at as string).getTime() : undefined
|
if (!msg.received_at) return undefined
|
||||||
|
try {
|
||||||
|
const date = new Date(msg.received_at)
|
||||||
|
return isNaN(date.getTime()) ? undefined : date.getTime()
|
||||||
|
} catch {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
sent_at: (() => {
|
||||||
|
if (!msg.sent_at) return undefined
|
||||||
|
try {
|
||||||
|
const date = new Date(msg.sent_at)
|
||||||
|
return isNaN(date.getTime()) ? undefined : date.getTime()
|
||||||
|
} catch {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
})()
|
||||||
}))}
|
}))}
|
||||||
selectedMessageId={null}
|
selectedMessageId={null}
|
||||||
onMessageSelect={fetchMessageDetail}
|
onMessageSelect={fetchMessageDetail}
|
||||||
@@ -279,8 +335,24 @@ export function SharedEmailPageClient({
|
|||||||
<SharedMessageDetail
|
<SharedMessageDetail
|
||||||
message={{
|
message={{
|
||||||
...selectedMessage,
|
...selectedMessage,
|
||||||
received_at: selectedMessage.received_at ? new Date(selectedMessage.received_at as string).getTime() : undefined,
|
received_at: (() => {
|
||||||
sent_at: selectedMessage.sent_at ? new Date(selectedMessage.sent_at as string).getTime() : undefined
|
if (!selectedMessage.received_at) return undefined
|
||||||
|
try {
|
||||||
|
const date = new Date(selectedMessage.received_at)
|
||||||
|
return isNaN(date.getTime()) ? undefined : date.getTime()
|
||||||
|
} catch {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
sent_at: (() => {
|
||||||
|
if (!selectedMessage.sent_at) return undefined
|
||||||
|
try {
|
||||||
|
const date = new Date(selectedMessage.sent_at)
|
||||||
|
return isNaN(date.getTime()) ? undefined : date.getTime()
|
||||||
|
} catch {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
})()
|
||||||
}}
|
}}
|
||||||
loading={messageLoading}
|
loading={messageLoading}
|
||||||
t={{
|
t={{
|
||||||
@@ -301,7 +373,7 @@ export function SharedEmailPageClient({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FloatingLanguageSwitcher />
|
<FloatingLanguageSwitcher />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ interface PageProps {
|
|||||||
export default async function SharedEmailPage({ params }: PageProps) {
|
export default async function SharedEmailPage({ params }: PageProps) {
|
||||||
const { token } = await params
|
const { token } = await params
|
||||||
const tShared = await getTranslations("emails.shared")
|
const tShared = await getTranslations("emails.shared")
|
||||||
|
|
||||||
// 服务端获取数据
|
// 服务端获取数据
|
||||||
const email = await getSharedEmail(token)
|
const email = await getSharedEmail(token)
|
||||||
|
|
||||||
if (!email) {
|
if (!email) {
|
||||||
return (
|
return (
|
||||||
<SharedErrorPage
|
<SharedErrorPage
|
||||||
@@ -33,17 +33,9 @@ export default async function SharedEmailPage({ params }: PageProps) {
|
|||||||
const messagesResult = await getSharedEmailMessages(token)
|
const messagesResult = await getSharedEmailMessages(token)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SharedEmailPageClient
|
<SharedEmailPageClient
|
||||||
email={{
|
email={email}
|
||||||
...email,
|
initialMessages={messagesResult.messages}
|
||||||
createdAt: email.createdAt.toISOString(),
|
|
||||||
expiresAt: email.expiresAt.toISOString()
|
|
||||||
}}
|
|
||||||
initialMessages={messagesResult.messages.map(msg => ({
|
|
||||||
...msg,
|
|
||||||
received_at: msg.received_at?.toISOString(),
|
|
||||||
sent_at: msg.sent_at?.toISOString()
|
|
||||||
}))}
|
|
||||||
initialNextCursor={messagesResult.nextCursor}
|
initialNextCursor={messagesResult.nextCursor}
|
||||||
initialTotal={messagesResult.total}
|
initialTotal={messagesResult.total}
|
||||||
token={token}
|
token={token}
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ export function ShareDialog({ emailId }: ShareDialogProps) {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-1 text-xs p-1 rounded truncate font-mono transition-colors",
|
"flex-1 text-xs p-1 rounded font-mono transition-colors break-all",
|
||||||
isExpired
|
isExpired
|
||||||
? "bg-destructive/10 text-destructive/70 cursor-not-allowed pointer-events-none"
|
? "bg-destructive/10 text-destructive/70 cursor-not-allowed pointer-events-none"
|
||||||
: "bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:text-primary dark:hover:text-primary cursor-pointer"
|
: "bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:text-primary dark:hover:text-primary cursor-pointer"
|
||||||
@@ -281,7 +281,7 @@ export function ShareDialog({ emailId }: ShareDialogProps) {
|
|||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4 text-xs">
|
<div className="flex gap-y-4 gap-x-2 sm:gap-x-4 text-xs">
|
||||||
<span className={cn(
|
<span className={cn(
|
||||||
isExpired ? "text-destructive/70" : "text-gray-500"
|
isExpired ? "text-destructive/70" : "text-gray-500"
|
||||||
)}>
|
)}>
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ export function ShareMessageDialog({ emailId, messageId, messageSubject, trigger
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-1 text-xs p-1 rounded truncate font-mono transition-colors",
|
"flex-1 text-xs p-1 rounded font-mono transition-colors break-all",
|
||||||
isExpired
|
isExpired
|
||||||
? "bg-destructive/10 text-destructive/70 cursor-not-allowed pointer-events-none"
|
? "bg-destructive/10 text-destructive/70 cursor-not-allowed pointer-events-none"
|
||||||
: "bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:text-primary dark:hover:text-primary cursor-pointer"
|
: "bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:text-primary dark:hover:text-primary cursor-pointer"
|
||||||
@@ -290,7 +290,7 @@ export function ShareMessageDialog({ emailId, messageId, messageSubject, trigger
|
|||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4 text-xs">
|
<div className="flex gap-y-4 gap-x-2 sm:gap-x-4 text-xs">
|
||||||
<span className={cn(
|
<span className={cn(
|
||||||
isExpired ? "text-destructive/70" : "text-gray-500"
|
isExpired ? "text-destructive/70" : "text-gray-500"
|
||||||
)}>
|
)}>
|
||||||
|
|||||||
Reference in New Issue
Block a user