前端(新增 ui/yohaku/,各视图按 ui_id=vivid 分支渲染;classic 与后台不受影响) - 令牌层:设计稿数值原样落地,28 个 --v-* 为站主接口(自定义 CSS 可覆盖, 放 @layer 让出优先级);另加防线,中和 styles.css 裸选择器 (h1 衬线 / body 行高 / 后台 .stat/.toolbar/.chip 卡片)漏进余白元素 - 顶栏(滚动收缩态)/ 阅读进度(顶部 2px + READ n%)/ 页脚 - 首页:hero(统计条)/ 筛选(类型分段 + 标签 chips + 搜索)/ 长文卡片与短文纸条 / 分页 / 作品展示区(3 件 + 查看所有,参照 diygod.cc); 面板栅格改 1.6fr 1fr 1fr 且高度自适应内容(标签云明显大一圈) - 文章页:三栏「纸」—— 左时间线(前后各两篇)/ 中纸张 / 右目录与阅读进度; 短文详情不显示标题(h1 视觉隐藏保留无障碍) - 全部规则收敛在 html[data-ui='vivid'] 前缀下;无 !important 后端 - GET /api/posts/:slug 增 neighbors(前后各两篇,详情页侧栏时间线用) - SPA 缓存头:index.html no-cache,assets/ 带 hash 长期 immutable (此前无缓存头,浏览器会一直拿旧构建) 其他 - 站主自定义 CSS 管线(customCss.js + ui/sections.js,按分区注入,仅 vivid) - 删除旧 vivid 覆盖层实现(VividNav/Hero/Footer/Timeline/Toc、ui/vivid.css) - 设计稿原型附于仓库根 index.html;作品表清掉外链 diygod.cc 的示例封面, 改用本地占位图(public/covers/)
81 lines
2.7 KiB
JavaScript
81 lines
2.7 KiB
JavaScript
import { watch } from 'vue'
|
||
import { site } from './site'
|
||
|
||
// 站主自定义 CSS 的注入管线。
|
||
//
|
||
// 两个 <style>:global 常驻,section 跟着路由换,这样切页面不用重新解析全局那段。
|
||
// 只在 vivid 下注入,并且 /admin 下一律清空 —— 站主写坏样式也影响不到后台。
|
||
//
|
||
// 安全:只写 textContent,绝不用 innerHTML。textContent 不会被 HTML 解析,
|
||
// 所以站主的 CSS 里即使写了 </style> 也逃不出去,因此不需要引入 CSS 清洗库。
|
||
// (能写这两段的只有管理员,本身也不是权限边界。)
|
||
|
||
const GLOBAL_ID = 'one-css-global'
|
||
const SECTION_ID = 'one-css-section'
|
||
|
||
// 路由名 → 分区 id。tag 归到 tags;not-found / 其他一律只吃 global。
|
||
const ROUTE_SECTION = {
|
||
home: 'home',
|
||
post: 'post',
|
||
archive: 'archive',
|
||
tags: 'tags',
|
||
tag: 'tags',
|
||
projects: 'projects',
|
||
about: 'about'
|
||
}
|
||
|
||
function ensureStyle(id) {
|
||
if (typeof document === 'undefined') return null
|
||
// 先按 id 找再建:dev 热更新会重复执行模块,不查会堆出一串重复 <style>
|
||
let el = document.getElementById(id)
|
||
if (!el) {
|
||
el = document.createElement('style')
|
||
el.id = id
|
||
document.head.appendChild(el)
|
||
}
|
||
return el
|
||
}
|
||
|
||
export function sectionForRoute(route) {
|
||
if (!route || !route.name) return ''
|
||
return ROUTE_SECTION[route.name] || ''
|
||
}
|
||
|
||
function cssFor(section) {
|
||
if (!section) return ''
|
||
const map = site.custom_css
|
||
if (!map || typeof map !== 'object') return ''
|
||
const v = map[section]
|
||
return typeof v === 'string' ? v : ''
|
||
}
|
||
|
||
export function refreshCustomCss(route) {
|
||
const globalEl = ensureStyle(GLOBAL_ID)
|
||
const sectionEl = ensureStyle(SECTION_ID)
|
||
if (!globalEl || !sectionEl) return
|
||
|
||
const isAdmin = !!(route && route.path && route.path.startsWith('/admin'))
|
||
if (site.ui_id !== 'vivid' || isAdmin) {
|
||
// 清空而不是跳过:避免从公开页跳到后台时残留上一页的分区样式
|
||
if (globalEl.textContent) globalEl.textContent = ''
|
||
if (sectionEl.textContent) sectionEl.textContent = ''
|
||
return
|
||
}
|
||
|
||
const globalCss = cssFor('global')
|
||
const sectionCss = cssFor(sectionForRoute(route))
|
||
if (globalEl.textContent !== globalCss) globalEl.textContent = globalCss
|
||
if (sectionEl.textContent !== sectionCss) sectionEl.textContent = sectionCss
|
||
}
|
||
|
||
export function installCustomCss(router) {
|
||
refreshCustomCss(router.currentRoute.value)
|
||
router.afterEach((to) => refreshCustomCss(to))
|
||
// 站主切 UI 时立刻重新注入/清空。保存 CSS 后需要重新加载页面才会生效
|
||
// (site 只在启动时取一次),这是当前有意的简化。
|
||
watch(
|
||
() => site.ui_id,
|
||
() => refreshCustomCss(router.currentRoute.value)
|
||
)
|
||
}
|