]> acesimba.cloud Git - StationBuilder.git/commitdiff
fix(vue): 修复配置站点纯白页与菜单交互失效
authorCodeBuddy <codebuddy@local>
Wed, 23 Sep 2026 02:29:33 +0000 (10:29 +0800)
committerCodeBuddy <codebuddy@local>
Wed, 23 Sep 2026 02:29:33 +0000 (10:29 +0800)
- App.vue: theme.getDesignToken 在 antd-vue 不存在, 改为 useToken() 注入 --ant-* 设计令牌, 否则整页无样式纯白
- Home.vue: 补 setLoading (React useState 残留未定义导致 onMounted 抛错数据不加载); SideMenu :on-click/:on-open-change 改为 @click/@open-change 事件绑定
- ui/parse.js: 新增 childContent() 处理 Vue vnode children 结构 (原直接读 props.children 为 React 写法, 导致 UiTable/Overlay/Actions 渲染 0 行)
- ui/UiTabbedRegion.vue: antd-vue Tabs 无 items 属性, 改用 TabPane 子节点
- lib/api.js: loadRenderer 改为加载 echarts.min.js + dashboard-renderer.js (原 cube-renderer.js 404)

src-vue/App.vue
src-vue/Home.vue
src-vue/lib/api.js
src-vue/ui/UiActions.vue
src-vue/ui/UiOverlay.vue
src-vue/ui/UiTabbedRegion.vue
src-vue/ui/UiTable.vue
src-vue/ui/parse.js

index a2fcaab9ef6b222dc4eaeff1530ef45ca39af70b..046d179f3d9b75793866664c0ae700874adb1fc7 100644 (file)
@@ -1,5 +1,5 @@
 <script setup>
-import { onMounted } from "vue";
+import { defineComponent, onMounted } from "vue";
 import { ConfigProvider, theme as antdTheme } from "ant-design-vue";
 import zhCN from "ant-design-vue/es/locale/zh_CN";
 import brandTheme from "./styles/theme.json";
@@ -17,25 +17,38 @@ const companyTheme = { ...brandTheme, cssVar: { key: "stationbuilder" } };
 const kebab = (name) =>
   name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
 
