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:
+56
-20
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user