File size: 5,097 Bytes
8f8a793 300744a 823ca50 8f8a793 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | /* shared.js -- common runtime for all module pages:
top navigation, Pyodide bootstrap, editable-code runner, plot helpers. */
const NAV_ITEMS = [
["index.html", "导航 Home"],
["m00_overview.html", "0 水量平衡"],
["m01_forcing.html", "1 Forcing"],
["m02_pet.html", "2 PET"],
["m05_runoff.html", "5 产流"],
["m06_response.html", "6 Response"],
["m07_routing.html", "7 Routing·方法"],
["m08_channel.html", "8a Routing·空间"],
];
function renderNav(here) {
const nav = document.createElement("div");
nav.className = "topnav";
const inner = document.createElement("div");
inner.className = "wrap";
const brand = document.createElement("span");
brand.className = "brand";
brand.textContent = "💧 HydroModel Builder";
inner.appendChild(brand);
for (const [href, label] of NAV_ITEMS) {
// skip links to pages that don't exist yet (marked with data-disabled below)
const a = document.createElement("a");
a.href = href;
a.textContent = label;
if (here === href) a.className = "here";
inner.appendChild(a);
}
nav.appendChild(inner);
document.body.prepend(nav);
}
/* ---------- Pyodide bootstrap (one shared promise) ---------- */
let _pyodidePromise = null;
function getPyodide(statusEl) {
if (!_pyodidePromise) {
if (statusEl) statusEl.textContent = "正在加载 Python 运行时 (~10s 首次) …";
_pyodidePromise = loadPyodide().then(async (py) => {
await py.loadPackage("numpy");
if (statusEl) statusEl.textContent = "Python (numpy) 就绪 — 可改代码后重跑";
return py;
});
}
return _pyodidePromise;
}
/* ---------- Editable-code runner ----------
makeRunner({codeEl, runBtn, resetBtn, statusEl, errEl, buildScript, onResult})
- codeEl: <textarea> holding user-editable Python
- buildScript(userCode, params) -> full python source to exec; must set __out
- onResult(jsValue, params): update plots/readouts
- params(): collect current slider values */
function makeRunner(cfg) {
const defaultCode = cfg.codeEl.value;
async function run() {
const py = await getPyodide(cfg.statusEl);
if (cfg.errEl) cfg.errEl.innerHTML = "";
cfg.runBtn.disabled = true;
try {
const params = cfg.params ? cfg.params() : {};
const src = cfg.buildScript(cfg.codeEl.value, params);
const t0 = performance.now();
py.runPython(src);
const ms = performance.now() - t0;
const out = py.globals.get("__out");
const js = out.toJs({ dict_converter: Object.fromEntries });
if (out.destroy) out.destroy();
cfg.onResult(js, params, ms);
} catch (e) {
if (cfg.errEl)
cfg.errEl.innerHTML =
'<div class="err">运行出错 / error:\n' + (e && e.message ? e.message : e) + "</div>";
} finally {
cfg.runBtn.disabled = false;
}
}
cfg.runBtn.addEventListener("click", run);
if (cfg.resetBtn)
cfg.resetBtn.addEventListener("click", () => {
cfg.codeEl.value = defaultCode;
run();
});
if (cfg.autorun !== false) getPyodide(cfg.statusEl).then(run);
return run;
}
/* ---------- slider helper: live label + rerun on release ---------- */
function bindSlider(id, labelId, fmt, onchange) {
const el = document.getElementById(id);
const lab = document.getElementById(labelId);
const f = fmt || ((v) => v);
const upd = () => (lab.textContent = f(parseFloat(el.value)));
el.addEventListener("input", upd);
if (onchange) el.addEventListener("change", onchange);
upd();
return el;
}
/* ---------- plot helpers ---------- */
const PLOT_CFG = { responsive: true, displayModeBar: false };
function linePlot(div, traces, ylab, xlab, extra) {
Plotly.react(
div,
traces,
Object.assign(
{
margin: { t: 10, r: 10, b: 40, l: 55 },
legend: { orientation: "h", y: 1.13 },
xaxis: { title: xlab || "day" },
yaxis: { title: ylab || "" },
},
extra || {}
),
PLOT_CFG
);
}
/* version tabs: makeTabs(containerId, [{key,label,crest}], onSwitch) */
function makeTabs(containerId, versions, onSwitch) {
const box = document.getElementById(containerId);
box.className = "vtabs";
let current = versions[0].key;
for (const v of versions) {
const b = document.createElement("button");
b.innerHTML = v.label + (v.crest ? '<span class="crest-mini">CREST</span>' : "");
b.dataset.key = v.key;
if (v.key === current) b.className = "on";
b.addEventListener("click", () => {
current = v.key;
for (const c of box.children) c.className = c.dataset.key === current ? "on" : "";
onSwitch(current);
});
box.appendChild(b);
}
return () => current;
}
function footer() {
const f = document.createElement("div");
f.className = "foot";
f.innerHTML =
'Made with Claude Code · hydrologic-model-builder skill · data: TU Delft CIE4431 Hesperange · anchor: <a href="https://github.com/HyDROSLab/EF5">CREST/EF5</a>';
document.querySelector(".wrap:last-of-type")?.appendChild(f) || document.body.appendChild(f);
}
|