余白 UI:按设计稿模块化还原 + 站主自定义 CSS + 详情 neighbors + 静态缓存头
前端(新增 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/)
This commit is contained in:
+75
-2
@@ -10,6 +10,31 @@ const LIGHT_SKINS = ['paper', 'sage', 'rose']
|
||||
const MODE_KEY = 'one.themeMode'
|
||||
const VALID_MODES = ['light', 'dark', 'auto']
|
||||
|
||||
// 整站 UI 版本 —— 站点级设置(后台切换),不属于访客偏好。
|
||||
// classic 是原来那套极简版;vivid 是另一套更外放、且支持自定义 CSS 的版本。
|
||||
const UI_KEY = 'one.ui'
|
||||
const VALID_UIS = ['classic', 'vivid']
|
||||
const UI_DEFAULT = 'classic'
|
||||
|
||||
function readUICache() {
|
||||
if (typeof localStorage === 'undefined') return UI_DEFAULT
|
||||
try {
|
||||
const v = localStorage.getItem(UI_KEY)
|
||||
return VALID_UIS.includes(v) ? v : UI_DEFAULT
|
||||
} catch (_) {
|
||||
return UI_DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
function writeUICache(id) {
|
||||
if (typeof localStorage === 'undefined') return
|
||||
try {
|
||||
localStorage.setItem(UI_KEY, id)
|
||||
} catch (_) {
|
||||
/* private mode 等场景静默 */
|
||||
}
|
||||
}
|
||||
|
||||
function readMode() {
|
||||
if (typeof localStorage === 'undefined') return 'auto'
|
||||
try {
|
||||
@@ -47,6 +72,10 @@ function isValidLightSkin(id) {
|
||||
return LIGHT_SKINS.includes(id)
|
||||
}
|
||||
|
||||
function isValidUI(id) {
|
||||
return VALID_UIS.includes(id)
|
||||
}
|
||||
|
||||
export const site = reactive({
|
||||
site_title: 'ONE · 一个博客',
|
||||
site_desc: '长文与短文,同一种节奏。',
|
||||
@@ -58,12 +87,34 @@ export const site = reactive({
|
||||
light_skin_id: 'paper',
|
||||
// theme_id 是老字段,保留读取兼容用;新代码不要再写。
|
||||
theme_id: 'paper',
|
||||
// ui_id 是站点级设置,服务端为准;本地缓存只用于首屏不闪。
|
||||
ui_id: readUICache(),
|
||||
// 按分区存放的站主自定义 CSS(仅 vivid 生效,且不在 /admin 注入)
|
||||
custom_css: {},
|
||||
themeMode: readMode(),
|
||||
loaded: false
|
||||
})
|
||||
|
||||
// data-theme → 前台皮肤(paper / sage / rose / ink)
|
||||
// data-theme → 前台皮肤。classic: paper/sage/rose/ink;vivid: vivid/vivid-dark
|
||||
// data-admin-theme → 后台明暗(light / dark),跟 site.themeMode 同步
|
||||
// data-ui → 整站 UI 版本(classic / vivid)
|
||||
//
|
||||
// 后台一律强制回 classic:vivid.css 是覆盖层,它也会命中 .btn / .input 这类
|
||||
// 全局类,而后台表单就在用这些类。所以后台不仅要走 --admin-*,还要在
|
||||
// data-ui / data-theme 两个轴上退回原路径,才能保证后台观感零变化。
|
||||
// App.vue 在路由变化时调用 setAdminScope 维护这个开关。
|
||||
// 初值从当前路径推断:直接刷新 /admin 时,模块加载阶段就要退回 classic,
|
||||
// 否则这里会覆盖掉 index.html 里那段预置脚本的结果,闪一下 vivid。
|
||||
let adminScope =
|
||||
typeof location !== 'undefined' && location.pathname.indexOf('/admin') === 0
|
||||
|
||||
export function setAdminScope(on) {
|
||||
const next = !!on
|
||||
if (next === adminScope) return
|
||||
adminScope = next
|
||||
applyTheme()
|
||||
}
|
||||
|
||||
function applyTheme(animate = false) {
|
||||
// 亮度突变是 Apple 明确点名的反模式:用户主动切换时给一次 250ms 交叉过渡
|
||||
if (animate && typeof document !== 'undefined') {
|
||||
@@ -72,8 +123,15 @@ function applyTheme(animate = false) {
|
||||
setTimeout(() => el.classList.remove('theme-anim'), 300)
|
||||
}
|
||||
const isDark = site.themeMode === 'dark' || (site.themeMode === 'auto' && systemPrefersDark())
|
||||
document.documentElement.setAttribute('data-theme', effectiveTheme(site.themeMode, site.light_skin_id))
|
||||
// vivid 自带亮/暗两套调色板,不复用 paper/sage/rose —— 否则两套 token 会互相打架。
|
||||
const vivid = !adminScope && site.ui_id === 'vivid'
|
||||
const theme = vivid
|
||||
? (isDark ? 'vivid-dark' : 'vivid')
|
||||
: effectiveTheme(site.themeMode, site.light_skin_id)
|
||||
const ui = vivid && isValidUI(site.ui_id) ? site.ui_id : UI_DEFAULT
|
||||
document.documentElement.setAttribute('data-theme', theme)
|
||||
document.documentElement.setAttribute('data-admin-theme', isDark ? 'dark' : 'light')
|
||||
document.documentElement.setAttribute('data-ui', ui)
|
||||
}
|
||||
applyTheme()
|
||||
|
||||
@@ -93,6 +151,10 @@ export async function loadSite() {
|
||||
Object.assign(site, data)
|
||||
// 防御性:服务端返回的 light_skin_id 必须是白名单之一,否则兜底 paper
|
||||
if (!isValidLightSkin(site.light_skin_id)) site.light_skin_id = 'paper'
|
||||
// ui_id 同理;同时回写本地缓存,让下一次首屏不用等接口就知道该用哪套 UI
|
||||
if (!isValidUI(site.ui_id)) site.ui_id = UI_DEFAULT
|
||||
if (!site.custom_css || typeof site.custom_css !== 'object') site.custom_css = {}
|
||||
writeUICache(site.ui_id)
|
||||
site.loaded = true
|
||||
} catch (e) {
|
||||
site.loaded = false
|
||||
@@ -120,6 +182,12 @@ watch(() => site.light_skin_id, () => {
|
||||
if (site.themeMode === 'auto') applyTheme()
|
||||
})
|
||||
|
||||
// 站主切 UI → 立刻换整套(自定义 CSS 的重新注入由 customCss.js 自己监听)
|
||||
watch(() => site.ui_id, () => {
|
||||
writeUICache(site.ui_id)
|
||||
applyTheme()
|
||||
})
|
||||
|
||||
export function applyDocTitle(sub) {
|
||||
const base = site.site_title || 'ONE'
|
||||
document.title = sub ? `${sub} · ${base}` : base
|
||||
@@ -128,4 +196,9 @@ export function applyDocTitle(sub) {
|
||||
export const SKIN_CONSTANTS = Object.freeze({
|
||||
DARK_SKIN,
|
||||
LIGHT_SKINS: [...LIGHT_SKINS]
|
||||
})
|
||||
|
||||
export const UI_CONSTANTS = Object.freeze({
|
||||
DEFAULT: UI_DEFAULT,
|
||||
UIS: [...VALID_UIS]
|
||||
})
|
||||
Reference in New Issue
Block a user