feat: 外观支持自定义 CSS/JS,并升级发帖工作台与积分存量看板
后台可注入全站 CSS/JS(SSR 生效,CSS 可热换);发帖/编辑改为锁一屏工作室;经济看板增加存量健康度;主题令牌与弹层误关一并收紧。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
100
frontend/lib/customCode.ts
Normal file
100
frontend/lib/customCode.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
// 全站自定义 CSS:SSR <style id="j13-custom-css"> + 客户端 #j13-custom-css-runtime。
|
||||
// 禁止对 SSR 节点 remove/改写;预览时 disable SSR,由 runtime 作为唯一源。
|
||||
|
||||
export const MAX_CUSTOM_CSS_CHARS = 48000;
|
||||
export const MAX_CUSTOM_JS_CHARS = 48000;
|
||||
|
||||
/** 空配置时展示的注释模板:保存前不生效,全部注释故误保存也无副作用 */
|
||||
export const SAMPLE_CUSTOM_CSS = `/* 姜十三 · 自定义 CSS 示例
|
||||
* 覆盖 globals.css 令牌或组件类。保存后对全站访客生效。
|
||||
* 勿写入 </style>
|
||||
*
|
||||
* 常用令牌:
|
||||
* --bg / --panel / --ink / --accent
|
||||
* --bg-halo 顶部光晕色(建议 ≤ 0.04)
|
||||
* --bg-noise-opacity 噪点层透明度
|
||||
* --r / --shadow-sm 卡片圆角与阴影
|
||||
*/
|
||||
|
||||
/* 改底色与光晕(浅 / 暗成对改)
|
||||
:root {
|
||||
--bg: #f7f7f5;
|
||||
--bg-halo: rgba(7, 192, 95, 0.04);
|
||||
--bg-noise-opacity: 0.03;
|
||||
--panel: #ffffff;
|
||||
--r: 12px;
|
||||
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.dark {
|
||||
--bg: #101211;
|
||||
--bg-halo: rgba(7, 192, 95, 0.03);
|
||||
--bg-noise-opacity: 0.04;
|
||||
}
|
||||
*/
|
||||
|
||||
/* 关掉光晕和噪点
|
||||
body::before,
|
||||
body::after {
|
||||
display: none;
|
||||
}
|
||||
*/
|
||||
|
||||
/* 隐藏页脚
|
||||
footer { display: none; }
|
||||
*/
|
||||
`;
|
||||
|
||||
export const SAMPLE_CUSTOM_JS = `// 姜十三 · 自定义 JS 示例
|
||||
// 仅整页加载执行一次(F5 / 首次进入),站内软导航不会重跑。
|
||||
// 勿写入 </script>
|
||||
//
|
||||
// 给 <html> 打标记,便于 CSS 写 html[data-custom] .panel { ... }
|
||||
// document.documentElement.dataset.custom = "1";
|
||||
//
|
||||
// 等 DOM 就绪后再改节点
|
||||
// document.addEventListener("DOMContentLoaded", function () {
|
||||
// // document.querySelector("footer")?.remove();
|
||||
// });
|
||||
`;
|
||||
|
||||
const SSR_STYLE_ID = "j13-custom-css";
|
||||
const RUNTIME_STYLE_ID = "j13-custom-css-runtime";
|
||||
|
||||
export function clipCustomSnippet(raw: string, max: number): string {
|
||||
const s = (raw || "").replace(/\u0000/g, "");
|
||||
if (s.length <= max) return s;
|
||||
return [...s].slice(0, max).join("");
|
||||
}
|
||||
|
||||
function ensureRuntimeStyle(): HTMLStyleElement {
|
||||
let el = document.getElementById(RUNTIME_STYLE_ID) as HTMLStyleElement | null;
|
||||
if (el) return el;
|
||||
el = document.createElement("style");
|
||||
el.id = RUNTIME_STYLE_ID;
|
||||
el.setAttribute("data-custom-css-source", "runtime");
|
||||
document.body.appendChild(el);
|
||||
return el;
|
||||
}
|
||||
|
||||
function removeRuntimeStyle(): void {
|
||||
document.getElementById(RUNTIME_STYLE_ID)?.remove();
|
||||
}
|
||||
|
||||
function ssrStyle(): HTMLStyleElement | null {
|
||||
return document.getElementById(SSR_STYLE_ID) as HTMLStyleElement | null;
|
||||
}
|
||||
|
||||
/** 将自定义 CSS 应用到当前文档。preview=true 时忽略 SSR 节点(外观页草稿)。 */
|
||||
export function applyCustomCSS(css: string, preview = false): void {
|
||||
if (typeof document === "undefined") return;
|
||||
const text = clipCustomSnippet(css, MAX_CUSTOM_CSS_CHARS).trim();
|
||||
const ssr = ssrStyle();
|
||||
if (ssr) ssr.disabled = preview;
|
||||
|
||||
if (!text) {
|
||||
removeRuntimeStyle();
|
||||
if (!preview && ssr) ssr.disabled = false;
|
||||
return;
|
||||
}
|
||||
ensureRuntimeStyle().textContent = text;
|
||||
}
|
||||
Reference in New Issue
Block a user