// =========================================================
// 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 (
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 });