// ========================================================= // FBIF 资讯订阅 · Left rails // IconRail — far-left app nav with labels, logo + views // FeedRail — subscription / topic navigation (the filter column) // ========================================================= const RailIcon = ({ icon, label, shortLabel, active, badge, onClick }) => { const [hover, setHover] = React.useState(false); const text = shortLabel || label; return ( ); }; const IconRail = ({ view, onNavigate }) => { useLucide(); return (
FBIF
onNavigate('settings')} />
); }; // ---- FeedRail ---------------------------------------------------------- const RailRow = ({ icon, mark, label, count, active, muted, onClick, indent, accent }) => { const [hover, setHover] = React.useState(false); const bg = active ? '#EBF1FE' : hover ? '#F2F3F5' : 'transparent'; return (
setHover(true)} onMouseLeave={() => setHover(false)} style={{ display: 'flex', alignItems: 'center', gap: 8, height: 30, borderRadius: 6, cursor: 'pointer', padding: `0 8px 0 ${indent ? 20 : 8}px`, margin: '1px 8px', background: bg, color: active ? '#0F4DD1' : '#1F2329', fontWeight: active ? 500 : 400, fontSize: 13, }}> {icon && } {mark} {label} {count != null && count > 0 && ( {count} )}
); }; const RailGroup = ({ label, action, children, defaultOpen = true }) => { const [open, setOpen] = React.useState(defaultOpen); return (
setOpen(o => !o)} style={{ display: 'flex', alignItems: 'center', gap: 2, cursor: 'pointer', flex: 1, color: '#8F959E' }}> {label}
{action}
{open && children}
); }; const FeedRail = ({ data, scope, onScope, onManage, onTags, counts, onCollapse }) => { useLucide(); const subTags = data.TAGS.filter(t => t.subscribed); const subSources = data.SOURCES.filter(s => s.subscribed); const byTier = { 'T1': [], 'T1.5': [], 'T2': [] }; subSources.forEach(s => byTier[s.tier].push(s)); // Count per tag is currently source-level article count; personal state belongs to account features later. const tagCount = (tagId) => subSources.filter(s => s.tags.includes(tagId)).reduce((a, s) => a + (s.article_count || 0), 0); return (
{/* header */}
资讯订阅
{/* home */}
onScope({ type: 'timeline' })} count={counts.total} accent="#1456F0" />
{/* tags (用户标签) */} }> {subTags.map(t => ( } label={t.name} count={tagCount(t.id)} active={scope.type === 'tag' && scope.id === t.id} onClick={() => onScope({ type: 'tag', id: t.id, name: t.name })} /> ))} {subTags.length === 0 &&
暂无标签
}
{/* sources grouped by tier */} }> {['T1', 'T1.5', 'T2'].map(tier => byTier[tier].length > 0 && (
{data.TIER_META[tier].label.split('·')[1]}
{byTier[tier].map(s => ( {s.health !== 'healthy' && } } label={s.name.replace(/\s*·.*/, '').replace(/\s*Newsroom/, '')} count={s.article_count} indent active={scope.type === 'source' && scope.id === s.id} onClick={() => onScope({ type: 'source', id: s.id, name: s.name })} /> ))}
))}
{/* footer */}
{(() => { const failed = subSources.filter(s => s.health === 'failed').length; const degraded = subSources.filter(s => s.health === 'degraded').length; const bad = failed + degraded; return (
{bad ? {failed ? `${failed} 个来源失效` : `${degraded} 个来源异常`} · 去处理 : {subSources.length} 个来源 · {subTags.length} 个标签 · 全部正常}
); })()}
); }; Object.assign(window, { IconRail, FeedRail, RailRow, RailGroup });