| |
| |
|
|
| 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) { |
| |
| 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); |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| 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; |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| 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 |
| ); |
| } |
|
|
| |
| 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); |
| } |
|
|