v0.4.2: host preview icons on OSS for slim Pages deploys.

Add STATIC_ASSET_BASE so Cloudflare Pages ships ~2MB HTML/JS/data; icons served from climperor OSS via _oss_static_assets.py. New machines can deploy without local fetch scripts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
voson
2026-07-27 21:28:01 +08:00
co-authored by Cursor
parent 74deb17888
commit 9898dd39fb
9 changed files with 384 additions and 93 deletions
+56 -20
View File
@@ -1,10 +1,12 @@
/* global fetch, document, ABILITY_VIDEO_BASE, SITE_VERSION */
/* global fetch, document, ABILITY_VIDEO_BASE, STATIC_ASSET_BASE, SITE_VERSION */
/** OSS / CDN base for ability demos; empty => same-origin `/ability-video/...`. */
function trimBase(raw) {
return typeof raw === "string" ? raw.trim().replace(/\/+$/, "") : "";
}
/** OSS base for ability demos; empty => same-origin `/ability-video/...`. */
function abilityVideoBase() {
const raw =
typeof ABILITY_VIDEO_BASE === "string" ? ABILITY_VIDEO_BASE.trim() : "";
return raw.replace(/\/+$/, "");
return trimBase(ABILITY_VIDEO_BASE);
}
function abilityVideoUrl(heroKey, abilityKey) {
@@ -13,6 +15,41 @@ function abilityVideoUrl(heroKey, abilityKey) {
return base ? `${base}${rel}` : rel;
}
/** OSS base for icons/portraits; empty => same-origin relative paths. */
function staticAssetBase() {
return trimBase(STATIC_ASSET_BASE);
}
function assetUrl(rel) {
const clean = String(rel || "").replace(/^\//, "");
const base = staticAssetBase();
return base ? `${base}/${clean}` : clean;
}
function attrIconSrc(key) {
return assetUrl(`attr/${key}.png`);
}
function portraitSrc(key) {
return assetUrl(`portrait/${encodeURIComponent(key)}.png?v=wide`);
}
function abilityIconSrc(abilityKey) {
return assetUrl(`ability/${encodeURIComponent(abilityKey)}.png`);
}
function innateIconSrc() {
return assetUrl("ability/innate.png");
}
function talentTreeIconSrc() {
return assetUrl("ability/talent_tree.png");
}
function itemCatIconSrc(icon) {
return assetUrl(`item-cat/${encodeURIComponent(icon)}`);
}
function siteVersionLabel() {
const v = typeof SITE_VERSION === "string" ? SITE_VERSION.trim() : "";
return v ? `v${v}` : "";
@@ -104,7 +141,7 @@ function renderBoard() {
for (const attr of attr_order) {
const col = document.createElement("section");
col.className = "col";
col.innerHTML = `<div class="col-head"><img class="attr-icon" src="attr/${attr}.png" alt="${attr_labels[attr]}" /><span>${attr_labels[attr]}</span></div>`;
col.innerHTML = `<div class="col-head"><img class="attr-icon" src="${attrIconSrc(attr)}" alt="${attr_labels[attr]}" /><span>${attr_labels[attr]}</span></div>`;
const grid = document.createElement("div");
grid.className = "grid";
@@ -157,7 +194,7 @@ function renderBoard() {
}
const img = document.createElement("img");
img.src = `portrait/${h.key}.png?v=wide`;
img.src = portraitSrc(h.key);
img.alt = h.name_loc;
img.loading = "lazy";
btn.appendChild(img);
@@ -243,9 +280,6 @@ function heroAbilityCell(heroKey) {
};
}
const INNATE_ICON_SRC = "ability/innate.png";
const TALENT_TREE_ICON_SRC = "ability/talent_tree.png";
/** Floating talent popover (body-level; avoids #detail overflow clipping). */
let talentPopoverEl = null;
let talentPopoverHideTimer = 0;
@@ -391,11 +425,11 @@ function wireTalentTrigger(btn, talents) {
*/
function setAbilityIcon(img, abilityKey, isInnate) {
if (isInnate) {
img.src = INNATE_ICON_SRC;
img.src = innateIconSrc();
img.onerror = null;
return;
}
img.src = `ability/${encodeURIComponent(abilityKey)}.png`;
img.src = abilityIconSrc(abilityKey);
img.onerror = () => {
img.onerror = null;
img.removeAttribute("src");
@@ -618,7 +652,7 @@ function buildSkillsPanel(heroKey) {
talentBtn.setAttribute("aria-haspopup", "dialog");
talentBtn.title = "天赋树";
const talentImg = document.createElement("img");
talentImg.src = TALENT_TREE_ICON_SRC;
talentImg.src = talentTreeIconSrc();
talentImg.alt = "天赋树";
talentImg.loading = "lazy";
talentBtn.appendChild(talentImg);
@@ -836,7 +870,7 @@ function mountSkillInspect(root, skillEnt, heroKey) {
cd.className = "skill-stat cooldown";
const ico = document.createElement("img");
ico.className = "skill-stat-icon";
ico.src = "ui-icon/cooldown.png";
ico.src = assetUrl("ui-icon/cooldown.png");
ico.alt = "";
const txt = document.createElement("span");
txt.className = "skill-stat-text";
@@ -1150,7 +1184,7 @@ function buildHeroVitalsAttrs(hero) {
(primary === a.key || primary === "all" ? " primary" : "");
cell.setAttribute("title", a.label);
cell.innerHTML =
`<img class="attr-icon" src="attr/${a.key}.png" alt="${a.label}" />` +
`<img class="attr-icon" src="${attrIconSrc(a.key)}" alt="${a.label}" />` +
`<span class="hero-stat-attr-base">${a.base}</span>` +
`<span class="hero-stat-attr-gain">${fmtGain(a.gain)}</span>`;
attrs.appendChild(cell);
@@ -1447,7 +1481,7 @@ function appendShopColumn(parent, sec) {
if (sec.icon) {
const cat = document.createElement("img");
cat.className = "shop-column-cat";
cat.src = `item-cat/${encodeURIComponent(sec.icon)}`;
cat.src = itemCatIconSrc(sec.icon);
cat.alt = sec.label || "";
cat.title = sec.label || "";
col.appendChild(cat);
@@ -1493,7 +1527,7 @@ function renderItemShop() {
function itemIconSrc(key) {
const stem = key.startsWith("recipe_") ? "recipe" : key;
return `item/${encodeURIComponent(stem)}.png`;
return assetUrl(`item/${encodeURIComponent(stem)}.png`);
}
function makeCraftChip(key, { clickable = true, onPick = null } = {}) {
@@ -1594,7 +1628,7 @@ const STAT_ATTR_ICON = {
function statChip(icon) {
if (!icon) return "";
const a = STAT_ATTR_ICON[icon];
if (a) return `<img class="stat-icon" src="attr/${a}.png" alt="" onerror="this.style.visibility='hidden'">`;
if (a) return `<img class="stat-icon" src="${attrIconSrc(a)}" alt="" onerror="this.style.visibility='hidden'">`;
const label = STAT_LABELS[icon] || icon;
return `<span class="stat-chip">${escapeHtml(label)}</span>`;
}
@@ -1655,7 +1689,7 @@ function patchAbilityBlockHtml(ab, lookup) {
const aname = ameta ? ameta.name_loc : "# " + ab.ability_id;
const innate = akey && innateAbilityKeys().has(akey);
const aicon = akey
? `<img class="patch-ability-icon" src="${innate ? INNATE_ICON_SRC : "ability/" + encodeURIComponent(akey) + ".png"}" alt="" onerror="this.style.visibility='hidden'">`
? `<img class="patch-ability-icon" src="${innate ? innateIconSrc() : abilityIconSrc(akey)}" alt="" onerror="this.style.visibility='hidden'">`
: "";
return `<div class="patch-ability">${aicon}<div class="patch-entry-body"><span class="patch-name">${escapeHtml(aname)}</span>${renderNotesList(ab.ability_notes)}</div></div>`;
}
@@ -1675,7 +1709,7 @@ function renderHeroEntry(hero, lookup) {
const key = meta ? meta.key : "";
const name = meta ? meta.name_loc : "其它单位";
const portrait = key
? `<img class="patch-hero-icon" src="portrait/${encodeURIComponent(key)}.png?v=wide" alt="" onerror="this.style.visibility='hidden'">`
? `<img class="patch-hero-icon" src="${portraitSrc(key)}" alt="" onerror="this.style.visibility='hidden'">`
: "";
let html = `<div class="patch-hero"><div class="patch-hero-head">${portrait}<span class="patch-name">${escapeHtml(name)}</span></div>`;
html += renderHeroNotes(hero.hero_notes);
@@ -2032,6 +2066,8 @@ async function main() {
);
bindSearch();
bindPageChrome();
const brandLogo = document.querySelector(".brand-logo");
if (brandLogo) brandLogo.src = assetUrl("ui-icon/dota2_logo.png");
const res = await fetch("data.json");
if (!res.ok) throw new Error(`data.json: HTTP ${res.status}`);
state.data = await res.json();
+2 -1
View File
@@ -1,3 +1,4 @@
/* Local defaults; production export overwrites via export_relations_site.py. */
var SITE_VERSION = "0.4.1";
var SITE_VERSION = "0.4.2";
var ABILITY_VIDEO_BASE = "";
var STATIC_ASSET_BASE = "";
+3 -3
View File
@@ -70,8 +70,8 @@
<section class="detail" id="detail" aria-live="polite"></section>
<script src="/config.js?v=0.4.1"></script>
<script src="/router.js?v=0.4.1"></script>
<script src="/app.js?v=0.4.1"></script>
<script src="/config.js?v=0.4.2"></script>
<script src="/router.js?v=0.4.2"></script>
<script src="/app.js?v=0.4.2"></script>
</body>
</html>