-onMounted(() => {
-  const resolved = antdTheme.getDesignToken(companyTheme);
-  const declarations = Object.entries(resolved)
-    .filter(([, value]) => typeof value === "string" || typeof value === "number")
-    .map(([name, value]) => "--ant-" + kebab(name) + ":" + tokenCssValue(name, value));
-  for (const [component, tokens] of Object.entries(brandTheme.components)) {
-    for (const [name, value] of Object.entries(tokens)) {
-      declarations.push("--ant-" + kebab(component) + "-" + kebab(name) + ":" + tokenCssValue(name, value));
-    }
-  }
-  const style = document.createElement("style");
-  style.setAttribute("data-station-brand-tokens", "");
-  style.textContent = ":root{" + declarations.join(";") + "}";
-  document.head.appendChild(style);
+// Vue port of React company-theme.tsx: ant-design-vue has no `theme.getDesignToken`
+// export, so resolve the merged design token via the useToken() composable from a
+// null-rendering child inside the ConfigProvider, then inject the `--ant-*`
+// variables that the app styles depend on (React renders an equivalent
+// <style data-station-brand-tokens> inside its ConfigProvider).
+const BrandTokenStyle = defineComponent({
+  name: "BrandTokenStyle",
+  setup() {
+    const { token } = antdTheme.useToken();
+    onMounted(() => {
+      const resolved = token.value || {};
+      const declarations = Object.entries(resolved)
+        .filter(([, value]) => typeof value === "string" || typeof value === "number")
+        .map(([name, value]) => "--ant-" + kebab(name) + ":" + tokenCssValue(name, value));
+      for (const [component, tokens] of Object.entries(brandTheme.components)) {
+        for (const [name, value] of Object.entries(tokens)) {
+          declarations.push("--ant-" + kebab(component) + "-" + kebab(name) + ":" + tokenCssValue(name, value));
+        }
+      }
+      const style = document.createElement("style");
+      style.setAttribute("data-station-brand-tokens", "");
+      style.textContent = ":root{" + declarations.join(";") + "}";
+      document.head.appendChild(style);
+    });
+    return () => null;
+  },
 });
 </script>
 
 <template>
   <ConfigProvider :theme="companyTheme" :locale="zhCN" :button="{ autoInsertSpace: false }">
+    <BrandTokenStyle />
     <Home />
   </ConfigProvider>
 </template>
index e400a6afaf74ab1c6c10bf2eb9889adea9bc33dc..9b6020ab1f764a90ddfb2a409e85ced5b5dbb680 100644 (file)
@@ -94,6 +94,7 @@ const sites = ref([]);
 const siteId = ref(null);
 const site = ref(null);
 const loading = ref(true);
+const setLoading = (v) => (loading.value = v);
 const saving = ref(false);
 const dirty = ref(false);
 const domains = ref([]);
@@ -513,8 +514,8 @@ function onMenuOpenChange(keys) { resourceNavOpen.value = keys.includes("resourc
             :collapsed="navCollapsed"
             :selected-keys="[domainArea ? 'domains' : projectArea ? 'projects' : page]"
             :open-keys="navCollapsed ? undefined : [...(resourceNavOpen ? ['resources'] : []), ...(systemNavOpen ? ['system'] : [])]"
-            :on-open-change="onMenuOpenChange"
-            :on-click="onMenuClick"
+            @open-change="onMenuOpenChange"
+            @click="onMenuClick"
             :items="menuItems"
           />
         </nav>
index 710c03aef39879531df01b01b18b77e3e48dbf97..9ae194a0ef5ce2dd4e7015c833c1908211a6ba34 100644 (file)
@@ -71,20 +71,26 @@ export function domainTagColor(name) {
 }
 
 // Cube renderer loader (used by DashboardThumb / DomainPageThumb).
+// Mirrors React page.tsx: echarts.min.js first, then dashboard-renderer.js,
+// resolving window.renderDashboard; resets on failure so it can be retried.
 let rendererPromise = null;
-export function loadRenderer() {
-  if (rendererPromise) return rendererPromise;
-  rendererPromise = new Promise((resolve, reject) => {
-    const w = window;
-    if (w.StationBuilderCubeRenderer) {
-      resolve(w.StationBuilderCubeRenderer);
-      return;
-    }
+function loadScript(src) {
+  return new Promise((resolve, reject) => {
     const s = document.createElement("script");
-    s.src = "/t/stationbuilder/cube-renderer.js";
-    s.onload = () => (w.StationBuilderCubeRenderer ? resolve(w.StationBuilderCubeRenderer) : reject(new Error("renderer missing")));
+    s.src = src;
+    s.onload = resolve;
     s.onerror = () => reject(new Error("renderer load failed"));
     document.head.appendChild(s);
   });
+}
+export function loadRenderer() {
+  if (rendererPromise) return rendererPromise;
+  rendererPromise = loadScript("/t/stationbuilder/echarts.min.js")
+    .then(() => loadScript("/t/stationbuilder/dashboard-renderer.js"))
+    .then(() => window.renderDashboard)
+    .catch((error) => {
+      rendererPromise = null;
+      throw error;
+    });
   return rendererPromise;
 }
index 38786d4eaefb9f12ad2a17079c97f31b5119b192..755dae60b8cba076b09a2735e514454995a939ac 100644 (file)
@@ -7,7 +7,7 @@ import {
 } from "ant-design-vue";
 import { EllipsisOutlined } from "@ant-design/icons-vue";
 import UiButton from "./UiButton.vue";
-import { elements, textOf, classOf } from "./parse.js";
+import { elements, textOf, classOf, childContent } from "./parse.js";
 
 export default {
   setup() {
@@ -78,7 +78,7 @@ export default {
               disabled: !!(a.props && a.props.disabled),
               onClick: (e) => triggerAction(a, e.domEvent || e),
             },
-            () => textOf(a.props ? a.props.children : a.children)
+            () => textOf(childContent(a))
           )
         );
         children.push(
index b1a30a7f516c380c5d9b5519ba9bcba1471dd31a..cb9f6af68027ddcde224096f678345192edfd50d 100644 (file)
@@ -2,7 +2,7 @@
 import { h, useSlots, useAttrs, Fragment } from "vue";
 import { Modal as AntModal } from "ant-design-vue";
 import UiActions from "./UiActions.vue";
-import { elements, textOf, classOf } from "./parse.js";
+import { elements, textOf, classOf, childContent } from "./parse.js";
 
 export default {
   setup() {
@@ -15,10 +15,10 @@ export default {
       const inner = all.find((c) => /\b(modal|dlg-box)\b/.test(classOf(c)));
       if (!inner) return null;
 
-      const innerChildren = elements(inner.props?.children);
+      const innerChildren = elements(childContent(inner));
       const header = innerChildren.find((p) => p.type === "header");
       const heading =
-        (header ? elements(header.props.children) : []).find((p) => p.type === "h2") ||
+        (header ? elements(childContent(header)) : []).find((p) => p.type === "h2") ||
         innerChildren.find((p) => p.type === "h2");
       const footer = innerChildren.find(
         (p) =>
@@ -44,15 +44,15 @@ export default {
       else if (attrs.onClick) close = attrs.onClick;
       else {
         const hdrBtn = header
-          ? elements(header.props.children).find(
+          ? elements(childContent(header)).find(
               (p) => p.props && p.props.onClick && !p.props.disabled
             )
           : null;
         if (hdrBtn) close = hdrBtn.props.onClick;
         else if (footer) {
-          const cancel = elements(footer.props.children).find(
+          const cancel = elements(childContent(footer)).find(
             (p) =>
-              textOf(p.props ? p.props.children : p.children).includes("取消") &&
+              textOf(childContent(p)).includes("取消") &&
               p.props &&
               !p.props.disabled
           );
@@ -72,7 +72,7 @@ export default {
       const modalProps = {
         open: true,
         centered: true,
-        title: heading ? heading.props.children : undefined,
+        title: heading ? childContent(heading) : undefined,
         width,
         class: name + " station-ui-modal",
         closable: !!close,
@@ -84,7 +84,7 @@ export default {
       const modalSlots = { default: () => body };
       if (footer) {
         modalSlots.footer = () =>
-          h(UiActions, { class: "station-modal-actions" }, () => footer.props.children);
+          h(UiActions, { class: "station-modal-actions" }, () => childContent(footer));
       } else {
         modalProps.footer = null;
       }
index 79c503ac95392d036971a73235e2935222783f0f..5cb3c0205d6e3725c0c438c79344a44102edb3db 100644 (file)
@@ -1,7 +1,7 @@
 <script>
 import { computed, h, useSlots } from "vue";
-import { Tabs as AntTabs } from "ant-design-vue";
-import { elements, classOf } from "./parse.js";
+import { Tabs as AntTabs, TabPane } from "ant-design-vue";
+import { elements, classOf, childContent } from "./parse.js";
 
 export default {
   setup() {
@@ -37,16 +37,25 @@ export default {
     }
 
     return () =>
-      h(AntTabs, {
-        class: navClass.value + " station-ui-tabs",
-        activeKey: activeKey.value,
-        onChange,
-        items: buttons.value.map((button, index) => ({
-          key: String(button.key ?? index),
-          label: button.props?.children,
-          children: () => (slots.default ? slots.default() : null),
-        })),
-      });
+      h(
+        AntTabs,
+        {
+          class: navClass.value + " station-ui-tabs",
+          activeKey: activeKey.value,
+          onChange,
+        },
+        () =>
+          buttons.value.map((button, index) =>
+            h(
+              TabPane,
+              {
+                key: String(button.key ?? index),
+                tab: childContent(button),
+              },
+              { default: () => (slots.default ? slots.default() : null) }
+            )
+          )
+      );
   },
 };
 </script>
index fcdb491466bcb2a6754ec215ac0de84280a768ed..b5bf2c9d1ba4c295f705f69f3f994be2075fa902 100644 (file)
@@ -1,6 +1,6 @@
 <script setup>
 import { computed, useAttrs, defineOptions } from "vue";
-import { elements, classOf } from "./parse.js";
+import { elements, classOf, childContent } from "./parse.js";
 
 defineOptions({ inheritAttrs: false });
 
@@ -20,19 +20,19 @@ const body = computed(() =>
   sections.value.find((s) => s.type === "tbody")
 );
 const headerRow = computed(() => {
-  const rows = elements(head.value?.props?.children);
+  const rows = elements(head.value?.children);
   return rows[0];
 });
-const headers = computed(() => elements(headerRow.value?.props?.children));
+const headers = computed(() => elements(headerRow.value?.children));
 const cols = computed(() => {
   const cg = sections.value.find((s) => s.type === "colgroup");
-  return elements(cg?.props?.children);
+  return elements(cg?.children);
 });
 
 const data = computed(() =>
-  elements(body.value?.props?.children).map((row, index) => ({
-    key: String(row.props?.key ?? index),
-    cells: elements(row.props?.children),
+  elements(body.value?.children).map((row, index) => ({
+    key: String(row.key ?? index),
+    cells: elements(row.children),
     props: row.props || {},
   }))
 );
@@ -49,11 +49,11 @@ const columns = computed(() =>
       hprops.style?.width;
     return {
       key: String(index),
-      title: hprops.children,
+      title: childContent(header),
       width,
       customRender: ({ index: ri }) => {
         const cell = data.value[ri]?.cells[index];
-        return cell?.props?.children;
+        return cell ? childContent(cell) : undefined;
       },
       onHeaderCell: () => hrest,
       onCell: (record) => {
index 0495c412507c2799924f51cb0f86bf6e01d83911..6487c84ed6a45d286f025f94b783c48654e54fb2 100644 (file)
@@ -33,6 +33,18 @@ function elements(children) {
   return out;
 }
 
+// Vue 3 vnodes carry children at `vnode.children`; component vnodes carry a
+// slots object there ({ default: fn }). Normalise both to renderable content.
+// (React's `props.children` does not exist in Vue vnodes.)
+function childContent(vnode) {
+  if (!vnode) return undefined;
+  const c = vnode.children;
+  if (c && typeof c === "object" && !Array.isArray(c) && typeof c.default === "function") {
+    return c.default();
+  }
+  return c;
+}
+
 function textOf(children) {
   if (children == null) return "";
   const arr = Array.isArray(children) ? children : [children];
@@ -54,4 +66,4 @@ function textOf(children) {
   return s;
 }
 
-export { isVNode, classOf, elements, textOf };
+export { isVNode, classOf, elements, textOf, childContent };