主题生成器支持素材前缀(主题素材可放 R2/OSS)
主题素材只有一处引用:主题 css 的 url()(分享卡是 Go 用 imaging 画的,不读素材), 所以只要让 CSS 里的地址指向对象存储即可。 - 导出面板新增「素材前缀」:空 = /static/images/themes/<slug>/(图片随二进制发布,与以前一致); 填 https://cdn.example.com/themes = CSS 写绝对 URL,zip 里的图片改为「上传用」 - 地址统一由 urlOf() 决定。修了一个重复实现:layerCSS 里复制过一份路径拼接, 只改 urlOf 会出现「背景走 CDN、装饰还指本地」 - 前缀随意切换不影响预览(预览恒用内联 dataURL);存进工程 json;?assetbase= 可自检 - INSTALL 文案按是否托管分叉,写清图片传到哪、换域名要重新导出 - 新增 docs/theme-assets-cdn.md:三种做法(生成器前缀 / 应用内 301 / 边缘改写)、 缓存与 ?v=、CORS 不需要、深色无素材、迁移用的 sed 命令 - README 更新;UI 修正:240px 面板里 4 字标签会被 flex 压窄,需 flex:0 0 auto + 84px
This commit is contained in:
@@ -190,6 +190,12 @@ WebP 体积 ≈ 像素数 × 质量。**真正的大头是像素**:CSS 里只
|
||||
| --- | --- | --- |
|
||||
| 质量 | 0.88 | 0.85~0.9 肉眼几乎看不出差别,低于 0.8 才明显糊 |
|
||||
| 长边上限 | 1600 | 只缩不放。整页背景建议 1920/不限制;装饰人物 1200 够用 |
|
||||
| 素材前缀 | 空 | 空 = 图片放 `static/images/themes/<slug>/` 随二进制发布;填 `https://cdn.example.com/themes` = CSS 写绝对地址,图片自己传对象存储(R2/OSS)。**前缀里不要带 slug** |
|
||||
|
||||
导出的 CSS 只有 `urlOf()` 一处决定图片地址,图层、背景、窄屏块全走它,不会出现
|
||||
「背景走 CDN、装饰还指本地」的半吊子状态。素材前缀只影响导出,预览永远是内联 dataURL。
|
||||
|
||||
详细取舍(生成器前缀 / 应用内 301 / 边缘改写)见 `docs/theme-assets-cdn.md`。
|
||||
|
||||
面板底部实时显示 `3 张,共 19KB;最大一张 bg.webp 7KB(1920×1080)`。
|
||||
改完立刻按「原图 + fx 参数」重编码全部素材,并报出省了多少 —— 与图片处理同一条非破坏原则,
|
||||
|
||||
@@ -338,6 +338,10 @@ const D = {
|
||||
/* 导出画质:webp 体积 ≈ 像素数 × 质量。压体积最有效的不是 q,是先砍像素
|
||||
—— CSS 里显示 400px 高的图,2 倍屏只需要 800px,上传 3000px 的立绘纯属白扛。 */
|
||||
export: { quality: 0.88, maxSide: 1600 },
|
||||
/* 素材前缀:空 = 走站点自己的 /static/images/themes(图片随二进制发布);
|
||||
填了(如 https://cdn.example.com/themes)= 导出的 CSS 写绝对 URL,图片放对象存储。
|
||||
前缀里不要带 slug,slug 由导出时自动拼在后面。 */
|
||||
assetBase: '',
|
||||
dark: {
|
||||
bg:'#141a22', surface:'#1d2530', navbar:'#141a22',
|
||||
text:'#e3eaf4', primary:'#6fa3e0', border:'#334052',
|
||||
@@ -356,6 +360,7 @@ const S = {
|
||||
palette: Object.assign({}, D.palette),
|
||||
narrow: Object.assign({}, D.narrow),
|
||||
export: Object.assign({}, D.export),
|
||||
assetBase: '',
|
||||
dark: Object.assign({}, D.dark),
|
||||
};
|
||||
|
||||
@@ -528,8 +533,8 @@ function layerCSS(l, preview){
|
||||
if (l.kind === 'image'){
|
||||
const a = assetById(l.asset);
|
||||
if (!a) return {image:'none', size:'auto', position:'left top'};
|
||||
const url = preview ? a.dataURL : `/static/images/themes/${S.slug}/${a.id}.webp`;
|
||||
return { image:`url("${url}")`, size: layerSize(l, a), position: POS(l) };
|
||||
// 统一走 urlOf:素材前缀(CDN)只在这一处判定,别再复制一份拼接逻辑
|
||||
return { image:`url("${urlOf(a, preview)}")`, size: layerSize(l, a), position: POS(l) };
|
||||
}
|
||||
if (l.kind === 'radial'){
|
||||
const c = rgba(l.color, l.alpha);
|
||||
@@ -559,7 +564,9 @@ function newLayer(kind){
|
||||
|
||||
/* ---------------------------- 导出 CSS ---------------------------- */
|
||||
function urlOf(a, preview){
|
||||
return preview ? a.dataURL : `/static/images/themes/${S.slug}/${a.id}.webp`;
|
||||
if (preview) return a.dataURL; // 预览必须用内联 dataURL,否则改一张图要等一轮部署
|
||||
const base = String(S.assetBase || '').trim().replace(/\/+$/, '');
|
||||
return base ? `${base}/${S.slug}/${a.id}.webp` : `/static/images/themes/${S.slug}/${a.id}.webp`;
|
||||
}
|
||||
|
||||
function bgVars(cfg, preview){
|
||||
@@ -1551,6 +1558,26 @@ function renderExportEditor(){
|
||||
};
|
||||
r2.append(label('长边上限', '62px'), side);
|
||||
|
||||
// 素材前缀:空 = 走站点自己的 /static/images/themes(图片随二进制发布);
|
||||
// 填了 = 导出的 CSS 写绝对 URL,图片自己传对象存储(R2 / OSS)。
|
||||
// 只影响导出,不影响预览 —— 预览永远用内联 dataURL。
|
||||
const r3 = row();
|
||||
const baseIn = document.createElement('input');
|
||||
baseIn.type = 'url';
|
||||
baseIn.placeholder = '空 = /static/images/themes';
|
||||
baseIn.value = S.assetBase || '';
|
||||
baseIn.style.cssText = 'flex:1 1 0;width:0;min-width:0';
|
||||
baseIn.title = '填 https://cdn.example.com/themes 这种前缀(不要带 slug),导出的 CSS 就会写绝对地址;'
|
||||
+ '图片仍会打进 zip,方便你上传。留空则沿用站点静态目录';
|
||||
baseIn.onchange = () => {
|
||||
S.assetBase = baseIn.value.trim().replace(/\/+$/, '');
|
||||
renderAll();
|
||||
};
|
||||
// 标签固定宽度:flex 会把 4 个中文字的 label 压窄,末字被输入框切掉
|
||||
const baseLbl = label('素材前缀', '84px');
|
||||
baseLbl.style.cssText = 'flex:0 0 auto;white-space:nowrap';
|
||||
r3.append(baseLbl, baseIn);
|
||||
|
||||
const stat = document.createElement('p');
|
||||
stat.className = 'hint';
|
||||
const total = S.assets.reduce((n, a) => n + (a.kb || 0), 0);
|
||||
@@ -1563,6 +1590,15 @@ function renderExportEditor(){
|
||||
}
|
||||
el.appendChild(stat);
|
||||
|
||||
if (S.assetBase){
|
||||
const cdn = document.createElement('p');
|
||||
cdn.className = 'hint';
|
||||
cdn.textContent = `图片地址将写成 ${S.assetBase.replace(/\/+$/, '')}/${S.slug}/<id>.webp —— `
|
||||
+ '记得把 zip 里的 static/images/themes/' + S.slug + '/ 整个传到这个路径下;'
|
||||
+ '仓库里就不需要这些图了。换 CDN 域名要重新导出 CSS。';
|
||||
el.appendChild(cdn);
|
||||
}
|
||||
|
||||
const tip = document.createElement('p');
|
||||
tip.className = 'hint';
|
||||
tip.textContent = '压体积的顺序:先砍长边,再降质量。改完按「原图 + 处理参数」重新编码全部素材,'
|
||||
@@ -1688,6 +1724,7 @@ function toJSON(){
|
||||
dark: S.dark,
|
||||
narrow: S.narrow,
|
||||
export: S.export,
|
||||
assetBase: S.assetBase,
|
||||
// 图片内嵌 base64:少一层「图片放哪」的约定,导入时一个文件就够。
|
||||
// 存的是 orig(原始字节)+ fx(处理参数),dataURL 只在真有处理时才存 ——
|
||||
// 未处理时它必然等于 orig,再存一份等于让 json 平白翻倍。
|
||||
@@ -1717,6 +1754,7 @@ function fromJSON(o){
|
||||
S.narrow = Object.assign({ enabled: true, bg: null, hideDeco: false, maxWidth: 900,
|
||||
scale: MOB_SCALE_DEFAULT }, o.narrow || {});
|
||||
S.export = Object.assign({}, D.export, o.export || {});
|
||||
S.assetBase = typeof o.assetBase === 'string' ? o.assetBase : '';
|
||||
S.assets = (o.assets || []).map(a => {
|
||||
const orig = a.orig || { dataURL: a.dataURL, w: a.w, h: a.h };
|
||||
const dataURL = a.dataURL || orig.dataURL;
|
||||
@@ -1765,6 +1803,7 @@ function resetAll(){
|
||||
S.palette = Object.assign({}, D.palette);
|
||||
S.narrow = Object.assign({}, D.narrow);
|
||||
S.export = Object.assign({}, D.export);
|
||||
S.assetBase = '';
|
||||
S.dark = Object.assign({}, D.dark);
|
||||
S.scopes = {};
|
||||
SCOPES.forEach(sc => { S.scopes[sc.k] = blankScope(sc.k); });
|
||||
@@ -1929,9 +1968,17 @@ function installTxt(slug){
|
||||
return `主题 ${slug} —— 安装说明
|
||||
=====================================
|
||||
|
||||
1) 解压(保持目录结构,覆盖即可)
|
||||
${S.assetBase
|
||||
? `1) 解压
|
||||
static/css/themes/${slug}.css ← 放进仓库(CSS 里已是绝对地址)
|
||||
static/images/themes/${slug}/*.webp ← 传到 ${S.assetBase.replace(/\/+$/, '')}/${slug}/,仓库里不需要
|
||||
|
||||
素材托管在对象存储,CSS 写的是绝对地址:
|
||||
换 CDN 域名 / 路径 = 必须重新导出(地址烘焙在文件里,不是运行时拼的)
|
||||
换图 = 覆盖同名对象,建议把 Cache-Control 设成 immutable + 用 ?v= 或换文件名做失效`
|
||||
: `1) 解压(保持目录结构,覆盖即可)
|
||||
static/css/themes/${slug}.css
|
||||
static/images/themes/${slug}/*.webp
|
||||
static/images/themes/${slug}/*.webp`}
|
||||
|
||||
2) 登记主题(conf/app.ini)
|
||||
[theme]
|
||||
@@ -2745,6 +2792,11 @@ function boot(){
|
||||
S.narrow.hideDeco = true;
|
||||
renderAll();
|
||||
}
|
||||
// ?assetbase=https://cdn.example.com/themes 模拟「素材放对象存储」的导出结果
|
||||
if (params.get('assetbase')){
|
||||
S.assetBase = params.get('assetbase');
|
||||
renderAll();
|
||||
}
|
||||
// ?rows=N 加 N 层并依次绑到不同素材(截图 / 人工核对多图层效果用)
|
||||
const rows = parseInt(params.get('rows') || '0', 10);
|
||||
for (let i = 0; i < rows; i++){
|
||||
|
||||
Reference in New Issue
Block a user