chore(spec): 归档 blog-5n——八任务收敛(后台深度优化全家桶)
Spec: 2026-11-05_blog-5n-admin-deep-optimization Co-authored-by: GLM-5.3
This commit is contained in:
1 parent
a555a3e450
commit
5c0e6ed900
29 files changed
+1559
-512
No files matched your search
+64
-76
@@ -24,6 +24,7 @@ collections:
|
||||
label_singular: 文章
|
||||
folder: content/posts
|
||||
create: true
|
||||
hint: 新文章可从置顶的《写作模板》复制起手(列表打开模板 → ⋯ 菜单 → Duplicate);英文版构建时自动机翻上线,无需在此维护
|
||||
# 中文主稿;英文为 TMT 预翻译入仓的 .en.md(posts-en 集合不在后台,防误改译文)
|
||||
slug: '{{slug}}'
|
||||
summary: '{{pubDate}} · {{title}}'
|
||||
@@ -34,14 +35,14 @@ collections:
|
||||
media_folder: assets
|
||||
public_folder: ./assets
|
||||
fields:
|
||||
- { label: 标题, name: title, widget: string }
|
||||
- { label: 标题, name: title, widget: string, hint: '建议 ≤32 字(搜索结果超长会截断)' }
|
||||
- { label: 日期, name: pubDate, widget: datetime, date_format: 'YYYY-MM-DD', time_format: false, format: 'YYYY-MM-DD' }
|
||||
- { label: 分轨, name: category, widget: select, options: [{label: 阅读, value: reading}, {label: 技术, value: tech}, {label: 照片, value: photo}] }
|
||||
- { label: 标签, name: tags, widget: list, required: true, hint: '至少一个(可写 read/写作/站点更新等);schema 必填,漏了构建会拦' }
|
||||
- { label: 标签, name: tags, widget: list, required: true, hint: '至少一个;优先复用现有标签:read(书评)/ 写作 / 站点更新——避免同义新造(如 阅读/read 分家);schema 必填,漏了构建会拦' }
|
||||
- { label: 置顶, name: pinned, widget: boolean, required: false, default: false, hint: '置顶文章排在首页/归档/书架最前;只影响顺序,不改日期、URL 与 RSS 时间语义' }
|
||||
- { label: 情绪色(书评色脊), name: mood, widget: color, required: false, hint: '#RRGGBB,仅阅读轨使用' }
|
||||
- { label: 原书作者, name: bookAuthor, widget: string, required: false }
|
||||
- { label: 一句话描述(书架策展语), name: description, widget: text, required: false }
|
||||
- { label: 一句话描述(书架策展语), name: description, widget: text, required: false, hint: '60–120 字最佳(搜索摘要与列表导语共用)' }
|
||||
- { label: 书封图, name: cover, widget: image, required: false, hint: '上传后自动生成,无需手填路径;仅阅读轨展示' }
|
||||
- label: 附视频(可选)
|
||||
name: video
|
||||
@@ -108,7 +109,7 @@ collections:
|
||||
widget: markdown
|
||||
required: false
|
||||
hint: '与图片至少填一项。支持 markdown 与排版类 HTML,例如 <span class="c-blue font-kai">彩色+楷体</span>;选中文字 → Insert ▸ 排版 可视化选字体/颜色(预览窗格直接显色显字体)。装饰色仍守“外壳灰阶、彩色只作强调”的纪律'
|
||||
- { label: 图片, name: image, widget: image, required: false, hint: '与文字至少填一项;建议先压缩' }
|
||||
- { label: 图片, name: image, widget: image, required: false, hint: '与文字至少填一项;上传后构建自动优化(WebP),原图无需手工压缩' }
|
||||
- label: 标签
|
||||
name: tags
|
||||
widget: list
|
||||
@@ -185,96 +186,82 @@ collections:
|
||||
media_folder: assets
|
||||
public_folder: ./assets
|
||||
fields:
|
||||
- label: 身份语
|
||||
name: tagline
|
||||
- label: 身份名片
|
||||
name: identity
|
||||
widget: object
|
||||
required: false
|
||||
hint: 首页大字身份语(双语,markdown 富文本);可写 <span class="font-kai c-blue">…</span> 做局部强调
|
||||
collapsed: true
|
||||
hint: 头像、身份语、副标题与站点图标(首页名片区 + 浏览器标签图标)
|
||||
fields:
|
||||
- { label: 中文, name: zh, widget: string, required: false }
|
||||
- { label: 英文, name: en, widget: string, required: false }
|
||||
- label: 页面排版覆盖(可选)
|
||||
name: typography
|
||||
- { label: 身份语(中文), name: tagline_zh, widget: string, required: false, hint: '首页大字身份语(markdown 富文本);可写 <span class="font-kai c-blue">…</span> 做局部强调' }
|
||||
- { label: 身份语(英文), name: tagline_en, widget: string, required: false }
|
||||
- { label: 副标题(中文,markdown 短文本), name: subtitle_zh, widget: string, required: false, hint: '渲染在身份语下方;留空不渲染' }
|
||||
- { label: 副标题(英文), name: subtitle_en, widget: string, required: false }
|
||||
- label: 头像(可选)
|
||||
name: avatar
|
||||
widget: image
|
||||
required: false
|
||||
hint: '留空 = 内置渐变占位;上传后自动优化为 60px WebP'
|
||||
- label: 站点图标 favicon
|
||||
name: favicon
|
||||
widget: image
|
||||
required: false
|
||||
# media_folder 按 file 集合语义相对 content/site/ 解析(写 content/site/assets 会嵌套翻倍——5l 实测教训)
|
||||
media_folder: ../../public/icons
|
||||
public_folder: /icons
|
||||
hint: '直接上传 .svg/.ico/.png(建议 64–512px 方图);留空回退 /favicon.svg'
|
||||
- label: 文案与排版
|
||||
name: copy
|
||||
widget: object
|
||||
required: false
|
||||
hint: 本页正文默认字体 / 强调色 / 字号缩放(0.8–1.25);不填用全站默认,只影响本页
|
||||
collapsed: true
|
||||
hint: 公告(渲染在名片区下方,留空不显示)+ 页面排版覆盖(本页正文默认字体/强调色/字号缩放,不填用全站默认,只影响主页)
|
||||
fields:
|
||||
- { label: 公告(中文,markdown), name: announcement_zh, widget: markdown, required: false, hint: '渲染在名片区下方;留空则不显示公告块' }
|
||||
- { label: 公告(英文), name: announcement_en, widget: markdown, required: false }
|
||||
- label: 正文字体类
|
||||
name: font
|
||||
name: typography_font
|
||||
widget: select
|
||||
required: false
|
||||
hint: 整页正文换字族;艺术/展示类字族体积大。5i 新增六款(小薇/庆科黄油/马善政/柳建毛草/得意黑/新晰黑)均按站内用字切片,单页增量 27–46KB;站外用字(生僻字)会回落系统字族,需按 docs 里的命令重切
|
||||
options: [font-serif, font-kai, font-sans, font-mono, font-song, font-kai-sys, font-hei-sys, font-fangsong, font-huiwen, font-kuaile, font-xiaowei, font-huangyou, font-mashanzheng, font-liujianmaocao, font-smiley, font-neoxihei]
|
||||
- label: 强调色类
|
||||
name: accent
|
||||
name: typography_accent
|
||||
widget: select
|
||||
required: false
|
||||
options: [ink-accent, ink-accent-soft, c-blue, c-blue-soft, c-teal, c-teal-soft, c-green, c-green-soft, c-yellow, c-yellow-soft, c-orange, c-orange-soft, c-red, c-red-soft, c-pink, c-pink-soft, c-purple, c-purple-soft]
|
||||
- { label: 字号缩放, name: scale, widget: number, required: false, value_type: float, min: 0.8, max: 1.25, step: 0.05, hint: '0.8–1.25;1 = 全站默认' }
|
||||
- label: 公告(可选,markdown)
|
||||
name: announcement
|
||||
- { label: 字号缩放, name: typography_scale, widget: number, required: false, value_type: float, min: 0.8, max: 1.25, step: 0.05, hint: '0.8–1.25;1 = 全站默认' }
|
||||
- label: 内容与开关
|
||||
name: content
|
||||
widget: object
|
||||
required: false
|
||||
hint: 渲染在名片区下方;留空则不显示公告块
|
||||
collapsed: true
|
||||
hint: 各区块条数与显隐开关;关掉的区块不渲染,越界数值自动夹取(默认 = 现状)
|
||||
fields:
|
||||
- { label: 中文, name: zh, widget: markdown, required: false }
|
||||
- { label: 英文, name: en, widget: markdown, required: false }
|
||||
- { label: 项目墙条数, name: projectsLimit, widget: number, required: false, value_type: int, min: 1, max: 99, hint: '首页项目墙显示几条(1–99,缺省 6)' }
|
||||
- label: 轨细分开关(首页文章列表)
|
||||
name: postsTracks
|
||||
widget: object
|
||||
required: false
|
||||
hint: 关掉某轨则该轨文章不在首页列出(全开=现状)
|
||||
fields:
|
||||
- { label: 阅读轨, name: reading, widget: boolean, required: false, default: true }
|
||||
- { label: 技术轨, name: tech, widget: boolean, required: false, default: true }
|
||||
- { label: 照片轨, name: photo, widget: boolean, required: false, default: true }
|
||||
- label: 副标题(可选,markdown 短文本)
|
||||
name: subtitle
|
||||
widget: object
|
||||
hint: 渲染在身份语下方;留空不渲染
|
||||
fields:
|
||||
- { label: 中文, name: zh, widget: string, required: false }
|
||||
- { label: 英文, name: en, widget: string, required: false }
|
||||
- label: 最新影像张数
|
||||
name: latestPhotos
|
||||
widget: number
|
||||
required: false
|
||||
default: 4
|
||||
min: 0
|
||||
max: 6
|
||||
hint: '1-6;填 0 = 隐藏「最新影像」区块。留空默认 4'
|
||||
- label: 首页每轨文章条数
|
||||
name: perTrackLimit
|
||||
widget: number
|
||||
required: false
|
||||
default: 6
|
||||
min: 4
|
||||
max: 8
|
||||
hint: 阅读轨/技术轨各显示条数,留空默认 6
|
||||
- label: 区块开关
|
||||
name: sections
|
||||
widget: object
|
||||
required: false
|
||||
hint: 关掉的区块不渲染(默认全开)
|
||||
fields:
|
||||
- { label: 文章列表, name: tracks, widget: boolean, required: false, default: true }
|
||||
- { label: 最新影像, name: photos, widget: boolean, required: false, default: true }
|
||||
- { label: 贡献热力图, name: heatmap, widget: boolean, required: false, default: true }
|
||||
- { label: 项目墙, name: projects, widget: boolean, required: false, default: true }
|
||||
- label: 头像(可选)
|
||||
name: avatar
|
||||
widget: image
|
||||
required: false
|
||||
hint: '留空 = 内置渐变占位;上传后自动优化为 60px WebP'
|
||||
- label: 站点图标 favicon
|
||||
name: favicon
|
||||
widget: image
|
||||
required: false
|
||||
# media_folder 按 file 集合语义相对 content/site/ 解析(写 content/site/assets 会嵌套翻倍——5l 实测教训)
|
||||
media_folder: ../../public/icons
|
||||
public_folder: /icons
|
||||
hint: '直接上传 .svg/.ico/.png(建议 64–512px 方图);留空回退 /favicon.svg'
|
||||
- label: 最新影像张数
|
||||
name: latestPhotos
|
||||
widget: number
|
||||
required: false
|
||||
default: 4
|
||||
min: 0
|
||||
max: 6
|
||||
hint: '1-6;填 0 = 隐藏「最新影像」区块。留空默认 4'
|
||||
- label: 首页每轨文章条数
|
||||
name: perTrackLimit
|
||||
widget: number
|
||||
required: false
|
||||
default: 6
|
||||
min: 4
|
||||
max: 8
|
||||
hint: 阅读轨/技术轨各显示条数,留空默认 6
|
||||
- { label: 项目墙条数, name: projectsLimit, widget: number, required: false, value_type: int, min: 1, max: 99, hint: '首页项目墙显示几条(1–99,缺省 6)' }
|
||||
- { label: 阅读轨, name: postsTracks_reading, widget: boolean, required: false, default: true, hint: '关掉则阅读轨文章不在首页列出(归档页仍可见);三轨全开 = 现状' }
|
||||
- { label: 技术轨, name: postsTracks_tech, widget: boolean, required: false, default: true }
|
||||
- { label: 照片轨, name: postsTracks_photo, widget: boolean, required: false, default: true }
|
||||
- { label: 显示文章列表, name: sections_tracks, widget: boolean, required: false, default: true }
|
||||
- { label: 显示最新影像, name: sections_photos, widget: boolean, required: false, default: true }
|
||||
- { label: 显示贡献热力图, name: sections_heatmap, widget: boolean, required: false, default: true }
|
||||
- { label: 显示项目墙, name: sections_projects, widget: boolean, required: false, default: true }
|
||||
- label: 社交链接(顺序即展示顺序)
|
||||
name: socials
|
||||
widget: list
|
||||
@@ -289,6 +276,7 @@ collections:
|
||||
required: false
|
||||
options: [github, rss, email, bilibili, x]
|
||||
hint: '不在列表里的留空 → 显示纯文字按钮'
|
||||
|
||||
- name: about
|
||||
label: 关于页
|
||||
file: content/site/about.yaml
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* 后台部署状态条(5n B1):Sveltia 管理页右下角浮层,轮询站点构建状态。
|
||||
* spec:.spec/specs/2026-11-05_blog-5n-admin-deep-optimization(T4)
|
||||
*
|
||||
* JSON 接口契约(build.sh 每次部署写 /admin/build-status.json):
|
||||
* { "sha": "a555a3e…", // 部署 commit(展示取前 7 位)
|
||||
* "time": "2026-11-05T19:20:25+08:00", // 构建完成时刻(展示取 HH:MM)
|
||||
* "state": "ok" | "building" | "fail",
|
||||
* "pages": 53 } // 页面数(仪表盘消费,本条不读)
|
||||
* 文件缺失 / 响应非 200 / JSON 解析失败 / state 非法 → 视为「不可知」:
|
||||
* 胶囊淡出、不报错,下一轮照常再试——状态条是增益件,接口坏了不能吵主人。
|
||||
*
|
||||
* 为什么轮询而不是 SSE/WebSocket:本站是纯静态托管(CDN 上没有可驻留的
|
||||
* 服务端进程,无推送通道);「保存→看部署结果」的节奏里 10s 间隔已足够
|
||||
* 感知,且零服务端成本、零额外基础设施。
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.DEPLOY_STATUS) return; // 防重复初始化:脚本被引入两次时静默退出
|
||||
var POLL_MS = 10000;
|
||||
var timer = null; // null = 停表中(初始 / 页面隐藏)
|
||||
|
||||
var STATES = {
|
||||
ok: { bg: 'rgba(28,30,34,.92)', label: '已上线' },
|
||||
building: { bg: 'rgba(52,84,209,.92)', label: '构建中…' },
|
||||
fail: { bg: 'rgba(179,32,32,.92)', label: '构建失败' },
|
||||
};
|
||||
|
||||
/** 构建中呼吸动画的 keyframes 无法用元素内联样式表达,注入一次 <style>(仍零外部资源) */
|
||||
function injectKeyframes() {
|
||||
var style = document.createElement('style');
|
||||
style.id = 'deploy-status-style';
|
||||
style.textContent = '@keyframes deploy-status-breath{0%,100%{opacity:.7}50%{opacity:1}}';
|
||||
document.head.appendChild(style);
|
||||
return style;
|
||||
}
|
||||
|
||||
function createPill() {
|
||||
var el = document.createElement('div');
|
||||
el.setAttribute('role', 'status'); // 状态变化对读屏器温和播报
|
||||
// 全部样式内联在 JS 里:不依赖外部 CSS,单文件即可被任意页面引入
|
||||
el.style.cssText = [
|
||||
'position:fixed',
|
||||
'right:14px',
|
||||
'bottom:14px',
|
||||
'z-index:10000',
|
||||
'padding:6px 12px',
|
||||
'font:12px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif',
|
||||
'color:#fff',
|
||||
'border-radius:999px',
|
||||
'box-shadow:0 2px 10px rgba(0,0,0,.18)',
|
||||
'user-select:none',
|
||||
'opacity:0', // 初始隐身,拿到合法数据才淡入
|
||||
'transition:opacity .2s,background-color .2s', // 出现与状态切换均为 200ms 过渡
|
||||
'pointer-events:none', // 默认不挡下层 UI;仅 fail 态(可点击)放开
|
||||
].join(';');
|
||||
el.addEventListener('click', function () {
|
||||
// 只有 fail 态放开 pointer-events,此监听只会在「构建失败」时被触发
|
||||
location.reload();
|
||||
});
|
||||
document.body.appendChild(el);
|
||||
return el;
|
||||
}
|
||||
|
||||
/** 从 ISO 串直接截 HH:MM 而不走 Date 本地化:展示构建机墙钟时间,不随浏览器时区漂移 */
|
||||
function hhmm(iso) {
|
||||
var m = /T(\d{2}:\d{2})/.exec(String(iso || ''));
|
||||
if (m) return m[1];
|
||||
var d = new Date(iso); // 兜底:万一 build.sh 改用非 ISO 格式,退本地时区展示
|
||||
if (isNaN(d.getTime())) return '';
|
||||
var pad = function (n) { return (n < 10 ? '0' : '') + n; };
|
||||
return pad(d.getHours()) + ':' + pad(d.getMinutes());
|
||||
}
|
||||
|
||||
function boot() {
|
||||
var el = createPill();
|
||||
|
||||
function hide() {
|
||||
el.style.opacity = '0';
|
||||
el.style.pointerEvents = 'none';
|
||||
el.style.animation = 'none';
|
||||
el.title = '';
|
||||
}
|
||||
|
||||
function render(data) {
|
||||
var conf = STATES[data && data.state];
|
||||
if (!conf) { // 不可知 = 淡出静默,不区分缺文件/坏 JSON/非法枚举(对用户都是同一件事)
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
if (data.state === 'ok') {
|
||||
var parts = [];
|
||||
var sha7 = String(data.sha || '').slice(0, 7);
|
||||
if (sha7) parts.push(sha7);
|
||||
var t = hhmm(data.time);
|
||||
if (t) parts.push(t);
|
||||
el.textContent = parts.length ? conf.label + ' · ' + parts.join(' · ') : conf.label;
|
||||
} else {
|
||||
el.textContent = conf.label;
|
||||
}
|
||||
el.style.backgroundColor = conf.bg;
|
||||
// 呼吸仅限 building;离开时置 none,状态间观感交给 background-color 的 200ms 过渡
|
||||
el.style.animation = data.state === 'building'
|
||||
? 'deploy-status-breath 1.6s ease-in-out infinite'
|
||||
: 'none';
|
||||
if (data.state === 'fail') {
|
||||
el.style.pointerEvents = 'auto';
|
||||
el.style.cursor = 'pointer';
|
||||
el.title = '点击刷新页面'; // fail 的唯一动作是刷新:取回最新静态产物(含本文件与状态 JSON)
|
||||
} else {
|
||||
el.style.pointerEvents = 'none';
|
||||
el.style.cursor = 'default';
|
||||
el.title = '';
|
||||
}
|
||||
el.style.opacity = '1';
|
||||
}
|
||||
|
||||
function poll() {
|
||||
fetch('/admin/build-status.json', { cache: 'no-store' }) // no-store:状态文件必须每次拿实况
|
||||
.then(function (r) { return r.ok ? r.json() : null; }) // 404 等非 200 → 当不可知处理
|
||||
.then(function (data) { render(data && typeof data === 'object' ? data : {}); })
|
||||
.catch(function () { hide(); }); // 网络层与 JSON 解析失败同路:淡出,下一轮再试
|
||||
}
|
||||
|
||||
function loop() {
|
||||
poll();
|
||||
timer = setTimeout(loop, POLL_MS);
|
||||
}
|
||||
|
||||
document.addEventListener('visibilitychange', function () {
|
||||
if (document.visibilityState === 'visible') {
|
||||
// 回来立即拉(隐藏期间可能已构建完成),并重置 10s 节拍
|
||||
if (timer) clearTimeout(timer);
|
||||
loop();
|
||||
} else if (timer) {
|
||||
// 隐藏即停:省请求,也避免后台标签被浏览器节流的定时器醒来后集中补发
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
});
|
||||
|
||||
window.DEPLOY_STATUS = {
|
||||
// 挂载即去重标记;stop 供控制台/测试卸载(页面自身是 SPA,正常导航不会触发)
|
||||
stop: function () {
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = null;
|
||||
el.remove();
|
||||
var style = document.getElementById('deploy-status-style');
|
||||
if (style) style.remove();
|
||||
delete window.DEPLOY_STATUS;
|
||||
},
|
||||
};
|
||||
|
||||
if (document.visibilityState === 'visible') loop();
|
||||
// 初始就隐藏(预渲染标签页):等 visibilitychange 回来再首拉
|
||||
}
|
||||
|
||||
if (document.body) boot();
|
||||
else document.addEventListener('DOMContentLoaded', boot); // 若被放到 <head> 且未加 defer
|
||||
})();
|
||||
+24
-2
@@ -3,10 +3,30 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="robots" content="noindex" />
|
||||
<title>Sveltia CMS</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>wait4x 后台</title>
|
||||
<!-- 站点图标 + PWA(5n B2:手机加主屏独立入口) -->
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="manifest" href="./manifest.webmanifest" />
|
||||
<style>
|
||||
/* 速通坞(5n D1):日常要跳的五个入口常驻右上,不挡 Sveltia 主 UI(对话层 z 更高) */
|
||||
#quick-dock{position:fixed;top:10px;right:12px;z-index:9000;display:flex;gap:4px;
|
||||
font:12px/1 system-ui,sans-serif;opacity:.78;transition:opacity .2s}
|
||||
#quick-dock:hover{opacity:1}
|
||||
#quick-dock a{color:#5b6472;text-decoration:none;padding:4px 8px;border-radius:6px;background:rgba(127,131,140,.12)}
|
||||
#quick-dock a:hover{color:#3454d1;background:rgba(52,84,209,.1)}
|
||||
@media (max-width:720px){#quick-dock a:not(.qd-key){display:none}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- pinned 本地 bundle(0.252.0):官方模板仅此一个 script,无 CSS link、无 type=module -->
|
||||
<nav id="quick-dock" aria-label="快捷入口">
|
||||
<a class="qd-key" href="https://wait4x.online/artalk/" target="_blank" rel="noopener">评论</a>
|
||||
<a class="qd-key" href="./stats.html" target="_blank" rel="noopener">仪表盘</a>
|
||||
<a href="https://git.wait4x.online/wait4x/blog" target="_blank" rel="noopener">仓库</a>
|
||||
<a href="https://git.wait4x.online/wait4x/blog/src/branch/main/docs/%E5%90%8E%E5%8F%B0%E5%9B%9E%E6%BB%9A%E6%8C%87%E5%BC%95.md" target="_blank" rel="noopener">回滚</a>
|
||||
<a href="https://git.wait4x.online/wait4x/blog/src/branch/main/docs/%E7%AE%A1%E7%90%86%E5%8F%B0.md" target="_blank" rel="noopener">手册</a>
|
||||
</nav>
|
||||
<!-- pinned 本地 bundle(0.259.0,5n T1 升级):官方模板仅此一个 script,无 CSS link、无 type=module -->
|
||||
<script src="./sveltia-cms.js"></script>
|
||||
<!-- 艺术字体切片(unicode-range 按需取片):选择面板的示例字形用真实字体渲染 -->
|
||||
<link rel="stylesheet" href="/fonts/subset/hwmc/fonts.css" />
|
||||
@@ -20,5 +40,7 @@
|
||||
<script src="./typography-classes.js"></script>
|
||||
<script src="./typography-editor.js"></script>
|
||||
<script src="./preview-style.js"></script>
|
||||
<!-- 部署状态条(5n B1):轮询 /admin/build-status.json -->
|
||||
<script src="./deploy-status.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "wait4x 后台",
|
||||
"short_name": "后台",
|
||||
"start_url": "/admin/",
|
||||
"scope": "/admin/",
|
||||
"display": "standalone",
|
||||
"background_color": "#ffffff",
|
||||
"theme_color": "#3454d1",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 后台预览一致性(5l T1):把站点排版效果注入 Sveltia 预览窗格。
|
||||
* 后台预览一致性(5l T1 / 5n T7):把站点排版效果注入 Sveltia 预览窗格。
|
||||
*
|
||||
* 现状问题:markdown 里的排版类(font- 字体、c-/ink- 颜色)在预览里是素文本——
|
||||
* 类名随 DOM 保留(typography-editor 的 toPreview 输出带 class),但预览
|
||||
@@ -12,6 +12,10 @@
|
||||
* 2. 自托管艺术字体挂切片 CSS(public/fonts/<family>/fonts.css,
|
||||
* unicode-range 按需取片——预览里只拉用到的字所落在的切片,不整包)。
|
||||
*
|
||||
* 暗色(5n T7):所有色值走 CSS 自定义属性,变量缺省值 = 亮态(与改造前同一批
|
||||
* 字面量,无暗色命中时逐字节等价),末尾一段 @media (prefers-color-scheme: dark)
|
||||
* 统一换档——后台切暗色时预览不再用亮态色值配暗底。
|
||||
*
|
||||
* serif/kai/kuaile 的 CSS 经 Vite ?url 打包成哈希名,后台引不到稳定 URL,
|
||||
* 预览走字体栈里的系统回退(Kaiti SC/Songti)——近似可见,文档已注明。
|
||||
* 加载顺序:须在 typography-classes.js 之后、CMS init 之前(index.html 固定序)。
|
||||
@@ -20,30 +24,96 @@
|
||||
const CLASSES = window.TYPOGRAPHY_CLASSES;
|
||||
if (!CLASSES || !window.CMS || !window.CMS.registerPreviewStyle) return;
|
||||
|
||||
/**
|
||||
* 基础排版的暗态值(亮态值不在这里登记,直接作各条 var() 的回落值)。
|
||||
* 出处:src/styles/tokens.css `[data-theme="dark"]` 段,勿凭观感改。
|
||||
*/
|
||||
const BASE_DARK = {
|
||||
'pv-accent': '#8ea2ff', // --accent
|
||||
'pv-line': '#262b33', // --line(站点暗色分隔线就是这个档)
|
||||
'pv-quote-ink': '#8b8f98', // --ink-3
|
||||
'pv-code-bg': '#1f242b', // --bg-soft
|
||||
};
|
||||
|
||||
/**
|
||||
* 颜色类暗态值,键 = typography-classes.js 里的 cls。同样取自暗色 tokens 段:
|
||||
* 站点语义色板暗态是独立取值(不是亮态加亮),硬套亮态在 #191d23 底上不达 AA。
|
||||
*/
|
||||
const CLASS_DARK = {
|
||||
'ink-1': '#f5f5f7', // --ink-1
|
||||
'ink-2': '#c9ccd4', // --ink-2
|
||||
'ink-3': '#8b8f98', // --ink-3
|
||||
// 站点 CSS 里 ink-muted 映射 --ink-4,而暗态 --ink-4 (#5c6066) 是装饰档,
|
||||
// 暗底上近乎不可见;亮态 swatch 本就取的是 --ink-faint 的值,暗态随之一致
|
||||
'ink-muted': '#84888f', // --ink-faint
|
||||
'ink-warm': '#d6cbbd', // --ink-warm
|
||||
'ink-cool': '#b9c6d6', // --ink-cool
|
||||
'ink-faint': '#84888f', // --ink-faint
|
||||
'ink-accent': '#8ea2ff', // --accent
|
||||
'ink-accent-soft': '#a8b7f7', // --accent-soft-text
|
||||
'c-blue': '#7fb0ff',
|
||||
'c-blue-soft': '#3381ff',
|
||||
'c-teal': '#6fd6c8',
|
||||
'c-teal-soft': '#2a9384',
|
||||
'c-green': '#7ddf99',
|
||||
'c-green-soft': '#269a47',
|
||||
'c-yellow': '#ffd166',
|
||||
'c-yellow-soft': '#af7a00',
|
||||
'c-orange': '#ffb163',
|
||||
'c-orange-soft': '#d16800',
|
||||
'c-red': '#ff8b8b',
|
||||
'c-red-soft': '#ff2828',
|
||||
'c-pink': '#ff9ecb',
|
||||
'c-pink-soft': '#ff0d7e',
|
||||
'c-purple': '#c4a6ff',
|
||||
'c-purple-soft': '#9a67ff',
|
||||
};
|
||||
|
||||
/**
|
||||
* 未登记暗态值的类(清单后续新增)用 color-mix 提亮近似——宁可偏亮,
|
||||
* 也不要暗底上跑亮态深色(那是这次要修的问题本身)。
|
||||
*/
|
||||
const approxDark = (swatch) => `color-mix(in oklab, ${swatch} 82%, #fff)`;
|
||||
|
||||
/** 预览基调:近似站点 .rich 的通用排版(目标是类效果可见,非像素级还原) */
|
||||
const baseCss = [
|
||||
'p{margin:.6em 0;line-height:1.75}',
|
||||
'h1,h2,h3,h4{font-weight:600;line-height:1.4;margin:1.2em 0 .5em}',
|
||||
'h2{font-size:1.3em}h3{font-size:1.15em}',
|
||||
'ul,ol{padding-left:1.5em;margin:.6em 0}li{margin:.25em 0}',
|
||||
'a{color:#3454d1;text-decoration:underline}',
|
||||
'blockquote{margin:.8em 0;padding:.2em 1em;border-left:3px solid #e6e7eb;color:#6b6e78}',
|
||||
"code{font-family:'JetBrains Mono',ui-monospace,monospace;font-size:.92em;background:#f2f3f5;padding:.1em .35em;border-radius:4px}",
|
||||
'a{color:var(--pv-accent,#3454d1);text-decoration:underline}',
|
||||
'blockquote{margin:.8em 0;padding:.2em 1em;border-left:3px solid var(--pv-line,#e6e7eb);color:var(--pv-quote-ink,#6b6e78)}',
|
||||
"code{font-family:'JetBrains Mono',ui-monospace,monospace;font-size:.92em;background:var(--pv-code-bg,#f2f3f5);padding:.1em .35em;border-radius:4px}",
|
||||
'img{max-width:100%}',
|
||||
'hr{border:none;border-top:1px solid #e6e7eb;margin:1.5em 0}',
|
||||
'hr{border:none;border-top:1px solid var(--pv-line,#e6e7eb);margin:1.5em 0}',
|
||||
'strong{font-weight:700}',
|
||||
].join('');
|
||||
|
||||
/** 排版类规则:清单驱动(swatch/ family 即预览所用值,亮态口径) */
|
||||
/** 排版类规则:清单驱动(swatch/family 亮态值作 var() 回落 = 面板所见) */
|
||||
const rules = [];
|
||||
const darkDecls = [];
|
||||
for (const f of CLASSES.font || []) {
|
||||
if (f.family) rules.push(`.${f.cls}{font-family:${f.family}}`);
|
||||
}
|
||||
for (const c of CLASSES.color || []) {
|
||||
if (c.swatch) rules.push(`.${c.cls}{color:${c.swatch}}`);
|
||||
if (!c.swatch) continue;
|
||||
const name = `--pv-${c.cls}`;
|
||||
rules.push(`.${c.cls}{color:var(${name},${c.swatch})}`);
|
||||
darkDecls.push(`${name}:${CLASS_DARK[c.cls] || approxDark(c.swatch)}`);
|
||||
}
|
||||
for (const [name, value] of Object.entries(BASE_DARK)) darkDecls.push(`--${name}:${value}`);
|
||||
|
||||
window.CMS.registerPreviewStyle(baseCss + rules.join(''), { raw: true });
|
||||
/**
|
||||
* 为什么用媒体查询而不是监听「后台切主题」:Sveltia 的 registerPreviewStyle 是
|
||||
* 一次性注入(无更新/移除接口),registerEventListener 只支持 pre/postSave 与
|
||||
* pre/postPublish 这类保存事件,没有任何主题切换回调;后台主题默认跟随系统,
|
||||
* 预览文档的 prefers-color-scheme 与后台同源,浏览器自己换档即可,零重注入。
|
||||
* 代价:用户在后台显式选 light 而系统是 dark 时预览偏暗——已知取舍,比监听
|
||||
* Sveltia 内部 DOM(非公开契约)稳妥。
|
||||
*/
|
||||
const darkCss = darkDecls.length ? `@media (prefers-color-scheme: dark){:root{${darkDecls.join(';')}}}` : '';
|
||||
|
||||
window.CMS.registerPreviewStyle(baseCss + rules.join('') + darkCss, { raw: true });
|
||||
|
||||
// 字体切片(URL 模式):预览文档自己的 @font-face;去重后逐个挂
|
||||
const seen = new Set();
|
||||
|
||||
+390
-361
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user