feat: 重设计私信页并优化开源展柜体验
私信改为飞书风布局,支持缩略图表情/图片、按会话草稿与用户搜索发起会话;主页发私信直达消息页。开源展柜修复空列表粘滞并改版目录式 UI。
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
import { ExternalLink, Globe2 } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ArrowLeft, ExternalLink, Globe2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { api } from '../api/client';
|
||||
import type { CommunityShowcaseItem } from '../api/types';
|
||||
@@ -7,12 +10,65 @@ import { getCachedSiteBranding, useSiteBranding } from '../hooks/useSiteBranding
|
||||
import { InFlowSiteFooter } from '../components/SiteFooter';
|
||||
import { useSessionResource } from '../hooks/useSessionResource';
|
||||
|
||||
function siteMark(name: string, url: string): string {
|
||||
const raw = (name || '').trim() || url.replace(/^https?:\/\//i, '');
|
||||
const ch = Array.from(raw)[0];
|
||||
return ch ? ch.toUpperCase() : '?';
|
||||
}
|
||||
|
||||
function hostLabel(url: string): string {
|
||||
try {
|
||||
return new URL(url).host;
|
||||
} catch {
|
||||
return url.replace(/^https?:\/\//i, '');
|
||||
}
|
||||
}
|
||||
|
||||
/** 目标站约定路径 /favicon.ico */
|
||||
function faviconURL(siteURL: string): string | null {
|
||||
try {
|
||||
return new URL('/favicon.ico', siteURL).href;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function ShowcaseFavicon({ name, url }: { name: string; url: string }) {
|
||||
const src = faviconURL(url);
|
||||
const [failed, setFailed] = useState(!src);
|
||||
const mark = siteMark(name, url);
|
||||
|
||||
if (failed || !src) {
|
||||
return (
|
||||
<span className="showcase-item-mark" aria-hidden>
|
||||
{mark}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="showcase-item-favicon">
|
||||
<img
|
||||
src={src}
|
||||
alt=""
|
||||
width={40}
|
||||
height={40}
|
||||
referrerPolicy="no-referrer"
|
||||
decoding="async"
|
||||
onError={() => setFailed(true)}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/** 官方精选的公网部署展柜(只读;仅人工精选条目) */
|
||||
export default function ShowcasePage() {
|
||||
const nav = useNavigate();
|
||||
const { branding } = useSiteBranding();
|
||||
const { data: items = [], loading } = useSessionResource<CommunityShowcaseItem[]>(
|
||||
'showcase',
|
||||
() => api.communityShowcase().then((r) => (Array.isArray(r.items) ? r.items : [])),
|
||||
{ revalidateEmpty: true },
|
||||
);
|
||||
|
||||
usePageSEO({
|
||||
@@ -24,52 +80,65 @@ export default function ShowcasePage() {
|
||||
|
||||
return (
|
||||
<div className="page-wrap">
|
||||
<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">
|
||||
<div className="feed-panel list-page-panel showcase-panel">
|
||||
<header className="list-page-panel__head showcase-head">
|
||||
<Button variant="ghost" size="sm" className="list-page-panel__back" onClick={() => nav('/')}>
|
||||
<ArrowLeft />
|
||||
返回
|
||||
</Button>
|
||||
<div className="showcase-head__title-row">
|
||||
<h1 className="page-title">开源部署展柜</h1>
|
||||
{!loading && items.length > 0 && (
|
||||
<span className="showcase-head__count">精选 {items.length} 站</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="page-desc">
|
||||
以下站点自愿开启社区上报,并由官方演示站精选推荐(非全量目录)
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
</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>
|
||||
)}
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-16"><Spinner size="lg" /></div>
|
||||
) : items.length === 0 ? (
|
||||
<div className="showcase-empty list-page-panel__empty">
|
||||
<Globe2 size={28} strokeWidth={1.5} aria-hidden />
|
||||
<p>暂无精选实例</p>
|
||||
<span>有站点被官方推荐后会出现在这里</span>
|
||||
</div>
|
||||
) : (
|
||||
<ul className="showcase-list">
|
||||
{items.map((item) => {
|
||||
const name = item.site_name || '未命名站点';
|
||||
return (
|
||||
<li key={item.site_url} className="showcase-item">
|
||||
<a
|
||||
href={item.site_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="showcase-item-link"
|
||||
>
|
||||
<ShowcaseFavicon name={name} url={item.site_url} />
|
||||
<span className="showcase-item-body">
|
||||
<span className="showcase-item-name">{name}</span>
|
||||
<span className="showcase-item-url">
|
||||
{hostLabel(item.site_url)}
|
||||
<ExternalLink size={12} aria-hidden />
|
||||
</span>
|
||||
{item.featured_note ? (
|
||||
<span className="showcase-item-note">{item.featured_note}</span>
|
||||
) : null}
|
||||
</span>
|
||||
{item.version ? (
|
||||
<span className="showcase-item-version">v{item.version}</span>
|
||||
) : null}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<InFlowSiteFooter />
|
||||
<InFlowSiteFooter />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user