Files
jiang13-forum/frontend/src/pages/ShowcasePage.tsx
freefire 95c2154087 feat: 可选社区上报与官方精选展柜
默认关闭,仪表盘一键开关;枢纽由运维配置收报,人工精选后展示于 /showcase。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-31 04:22:59 +08:00

88 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from 'react';
import { ExternalLink, Globe2 } from 'lucide-react';
import { Spinner } from '@/components/ui/spinner';
import { api } from '../api/client';
import type { CommunityShowcaseItem } from '../api/types';
import { joinSEOKeywords, usePageSEO } from '../hooks/usePageSEO';
import { getCachedSiteBranding, useSiteBranding } from '../hooks/useSiteBranding';
import { InFlowSiteFooter } from '../components/SiteFooter';
/** 官方精选的公网部署展柜(只读;仅人工精选条目) */
export default function ShowcasePage() {
const { branding } = useSiteBranding();
const [items, setItems] = useState<CommunityShowcaseItem[]>([]);
const [loading, setLoading] = useState(true);
usePageSEO({
title: '开源部署展柜',
description: `${branding.name} 精选的姜十三论坛公网部署`,
keywords: joinSEOKeywords('开源', '部署', '展柜', getCachedSiteBranding().keywords),
canonicalPath: '/showcase',
});
useEffect(() => {
let cancelled = false;
api.communityShowcase()
.then((r) => {
if (!cancelled) setItems(Array.isArray(r.items) ? r.items : []);
})
.catch(() => {
if (!cancelled) setItems([]);
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => { cancelled = true; };
}, []);
return (
<div className="showcase-page">
<header className="showcase-head">
<div className="showcase-head-mark" aria-hidden>
<Globe2 size={22} />
</div>
<div>
<h1 className="showcase-title">开源部署展柜</h1>
<p className="showcase-desc">
以下站点自愿开启社区上报,并由官方演示站精选推荐(非全量目录)
</p>
</div>
</header>
{loading ? (
<div className="flex justify-center py-16"><Spinner size="lg" /></div>
) : items.length === 0 ? (
<p className="showcase-empty">暂无精选实例</p>
) : (
<ul className="showcase-list">
{items.map((item) => (
<li key={item.site_url} className="showcase-item">
<a
href={item.site_url}
target="_blank"
rel="noopener noreferrer"
className="showcase-item-link"
>
<span className="showcase-item-name">{item.site_name || '未命名站点'}</span>
<span className="showcase-item-url">
{item.site_url}
<ExternalLink size={12} aria-hidden />
</span>
{(item.featured_note || item.version) && (
<span className="showcase-item-meta">
{item.featured_note || null}
{item.featured_note && item.version ? ' · ' : null}
{item.version ? `v${item.version}` : null}
</span>
)}
</a>
</li>
))}
</ul>
)}
<InFlowSiteFooter />
</div>
);
}