This commit is contained in:
eric
2026-03-15 04:25:45 -05:00
parent df12c7af4a
commit bbaa38b3b3
32 changed files with 4312 additions and 152 deletions

View File

@@ -0,0 +1,166 @@
"use client";
import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
import { CommunityPageHero } from "../components/CommunityPageHero";
import { COMMUNITY_CHANGED_EVENT } from "../components/communityEvents";
import styles from "../components/memos.module.css";
type AttachmentItem = {
id: string;
url: string;
original_name: string;
created: string;
size: number;
};
function getStorage(key: string): string | null {
if (typeof window === "undefined") {
return null;
}
return localStorage.getItem(key);
}
function formatFileSize(size: number) {
if (size < 1024) {
return `${size} B`;
}
if (size < 1024 * 1024) {
return `${(size / 1024).toFixed(1)} KB`;
}
return `${(size / 1024 / 1024).toFixed(1)} MB`;
}
export default function AttachmentsPage() {
const [userId, setUserId] = useState<string | null>(null);
const [isVip, setIsVip] = useState(false);
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
const [items, setItems] = useState<AttachmentItem[]>([]);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [loading, setLoading] = useState(false);
useEffect(() => {
const uid = getStorage("userId") || getStorage("memosAccount");
const type = getStorage("paidType");
setUserId(uid || null);
setIsVip(type === "vip");
}, []);
const fetchAttachments = useCallback(
async (pageNum: number, append: boolean) => {
if (!userId || !isVip) {
return;
}
setLoading(true);
try {
const response = await fetch(
`/api/community/attachments?user_id=${encodeURIComponent(userId)}&page=${pageNum}&per_page=24`,
{ cache: "no-store" }
);
const data = await response.json().catch(() => null);
if (response.status === 403) {
setShowLoginPrompt(true);
return;
}
if (!response.ok) {
return;
}
const nextItems = Array.isArray(data?.items) ? (data.items as AttachmentItem[]) : [];
setItems((current) => (append ? [...current, ...nextItems] : nextItems));
setHasMore(Boolean(data?.next_page_token));
setPage(pageNum);
} finally {
setLoading(false);
}
},
[isVip, userId]
);
useEffect(() => {
void fetchAttachments(1, false);
}, [fetchAttachments]);
useEffect(() => {
const handler = () => {
void fetchAttachments(1, false);
};
window.addEventListener(COMMUNITY_CHANGED_EVENT, handler);
return () => window.removeEventListener(COMMUNITY_CHANGED_EVENT, handler);
}, [fetchAttachments]);
return (
<div className={styles.pageStack}>
<CommunityPageHero
eyebrow="Attachment Library"
title="图片和资料集中沉淀"
description="所有在动态里上传的图片都会自动汇总到这里,方便回看和二次引用。"
chips={["自动聚合", "适合资料沉淀", "支持快速回看"]}
actions={[{ href: "/community", label: "返回首页", secondary: true }]}
/>
{showLoginPrompt ? (
<section className={styles.vipBanner}>
<p>附件资料库仅对会员开放。</p>
<Link href="/" className={styles.ctaLink}>
立即加入会员
</Link>
</section>
) : null}
{isVip && userId ? (
items.length ? (
<>
<section className={styles.attachmentGrid}>
{items.map((item) => (
<a key={item.id} href={item.url} target="_blank" rel="noreferrer" className={styles.attachmentCard}>
<img src={item.url} alt={item.original_name} className={styles.attachmentImage} />
<div className={styles.attachmentMeta}>
<span className={styles.attachmentName}>{item.original_name}</span>
<span className={styles.attachmentSubMeta}>
{formatFileSize(item.size)} · {new Date(item.created).toLocaleDateString("zh-CN")}
</span>
</div>
</a>
))}
</section>
{hasMore ? (
<button
type="button"
className={styles.loadMorePanelBtn}
onClick={() => void fetchAttachments(page + 1, true)}
disabled={loading}
>
{loading ? "加载中..." : "加载更多附件"}
</button>
) : null}
</>
) : (
<section className={styles.lockedPanel}>
<div className={styles.lockedPanelCopy}>
<span className={styles.lockedPanelEyebrow}>Attachment Library</span>
<h2 className={styles.lockedPanelTitle}>还没有附件</h2>
<p className={styles.lockedPanelDesc}>在首页动态中上传图片后,这里会自动生成资料缩略图墙。</p>
</div>
</section>
)
) : (
<section className={styles.lockedPanel}>
<div className={styles.lockedPanelCopy}>
<span className={styles.lockedPanelEyebrow}>Members Only</span>
<h2 className={styles.lockedPanelTitle}>附件资料库需要会员权限</h2>
<p className={styles.lockedPanelDesc}>加入后即可查看所有上传图片与资料沉淀。</p>
</div>
<div className={styles.lockedPanelActions}>
<Link href="/" className={styles.heroPrimaryAction}>
去开通会员
</Link>
</div>
</section>
)}
</div>
);
}