- 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)
<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";
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>
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([]);
: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>
}
// 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;
}
} 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() {
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(
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() {
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) =>
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
);
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,
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;
}
<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() {
}
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>
<script setup>
import { computed, useAttrs, defineOptions } from "vue";
-import { elements, classOf } from "./parse.js";
+import { elements, classOf, childContent } from "./parse.js";
defineOptions({ inheritAttrs: false });
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 || {},
}))
);
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) => {
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];
return s;
}
-export { isVNode, classOf, elements, textOf };
+export { isVNode, classOf, elements, textOf, childContent };