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
+42
View File
@@ -0,0 +1,42 @@
/**
* 外链跳转确认页工具(配合后端 GET /out?url=<encoded>):
* - 站内路径(/ 开头)原样返回
* - http(s) 外链 → /out?url=<encodeURIComponent(url)>(确认页 5s 倒计时后跳转)
* - 其他协议(javascript: 等)→ 返回 '#' 并 console 警告(防 XSS 链接)
*/
export function safeOutUrl(url) {
const u = String(url || '').trim();
if (!u) return '#';
if (u.startsWith('/')) return u; // 站内绝对/相对路径(含 /uploads、/out 本身)
if (/^https?:\/\//i.test(u)) return '/out?url=' + encodeURIComponent(u);
console.warn('[outlink] 不支持的链接协议,已拦截:', u);
return '#';
}
/** 判断链接是否为站内链接(/ 开头) */
export function isInternalUrl(url) {
return String(url || '').startsWith('/');
}
/**
* footer_columnsJSON 字符串)解析为 [{ title, links: [{ label, url }] }]
* 解析失败或非数组返回 [](前端回退硬编码栏目)。
*/
export function parseFooterColumns(raw) {
try {
const arr = JSON.parse(raw || '[]');
if (!Array.isArray(arr)) return [];
return arr
.filter((c) => c && typeof c === 'object')
.map((c) => ({
title: String(c.title || ''),
links: Array.isArray(c.links)
? c.links
.filter((l) => l && typeof l === 'object')
.map((l) => ({ label: String(l.label || ''), url: String(l.url || '') }))
: [],
}));
} catch {
return [];
}
}