Files
jiang13-forum/frontend/src/components/ShowcaseAsideWidget.tsx
freefire 52d328ffc3 chore: 工具链升级到最新版本(Vite 8 / React 19 / react-router 8 / TS 7 / Go 1.26 依赖更新)
- 前端:vite ^8.2.2、react/react-dom ^19.2.8、react-router ^8.3.1(移除 react-router-dom)、typescript ^7.0.2、zustand ^5.0.15;适配升级带来的类型变化(react-hook-form Resolver、ProseMirror node.parent、tiptap focus 参数、timer undefined 等)
- 后端:升级 gin/jwt/minio-go/gorm/modernc-sqlite/x-crypto 等直接依赖至最新;gowebp v0.1.1 已是最新版本
- tsconfig 移除 baseUrl,paths 改为相对路径以兼容 TS7
2026-09-09 00:14:12 +08:00

125 lines
4.5 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 { Globe2 } from 'lucide-react';
import { useNavigate } from 'react-router';
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>
);
}