feat: 页脚栏目自定义(后台编辑器+前台渲染)+ 外链跳转确认页(/out 5秒倒计时+全站拦截)

This commit is contained in:
2026-08-12 22:27:46 +08:00
parent 5c220f7ecf
commit 2cf6368abf
7 changed files with 236 additions and 21 deletions
+24 -17
View File
@@ -1,5 +1,6 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { safeOutUrl, parseFooterColumns } from '../lib/outlink.js';
/**
* 页脚文案渲染:footer_copyright / footer_powered / footer_desc 为管理员字段,
@@ -28,18 +29,30 @@ export default function Footer({ settings = {}, version = '' }) {
const desc = settings.footer_desc || settings.site_description || '';
if (style === 'columns') {
// 导航栏目固定不可编辑(后台提示),首页/博客/论坛走 SPA,管理后台整页跳转
const navCols = [
// 栏目来源:后台 footer_columns 配置(非空优先);空则回退硬编码导航
const hardcodedCols = [
{ title: '导航', links: [
{ to: '/', label: '首页' },
{ to: '/blog.html', label: '博客' },
{ to: '/forum.html', label: '论坛' },
{ label: '首页', url: '/' },
{ label: '博客', url: '/blog.html' },
{ label: '论坛', url: '/forum.html' },
] },
{ title: '其他', links: [
{ to: '/admin', label: '管理后台', external: true },
{ to: '/profile.html', label: '个人中心' },
{ label: '管理后台', url: '/admin', fullPage: true },
{ label: '个人中心', url: '/profile.html' },
] },
];
const customCols = parseFooterColumns(settings.footer_columns);
const cols = customCols.length > 0 ? customCols : hardcodedCols;
/** 栏目链接:站内路径走 SPA Link(整页跳转项除外);http(s) 外链走 /out 确认页 */
const colLink = (l, i) => {
const isSite = l.url.startsWith('/');
const isSpa = isSite && !l.fullPage;
return isSpa
? <Link key={i} to={l.url}>{l.label || l.url}</Link>
: <a key={i} href={safeOutUrl(l.url)}>{l.label || l.url}</a>;
};
return (
<footer className="footer-columns">
<div className="fc-grid">
@@ -51,17 +64,11 @@ export default function Footer({ settings = {}, version = '' }) {
</div>
{desc && <HtmlOrText value={desc} className="brand-desc" />}
</div>
{navCols.map((col) => (
<nav key={col.title} className="fc-col">
<div className="col-title">{col.title}</div>
{cols.map((col, ci) => (
<nav key={ci} className="fc-col">
<div className="col-title">{col.title || '未命名栏目'}</div>
<div className="col-links">
{col.links.map((l) =>
l.external ? (
<a key={l.to} href={l.to}>{l.label}</a>
) : (
<Link key={l.to} to={l.to}>{l.label}</Link>
)
)}
{(col.links || []).map((l, li) => colLink(l, ci + '-' + li))}
</div>
</nav>
))}
+19
View File
@@ -44,6 +44,25 @@ export default function Layout() {
const closeMenu = () => setMenuOpen(false);
// ── 全站外链拦截:http(s) 非本站链接 → /out 确认页(事件委托,覆盖 markdown 渲染出的外链)──
useEffect(() => {
const handler = (e) => {
if (e.defaultPrevented) return;
// 新标签打开(Ctrl/Cmd/Shift/中键)是用户主动分屏意图,不劫持
if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1) return;
const a = e.target && e.target.closest ? e.target.closest('a[href]') : null;
if (!a) return;
const href = (a.getAttribute('href') || '').trim();
if (!/^https?:\/\//i.test(href)) return; // 站内路径(/ 开头)不拦
// 本站同源(www.rainnya.asia)不拦——解析相对链接得到本站 origin 也自然跳过
if (new URL(href, window.location.origin).origin === window.location.origin) return;
e.preventDefault();
window.location.href = '/out?url=' + encodeURIComponent(href);
};
document.addEventListener('click', handler);
return () => document.removeEventListener('click', handler);
}, []);
// 初始化向导:未完成安装时跳转 /setup.html(迁移自 nav.js
useEffect(() => {
fetch('/api/setup/status')