qinyan 1 年間 前
コミット
5819d198a0

+ 45 - 0
src/modules/resource/actions/editor.ts

@@ -0,0 +1,45 @@
+import { queenApi } from "queenjs";
+import { ResourceModule } from "..";
+import { cloneDeep } from "lodash";
+
+export const editorActions = ResourceModule.action({
+  async queryTplsDetail(id) {
+    const res = await this.https.queryTplsDetail(id);
+    this.store.setSourceDetail(res.result);
+    this.actions.initTreeData();
+  },
+
+  initTreeData() {
+    const data: any = [];
+    this.store.sourceDetail.webEditor?.meshSlots?.forEach((mesh: any) => {
+      mesh.children = this.store.sourceDetail.webEditor?.matSlots?.filter(
+        (mat: any) => mat.meshSlotId == mesh.Id
+      );
+      data.push(cloneDeep(mesh));
+    });
+    this.store.setTreeData(data);
+  },
+
+  async submitRender(id: string, images: any, vidoes: any) {
+    queenApi.showLoading("任务提交中");
+
+    console.log("iamges=>", images, "videos->", vidoes);
+
+    try {
+      await this.https.sourceGen({
+        genRequest: {
+          templateId: id,
+          vidoes: vidoes,
+          images: images,
+          matSlots: this.store.matSlots,
+        },
+      });
+      queenApi.messageSuccess("任务提交成功");
+      return true;
+    } catch (error) {
+      console.error(error);
+    } finally {
+      queenApi.hideLoading();
+    }
+  },
+});

+ 2 - 1
src/modules/resource/actions/index.ts

@@ -1,4 +1,5 @@
+import { editorActions } from "./editor";
 import { materialActions } from "./material";
 import { promotionAction } from "./promotion";
 
-export const actions = [materialActions, promotionAction];
+export const actions = [editorActions, materialActions, promotionAction];

+ 0 - 28
src/modules/resource/actions/material.ts

@@ -47,32 +47,4 @@ export const materialActions = ResourceModule.action({
     const url = `${location.origin}/index.html#/create/${record._id}`;
     location.href = url;
   },
-
-  async queryTplsDetail(id) {
-    const res = await this.https.queryTplsDetail(id);
-    this.store.setSourceDetail(res.result);
-    this.store.setSelectedId(res.result.webEditor?.meshSlots[0]?.Id);
-  },
-  async submitRender(id: string, images: any, vidoes: any) {
-    queenApi.showLoading("任务提交中");
-
-    console.log("iamges=>", images, "videos->", vidoes);
-
-    try {
-      await this.https.sourceGen({
-        genRequest: {
-          templateId: id,
-          vidoes: vidoes,
-          images: images,
-          matSlots: this.store.matSlots,
-        },
-      });
-      queenApi.messageSuccess("任务提交成功");
-      return true;
-    } catch (error) {
-      console.error(error);
-    } finally {
-      queenApi.hideLoading();
-    }
-  },
 });

+ 35 - 10
src/modules/resource/store.ts

@@ -1,26 +1,48 @@
 import { ResourceModule } from ".";
 
