125 lines
4.5 KiB
TypeScript
125 lines
4.5 KiB
TypeScript
import { useEffect, useState } from 'react';
|
||
import { Globe2 } from 'lucide-react';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import { api } from '../api/client';
|
||
import type { CommunityShowcaseItem } from '../api/types';
|
||
import { getSessionSnapshot, setSessionSnapshot } from '../utils/sessionPageCache';
|
||
import { PAGE_FORCE_REFRESH_EVENT } from '../utils/feedCache';
|
||
import { PAGE_SOFT_REFRESH_COMMIT_EVENT } from '../utils/softRefresh';
|
||
|
||
const SHOWCASE_KEY = 'showcase';
|
||
|
||
/** 右侧栏:开源展柜精简列表 */
|
||
export default function ShowcaseAsideWidget() {
|
||
const nav = useNavigate();
|
||
const cached = getSessionSnapshot<CommunityShowcaseItem[]>(SHOWCASE_KEY);
|
||
const [items, setItems] = useState<CommunityShowcaseItem[]>(() => cached ?? []);
|
||
const [loading, setLoading] = useState(() => cached === undefined);
|
||
|
||
useEffect(() => {
|
||
let cancelled = false;
|
||
const hit = getSessionSnapshot<CommunityShowcaseItem[]>(SHOWCASE_KEY);
|
||
// 非空快照直接用;空数组可能是启动竞态假空,仍再拉
|
||
if (hit !== undefined && hit.length > 0) {
|
||
setItems(hit);
|
||
setLoading(false);
|
||
return;
|
||
}
|
||
if (hit !== undefined) setItems(hit);
|
||
setLoading(hit === undefined);
|
||
api.communityShowcase()
|
||
.then((r) => {
|
||
if (cancelled) return;
|
||
const next = Array.isArray(r.items) ? r.items : [];
|
||
setSessionSnapshot(SHOWCASE_KEY, next);
|
||
setItems(next);
|
||
})
|
||
.catch(() => {
|
||
// 失败不写空快照,避免假空粘死
|
||
if (!cancelled && hit === undefined) setItems([]);
|
||
})
|
||
.finally(() => {
|
||
if (!cancelled) setLoading(false);
|
||
});
|
||
return () => { cancelled = true; };
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
const fetchShowcase = (opts: { showLoading: boolean }) => {
|
||
if (opts.showLoading) setLoading(true);
|
||
api.communityShowcase()
|
||
.then((r) => {
|
||
const next = Array.isArray(r.items) ? r.items : [];
|
||
setSessionSnapshot(SHOWCASE_KEY, next);
|
||
setItems(next);
|
||
})
|
||
.catch(() => {
|
||
// 软刷新 / 强刷失败:保持旧 UI,不写空快照
|
||
})
|
||
.finally(() => setLoading(false));
|
||
};
|
||
const applyHitOrReload = (showLoading: boolean) => {
|
||
const hit = getSessionSnapshot<CommunityShowcaseItem[]>(SHOWCASE_KEY);
|
||
if (hit !== undefined && hit.length > 0) {
|
||
setItems(hit);
|
||
setLoading(false);
|
||
return;
|
||
}
|
||
fetchShowcase({ showLoading });
|
||
};
|
||
const onForce = () => applyHitOrReload(true);
|
||
const onCommit = () => applyHitOrReload(false);
|
||
window.addEventListener(PAGE_FORCE_REFRESH_EVENT, onForce);
|
||
window.addEventListener(PAGE_SOFT_REFRESH_COMMIT_EVENT, onCommit);
|
||
return () => {
|
||
window.removeEventListener(PAGE_FORCE_REFRESH_EVENT, onForce);
|
||
window.removeEventListener(PAGE_SOFT_REFRESH_COMMIT_EVENT, onCommit);
|
||
};
|
||
}, []);
|
||
|
||
return (
|
||
<div className="widget-card widget-card--showcase">
|
||
<div className="widget-card-head widget-card-head--split">
|
||
<span className="widget-card-head-main">
|
||
<Globe2 className="widget-card-icon widget-card-icon--showcase" aria-hidden />
|
||
<button type="button" className="widget-friend-links-title" onClick={() => nav('/showcase')}>
|
||
开源展柜
|
||
</button>
|
||
</span>
|
||
<button type="button" className="widget-friend-links-more" onClick={() => nav('/showcase')}>
|
||
全部
|
||
</button>
|
||
</div>
|
||
<div className="widget-card-body widget-card-body--friend-links">
|
||
{loading ? (
|
||
<div className="widget-empty">加载中…</div>
|
||
) : items.length === 0 ? (
|
||
<div className="widget-empty">
|
||
暂无精选实例
|
||
<button type="button" className="widget-friend-links-more" onClick={() => nav('/showcase')}>
|
||
查看展柜
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<>
|
||
<ul className="widget-friend-links-list">
|
||
{items.slice(0, 8).map((item) => (
|
||
<li key={item.site_url}>
|
||
<a href={item.site_url} target="_blank" rel="noopener noreferrer" title={item.site_name || item.site_url}>
|
||
{item.site_name || '未命名站点'}
|
||
</a>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
{items.length > 8 && (
|
||
<button type="button" className="widget-friend-links-more" onClick={() => nav('/showcase')}>
|
||
查看全部 {items.length} 个
|
||
</button>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|