feat: 页脚栏目自定义(后台编辑器+前台渲染)+ 外链跳转确认页(/out 5秒倒计时+全站拦截)
This commit is contained in:
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user