+export type ActiveKeys = {
+  type: string;
+  id?: string;
+  mId?: string;
+};
+
 export const store = ResourceModule.store({
   state: () => ({
     type: "video",
-    selectedId: "",
     sourceDetail: {
       webEditor: { pack: {} },
     } as any,
     matSlots: [] as any[],
+    meshSlots: [] as any[],
+    activeKeys: { type: "" } as ActiveKeys,
+    treeData: [],
   }),
-  
   getters: {
     currentMesh(state) {
-      return state.sourceDetail.webEditor?.meshSlots?.find(
-        (e: any) => e.Id == state.selectedId
+      return (
+        state.treeData.find(
+          (e: any) =>
+            e.Id ==
+            (state.activeKeys.type == "mesh"
+              ? state.activeKeys.id
+              : state.activeKeys.mId)
+        ) || {}
       );
     },
-    currentMats(state) {
-      return state.sourceDetail.webEditor?.matSlots?.filter(
-        (e: any) => e.meshSlotId == state.selectedId
-      );
+    currentMat(state) {
+      return {};
     },
+    currentSlot(state) {
+      if (state.activeKeys.type == "mesh")
+        return state.meshSlots.find((d) => d.id == state.activeKeys.id);
+      else if (state.activeKeys.type == "mat")
+        return state.matSlots.find((d) => d.id == state.activeKeys.id);
+    },
+    // currentMat(state) {
+    //   return this.store.currentMesh?.children?.find(
+    //     (e: any) => e.id == state.activeKeys.id
+    //   );
+    // },
   },
   actions: {
     setSourceType(v: string) {
@@ -29,8 +51,11 @@ export const store = ResourceModule.store({
     setSourceDetail(data) {
       this.store.sourceDetail = data;
     },
-    setSelectedId(id: string) {
-      this.store.selectedId = id;
+    setActiveKey(data: ActiveKeys) {
+      this.store.activeKeys = data;
+    },
+    setTreeData(data) {
+      this.store.treeData = data;
     },
   },
 });

+ 7 - 7
src/pages/website/CreateMat/components/AttrPanel.tsx → src/pages/website/CreateMat/components/AttrPanel/MatAttr.tsx

@@ -1,19 +1,19 @@
 import { useResource } from "@/modules/resource";
 import { useQueditor } from "@queenjs-modules/queditor";
-import { defineComponent } from "vue";
+import { defineUI } from "queenjs";
 
-export default defineComponent({
+export default defineUI({
   setup() {
     const resource = useResource();
     const queditor = useQueditor();
 
     queditor.actions.on("updateCurrPackFormData:success", () => {
-      // queditor.store.currActiveMat
-      console.log(
-        "queditor.store.currActiveMat: ",
-        queditor.store.currActiveMat
+      const matConf = resource.store.matSlots.find(
+        (item) => item.id == resource.store.activeKeys.id
       );
-      console.log("resource: ", resource.store);
+      if (matConf) {
+        matConf.material = queditor.store.currActiveMat;
+      }
     });
 
     return () => {

+ 59 - 0
src/pages/website/CreateMat/components/AttrPanel/MeshAttr.tsx

@@ -0,0 +1,59 @@
+import { useResource } from "@/modules/resource";
+import { useQueditor } from "@queenjs-modules/queditor";
+import FormUI, { ColumnItem } from "@queenjs/components/FormUI";
+import { set } from "lodash";
+import { defineUI } from "queenjs";
+
+const bgColumns: ColumnItem[] = [
+  {
+    label: "缩放",
+    dataIndex: "background.image",
+    component: "Slider",
+    props: {
+      max: 2,
+      min: 0.1,
+    },
+  },
+  {
+    label: "位置",
+    dataIndex: "background.image",
+    component: "Slider",
+    props: {
+      max: 2,
+      min: 0.1,
+    },
+  },
+  {
+    label: "旋转",
+    dataIndex: "background.image",
+    component: "Slider",
+    props: {
+      max: 2,
+      min: 0.1,
+    },
+  },
+];
+
+export default defineUI({
+  setup() {
+    const resource = useResource();
+    const queditor = useQueditor();
+
+    function changeVal(e: { dataIndex: string; value: any }) {
+      set({}, e.dataIndex, e.value);
+    }
+
+    return () => {
+      return (
+        <div class="w-300px flex flex-col">
+          <div class="p-15px text-16px text-white border-dark-800 border-0 border-b-1 border-solid">
+            属性编辑
+          </div>
+          <div class="py-15px flex-1 overflow-y-auto scrollbar">
+            <FormUI data={{}} columns={bgColumns} onChange={changeVal} />
+          </div>
+        </div>
+      );
+    };
+  },
+});

+ 33 - 0
src/pages/website/CreateMat/components/AttrPanel/index.tsx

@@ -0,0 +1,33 @@
+import { useResource } from "@/modules/resource";
+import { useQueditor } from "@queenjs-modules/queditor";
+import { Empty } from "ant-design-vue";
+import { defineUI } from "queenjs";
+import MatAttr from "./MatAttr";
+import { upperFirst } from "lodash";
+import MeshAttr from "./MeshAttr";
+
+export default defineUI({
+  slots: {
+    MatAttr,
+    MeshAttr,
+  },
+  setup(props, { slots }) {
+    const resource = useResource();
+    const queditor = useQueditor();
+
+    return () => {
+      console.error(resource.store.currentSlot);
+      const { activeKeys } = resource.store;
+      if (activeKeys.type == "")
+        return <Empty class="pt-80px" description="暂无数据" />;
+      else {
+        const CurrComp = (slots as any)[`${upperFirst(activeKeys.type)}Attr`];
+        return (
+          <CurrComp
+          // data={resource.store.currentSlot}
+          />
+        );
+      }
+    };
+  },
+});

+ 0 - 229
src/pages/website/CreateMat/components/LeftPanel.tsx

@@ -1,229 +0,0 @@
-import { useResource } from "@/modules/resource";
-import { cx } from "@linaria/core";
-import { useQueditor } from "@queenjs-modules/queditor";
-import { switchSceneProdComp } from "@queenjs-modules/queditor/module/controls/Queen3dCtrl/actions/geom";
-import { Pack, PackMat } from "@queenjs-modules/queditor/objects";
-import { AssetItemFile } from "@queenjs-modules/queentree-explorer/objects/fileSystem/assetFiles";
-import { Image, List } from "@queenjs/ui";
-import { Button } from "ant-design-vue";
-import { computed, defineComponent, reactive } from "vue";
-import { any, bool } from "vue-types";
-import LibraryModal from "./LibraryModal";
-import { IQueentree } from "@queenjs-modules/queentree";
-
-export default defineComponent({
-  setup() {
-    const queditor = useQueditor();
-    const resource = useResource();
-    const { store } = resource;
-
-    const state = reactive({
-      treeData: computed(() => {
-        // const data: any = [];
-        store.sourceDetail.webEditor?.meshSlots?.forEach((mesh: any) => {
-          console.error("mesh: ", mesh);
-          mesh.children = store.sourceDetail.webEditor?.matSlots?.filter(
-            (mat: any) => mat.meshSlotId == mesh.Id
-          );
-        });
-
-        return store.sourceDetail.webEditor?.meshSlots;
-      }),
-    });
-
-    const replaceMesh = async () => {
-      const branchFolder = await resource.showModal<AssetItemFile>(
-        <LibraryModal />,
-        { width: "900px" }
-      );
-
-      const pack = await branchFolder.getAssetDetail();
-      const data = queditor.helper.getRelatedSourceByProduct(
-        pack.source as Pack["source"],
-        branchFolder.state.id
-      );
-
-      queditor.actions.insertMesh(data);
-    };
-
-    const replaceMat = async (record: any) => {
-      // console.error("replace=>", record);
-      const branchFolder = await resource.showModal<AssetItemFile>(
-        <LibraryModal nodeTypes={["matGroupItem", "mat", "packMat"]} />,
-        { width: "900px" }
-      );
-      // console.error("branchFolder: ", branchFolder);
-      const data = await branchFolder.getAssetDetail();
-      // console.error("data: ", data);
-
-      let mat;
-      switch (branchFolder.nodeType) {
-        case "matGroupItem":
-          mat = (data as IQueentree.IAssetMatGroup).source.colorCards.find(
-            (d: PackMat) => d.id == branchFolder.state.id
-          );
-          break;
-        case "packMat":
-          mat = (data.source as Pack["source"]).mats.find(
-            (d: PackMat) => d.id == branchFolder.state.id
-          );
-          break;
-        case "mat":
-          mat = data.source as PackMat;
-          break;
-      }
-
-      if (data && mat) {
-        const matConf = resource.store.matSlots.find(
-          (item) => item.id == record.id
-        );
-        if (matConf) {
-          matConf.material = mat;
-        } else {
-          const matSlot = { id: record.id, material: mat };
-          resource.store.matSlots.push(matSlot);
-        }
-      }
-      // console.error("resource.store.matSlots: ", resource.store.matSlots);
-
-      const targetComp = getTargetProCom();
-      if (!targetComp || !mat) return;
-      queditor.actions.updatePackProductCompMat(targetComp, mat);
-    };
-
-    const getTargetProCom = () => {
-      const source: Pack["source"] = queditor.store.pack;
-      const targetPro = source.products[0];
-      const targetComp = targetPro?.components.find(
-        (c) => c.name == resource.store.currentMesh.meshName
-      );
-      return targetComp;
-    };
-
-    return () => {
-      const Meshlist = state.treeData || [];
-      // console.error("state.treeData", state.treeData);
-
-      return (
-        <div class="w-300px flex flex-col">
-          <div class="p-15px text-16px text-white border-dark-800 border-0 border-b-1 border-solid">
-            模型列表
-          </div>
-
-          <List
-            data={Meshlist}
-            gap="10px"
-            class="scrollbar flex-1 py-15px px-15px"
-          >
-            {{
-              item: (record: any) => (
-                <div>
-                  <ProductItem
-                    record={record}
-                    onChange={replaceMesh}
-                    active={record.Id == resource.store.selectedId}
-                    onClick={() => {
-                      switchSceneProdComp.call(
-                        queditor.controls.queen3dCtrl,
-                        queditor.store.pack.scenes[0].products[0].id,
-                        record.meshName
-                      );
-                      resource.store.setSelectedId(record.Id);
-                    }}
-                  />
-                  <List
-                    gap="10px"
-                    class="py-15px pl-15px"
-                    data={record.children}
-                  >
-                    {{
-                      item: (item: any) => (
-                        <CompItem
-                          record={item}
-                          onClick={() => replaceMat(item)}
-                        />
-                      ),
-                    }}
-                  </List>
-                </div>
-              ),
-              loadmore: () => (
-                <div class="text-center py-20px text-12px opacity-80">
-                  没有更多了
-                </div>
-              ),
-            }}
-          </List>
-        </div>
-      );
-    };
-  },
-});
-
-const ProductItem = defineComponent({
-  props: {
-    record: any().isRequired,
-    active: bool().def(false),
-  },
-  emits: ["change", "click"],
-  setup(props, { emit }) {
-    return () => {
-      const { active, record } = props;
-      return (
-        <div
-          style={{ backgroundColor: "#303030" }}
-          class={cx(
-            "flex items-center py-6px px-12px rounded-4px border-1 border-solid cursor-pointer hover:opacity-80 transition-all",
-            active ? "border-orange-200" : "border-transparent"
-          )}
-          onClick={() => emit("click", record)}
-        >
-          <Image src={record.thumbnail} class="w-48px rounded-4px" />
-          <div class="ml-10px flex-1 mr-5px truncate w-0">
-            {record.meshName || "未命名"}
-          </div>
-          <Button
-            type="primary"
-            ghost
-            size="small"
-            onClick={(e) => {
-              e.stopPropagation();
-              emit("change", record);
-            }}
-          >
-            替换
-          </Button>
-        </div>
-      );
-    };
-  },
-});
-
-const CompItem = defineComponent({
-  props: {
-    record: any().isRequired,
-  },
-  emits: ["change", "click"],
-  setup(props, { emit }) {
-    return () => {
-      const { record } = props;
-      return (
-        <div
-          class="flex items-center py-6px px-12px rounded-4px"
-          style={{ backgroundColor: "#303030" }}
-        >
-          <Image src={record.thumbnail} class="w-48px rounded-4px" />
-          <div class="ml-10px flex-1 mr-5px w-0">{record.name || "未命名"}</div>
-          <Button
-            ghost
-            type="primary"
-            size="small"
-            onClick={() => emit("click", record)}
-          >
-            替换
-          </Button>
-        </div>
-      );
-    };
-  },
-});

+ 42 - 0
src/pages/website/CreateMat/components/TreePanel/MatItem.tsx

@@ -0,0 +1,42 @@
+import { cx } from "@linaria/core";
+import { Image } from "@queenjs/ui";
+import { Button } from "ant-design-vue";
+import { defineComponent } from "vue";
+import { any, bool } from "vue-types";
+
+export default defineComponent({
+  props: {
+    record: any().isRequired,
+    active: bool().def(false),
+  },
+  emits: ["replace", "click"],
+  setup(props, { emit }) {
+    return () => {
+      const { active, record } = props;
+      return (
+        <div
+          class={cx(
+            "flex items-center py-6px px-12px rounded-4px cursor-pointer border-1 border-solid hover:opacity-80 transition-all",
+            active ? "border-orange-200" : "border-transparent"
+          )}
+          style={{ backgroundColor: "#303030" }}
+          onClick={() => emit("click", record)}
+        >
+          <Image src={record.thumbnail} class="w-48px rounded-4px" />
+          <div class="ml-10px flex-1 mr-5px w-0">{record.name || "未命名"}</div>
+          <Button
+            ghost
+            type="primary"
+            size="small"
+            onClick={(e) => {
+              e.stopPropagation();
+              emit("replace", record);
+            }}
+          >
+            替换
+          </Button>
+        </div>
+      );
+    };
+  },
+});

+ 44 - 0
src/pages/website/CreateMat/components/TreePanel/MeshItem.tsx

@@ -0,0 +1,44 @@
+import { cx } from "@linaria/core";
+import { Image } from "@queenjs/ui";
+import { Button } from "ant-design-vue";
+import { defineComponent } from "vue";
+import { any, bool } from "vue-types";
+
+export default defineComponent({
+  props: {
+    record: any().isRequired,
+    active: bool().def(false),
+  },
+  emits: ["replace", "click"],
+  setup(props, { emit }) {
+    return () => {
+      const { active, record } = props;
+      return (
+        <div
+          style={{ backgroundColor: "#303030" }}
+          class={cx(
+            "flex items-center py-6px px-12px rounded-4px border-1 border-solid cursor-pointer hover:opacity-80 transition-all",
+            active ? "border-orange-200" : "border-transparent"
+          )}
+          onClick={() => emit("click", record)}
+        >
+          <Image src={record.thumbnail} class="w-48px rounded-4px" />
+          <div class="ml-10px flex-1 mr-5px truncate w-0">
+            {record.meshName || "未命名"}
+          </div>
+          <Button
+            type="primary"
+            ghost
+            size="small"
+            onClick={(e) => {
+              e.stopPropagation();
+              emit("replace", record);
+            }}
+          >
+            替换
+          </Button>
+        </div>
+      );
+    };
+  },
+});

+ 167 - 0
src/pages/website/CreateMat/components/TreePanel/index.tsx

@@ -0,0 +1,167 @@
+import { useResource } from "@/modules/resource";
+import { useQueditor } from "@queenjs-modules/queditor";
+import { switchSceneProdComp } from "@queenjs-modules/queditor/module/controls/Queen3dCtrl/actions/geom";
+import { Pack, PackMat } from "@queenjs-modules/queditor/objects";
+import { IQueentree } from "@queenjs-modules/queentree";
+import { AssetItemFile } from "@queenjs-modules/queentree-explorer/objects/fileSystem/assetFiles";
+import { List } from "@queenjs/ui";
+import { defineComponent } from "vue";
+import LibraryModal from "../LibraryModal";
+import MatItem from "./MatItem";
+import MeshItem from "./MeshItem";
+
+export default defineComponent({
+  setup() {
+    const queditor = useQueditor();
+    const resource = useResource();
+    const { store } = resource;
+
+    const replaceMesh = async () => {
+      const branchFolder = await resource.showModal<AssetItemFile>(
+        <LibraryModal />,
+        { width: "900px" }
+      );
+
+      const pack = await branchFolder.getAssetDetail();
+      console.log("pack: ", pack);
+      const data = queditor.helper.getRelatedSourceByProduct(
+        pack.source as Pack["source"],
+        branchFolder.state.id
+      );
+
+      console.error("data: ", data);
+      queditor.actions.insertMesh(data);
+    };
+
+    const replaceMat = async (record: any) => {
+      const branchFolder = await resource.showModal<AssetItemFile>(
+        <LibraryModal nodeTypes={["matGroupItem", "mat", "packMat"]} />,
+        { width: "900px" }
+      );
+      // console.error("branchFolder: ", branchFolder);
+      const data = await branchFolder.getAssetDetail();
+      // console.error("data: ", data);
+
+      let mat;
+      switch (branchFolder.nodeType) {
+        case "matGroupItem":
+          mat = (data as IQueentree.IAssetMatGroup).source.colorCards.find(
+            (d: PackMat) => d.id == branchFolder.state.id
+          );
+          break;
+        case "packMat":
+          mat = (data.source as Pack["source"]).mats.find(
+            (d: PackMat) => d.id == branchFolder.state.id
+          );
+          break;
+        case "mat":
+          mat = data.source as PackMat;
+          break;
+      }
+
+      if (!data || !mat) return;
+      const matConf = resource.store.matSlots.find(
+        (item) => item.id == record.id
+      );
+      if (matConf) {
+        matConf.material = mat;
+      } else {
+        const matSlot = { id: record.id, material: mat };
+        resource.store.matSlots.push(matSlot);
+      }
+
+      record.thumbnail = mat?.thumbnail?.url;
+
+      // console.error("resource.store.matSlots: ", resource.store.matSlots);
+
+      const targetComp = getTargetProCom(record);
+      if (!targetComp || !mat) return;
+      queditor.actions.updatePackProductCompMat(targetComp, mat);
+      switchComp(record.meshSlotName);
+
+      // const targetMesh = store.treeData.find(
+      //   (m: any) => m.Id == record.meshSlotId
+      // );
+      // store.activeKey = { type: "comp", CompId: targetMesh.Id };
+    };
+
+    const getTargetProCom = (record: any) => {
+      const source: Pack["source"] = queditor.store.pack;
+      const targetPro = source.products[0];
+      const targetComp = targetPro?.components.find(
+        (c) => c.name == record.meshSlotName
+      );
+      return targetComp;
+    };
+
+    const switchComp = (compName: string) => {
+      switchSceneProdComp.call(
+        queditor.controls.queen3dCtrl,
+        queditor.store.pack.scenes[0].products[0].id,
+        compName
+      );
+    };
+
+    const clickMesh = (item: any) => {
+      resource.store.setActiveKey({
+        type: "mesh",
+        id: item.Id,
+      });
+      switchComp(item.meshName);
+    };
+
+    const clickMat = (item: any) => {
+      switchComp(item.meshSlotName);
+      resource.store.setActiveKey({
+        type: "mat",
+        id: item.id,
+      });
+    };
+
+    return () => {
+      const Meshlist = store.treeData || [];
+
+      return (
+        <div class="w-300px flex flex-col">
+          <div class="p-15px text-16px text-white border-dark-800 border-0 border-b-1 border-solid">
+            模型列表
+          </div>
+          <List
+            data={Meshlist}
+            gap="10px"
+            class="scrollbar flex-1 py-15px px-15px"
+          >
+            {{
+              item: (record: any) => (
+                <div>
+                  <MeshItem
+                    record={record}
+                    active={record.Id == store.activeKeys.id}
+                    onReplace={replaceMesh}
+                    onClick={clickMesh}
+                  />
+                  <div class="grid gap-y-10px pt-10px pl-10px">
+                    {record.children.map((item: any) => (
+                      <MatItem
+                        key={item.id}
+                        record={item}
+                        active={item.id == store.activeKeys.id}
+                        onClick={clickMat}
+                        onReplace={replaceMat}
+                      />
+                    ))}
+                  </div>
+                </div>
+              ),
+              loadmore: () => (
+                <div class="text-center py-20px text-12px opacity-80">
+                  没有更多了
+                </div>
+              ),
+            }}
+          </List>
+        </div>
+      );
+    };
+  },
+});

+ 2 - 11
src/pages/website/CreateMat/index.tsx

@@ -1,12 +1,11 @@
 import { useResource } from "@/modules/resource";
 import { initQueditor } from "@queenjs-modules/queditor";
-import { switchSceneProdComp } from "@queenjs-modules/queditor/module/controls/Queen3dCtrl/actions/geom";
 import { initExpViewer } from "@queenjs-modules/queentree-explorer-viewer";
 import { defineComponent, onBeforeUnmount, onMounted } from "vue";
 import { useRoute } from "vue-router";
 import AttrPanel from "./components/AttrPanel";
 import Header from "./components/Header";
-import LeftPanel from "./components/LeftPanel";
+import TreePanel from "./components/TreePanel";
 
 export default defineComponent({
   setup() {
@@ -38,7 +37,7 @@ export default defineComponent({
     queditor.initComponents({
       Viewport: {
         SiderLeft: {
-          default: LeftPanel,
+          default: TreePanel,
         },
         Toolbar: {
           default: () => null,
@@ -53,14 +52,6 @@ export default defineComponent({
       queditor.actions.initPack(resource.store.sourceDetail?.webEditor?.pack);
       queditor.store.setCurrScene(0);
       expViewer.store.setEditNodeUid(resource.store.sourceDetail._id);
-
-      queditor.actions.on("initQueen3dScene:success", () => {
-        switchSceneProdComp.call(
-          queditor.controls.queen3dCtrl,
-          queditor.store.pack.scenes[0].products[0].id,
-          resource.store.currentMesh.meshName
-        );
-      });
     };
 
     onMounted(() => init());