import { fetchDb, buildMuLeaves } from "./lib/api.js";
import { meta, SITE_EDIT_STEPS, MODELS } from "./lib/meta.js";
+// `window` is not in Vue's template global allow-list, so referencing it inside a
+// template expression resolves to the (undefined) component scope. Expose it as a
+// setup binding instead.
+const win = window;
+
const notify = (m) => { message.info(m, 2.2); };
const confirm = (msg) => Promise.resolve(window.confirm(msg));
<small>领域分类</small>
<div v-for="d in domainOptions" :key="d.id" class="domain-nav-row" :class="page === 'domains' && domainFilter === d.id ? ' active' : ''">
<UiButton :class="page === 'domains' && domainFilter === d.id ? 'active' : ''" @click="() => { domainFilter = d.id; go('domains'); }"><b>◆</b><span class="domain-nav-name">{{ d.name }}</span></UiButton>
- <UiButton v-if="d.name !== '其他'" className="domain-nav-del" title="删除领域" @click="async () => { if (!window.confirm(`确定删除领域「${d.name}」?其下的方案将移入「其他」领域。`)) return; try { await fetchDb(`/domains/${d.id}`, { method: 'DELETE' }); notify('已删除领域,相关方案已归入「其他」'); await handleDomainDeleted(d.id); } catch (e) { notify('删除失败:' + e.message); } }">×</UiButton>
+ <UiButton v-if="d.name !== '其他'" className="domain-nav-del" title="删除领域" @click="async () => { if (!win.confirm(`确定删除领域「${d.name}」?其下的方案将移入「其他」领域。`)) return; try { await fetchDb(`/domains/${d.id}`, { method: 'DELETE' }); notify('已删除领域,相关方案已归入「其他」'); await handleDomainDeleted(d.id); } catch (e) { notify('删除失败:' + e.message); } }">×</UiButton>
</div>
<div v-if="addingDomain" class="domain-nav-row add">
<UiInput className="domain-nav-input" autoFocus :value="newDomainName" placeholder="领域名称" @change="(e) => (newDomainName = e.target.value)" @keydown="async (e) => { if (e.key === 'Enter') { await doAddDomain(); } else if (e.key === 'Escape') { addingDomain = false; newDomainName = ''; } }" />
</div>
<div>
<label v-if="page === 'siteEdit'" class="site-edit-switcher"><span>当前站点</span>
- <UiSelect :value="siteId ?? ''" @change="async (e) => { const id = Number(e.target.value); if (id === siteId) return; try { await saveCurrentConfiguration(); await loadSite(id); siteEditSection = 'basic'; } catch (error) { notify('切换失败:' + error.message); return; } window.history.replaceState({}, '', `?page=siteEdit&id=${id}`); }">
+ <UiSelect :value="siteId ?? ''" @change="async (e) => { const id = Number(e.target.value); if (id === siteId) return; try { await saveCurrentConfiguration(); await loadSite(id); siteEditSection = 'basic'; } catch (error) { notify('切换失败:' + error.message); return; } win.history.replaceState({}, '', `?page=siteEdit&id=${id}`); }">
<option v-for="item in sites" :key="item.id" :value="item.id">{{ item.name || '未命名站点' }}</option>
</UiSelect>
</label>
</UiOverlay>
<ModalComp v-if="modal" :type="modal" :close="() => (modal = '')" :notify="notify" :prompt-data="promptData" :feedback-cube="feedbackCube" :refine-cube="refineCube" :refining="refining" :refine-log="refineLog" />
<UiButton v-if="!aiOpen && !manualAssetActive && !domainArea" ref="fabRef" className="ai-fab" title="AI 助手 · 拖动可移动位置,双击复位" aria-label="AI 助手"
- :style="fabPos ? { left: fabPos.x + 'px', top: page === 'siteEdit' ? Math.min(fabPos.y, window.innerHeight - 132) + 'px' : fabPos.y + 'px', right: 'auto', bottom: 'auto' } : page === 'siteEdit' ? { bottom: '76px' } : undefined"
+ :style="fabPos ? { left: fabPos.x + 'px', top: page === 'siteEdit' ? Math.min(fabPos.y, win.innerHeight - 132) + 'px' : fabPos.y + 'px', right: 'auto', bottom: 'auto' } : page === 'siteEdit' ? { bottom: '76px' } : undefined"
@pointerdown="onFabPointerDown" @pointermove="onFabPointerMove" @pointerup="onFabPointerUp" @dblclick="resetFab">
<img src="/t/stationbuilder/ai-icon.png" alt="AI 助手" className="ai-fab-icon" draggable="false" />
</UiButton>
import App from "./App.vue";
-createApp(App).use(Antd).mount("#root");
+// Visible error overlay — surfaces runtime errors that would otherwise blank the page.
+function showFatalError(title, detail) {
+ try {
+ let box = document.getElementById("sb-fatal-error");
+ if (!box) {
+ box = document.createElement("div");
+ box.id = "sb-fatal-error";
+ box.setAttribute("style", "position:fixed;top:0;left:0;right:0;z-index:99999;background:#fff2f0;border-bottom:2px solid #ff4d4f;padding:12px 16px;font:13px/1.6 monospace;color:#a8071a;white-space:pre-wrap;word-break:break-all;max-height:60vh;overflow:auto;");
+ document.body && document.body.appendChild(box);
+ }
+ box.textContent += `[${title}] ${detail}\n\n`;
+ } catch (e) { /* ignore */ }
+}
+window.addEventListener("error", (e) => showFatalError("error", (e.message || "") + "\n" + (e.filename || "") + ":" + (e.lineno || "") + "\n" + (e.error && e.error.stack ? e.error.stack : "")));
+window.addEventListener("unhandledrejection", (e) => showFatalError("unhandledrejection", String(e.reason && e.reason.stack ? e.reason.stack : e.reason)));
+
+const app = createApp(App);
+app.config.errorHandler = (err, instance, info) => {
+ showFatalError("vue:" + info, String(err && err.stack ? err.stack : err));
+};
+app.use(Antd).mount("#root");