bianjiang hai 1 ano
pai
achega
5c21d1f8cb

+ 1 - 0
package.json

@@ -20,6 +20,7 @@
     "@queenjs-modules/queditor": "^0.0.16",
     "@queenjs-modules/queentree": "^0.0.8",
     "@queenjs-modules/queentree-explorer": "^0.0.2",
+    "@queenjs/assets": "^0.0.22",
     "@queenjs/components": "0.0.23",
     "@queenjs/controllers": "^0.0.8",
     "@queenjs/icons": "^0.0.20",

+ 0 - 85
src/comm/controllers/appMsgRecvCtrl.ts

@@ -1,85 +0,0 @@
-/**
- * 负责app之间的消息的接收模块
- */
-import { nanoid } from "nanoid";
-import { Controller } from "../core/controller";
-import { useCtx } from "../ctx";
-
-export type AssetSendedCallback = (assetUri: string) => Promise<boolean>;
-export type AssetType = "empty" | "mat";
-const RevcChangeEvent = "app.recv.change";
-class AssetListener {
-  id = "";
-  type = "empty" as AssetType;
-  constructor(public actionName: string, public callback: AssetSendedCallback) {
-    this.id = actionName;
-  }
-
-  toJson() {
-    return {
-      id: this.id,
-      type: this.type,
-      action: this.actionName,
-    };
-  }
-}
-
-export class AppMsgRecvController extends Controller {
-  listeners = [] as AssetListener[];
-  appGuid = "";
-
-  async onReady() {
-    const { deviceCtrl, natsCtrl } = useCtx();
-
-    this.appGuid = deviceCtrl.profile.appGuid;
-
-    natsCtrl.subscribe(
-      `send.${this.appGuid}`,
-      async (msg: { id: string; shareId: string }) => {
-        const listen = this.listeners.find((item) => item.id == msg.id);
-        if (!listen)
-          return {
-            isOk: false,
-            error: "nolistener",
-          };
-
-        const ok = await listen.callback(msg.shareId);
-        return JSON.stringify({ isOk: ok });
-      }
-    );
-
-    natsCtrl.subscribe(`recv.actions.${this.appGuid}`, async () => {
-      return JSON.stringify(this.listeners.map((item) => item.toJson()));
-    });
-  }
-
-  //清除所有监听者
-  clearListeners() {
-    this.listeners = [];
-  }
-
-  emitChange() {
-    const { natsCtrl } = useCtx();
-    natsCtrl.publish(RevcChangeEvent, JSON.stringify({ Guid: this.appGuid }));
-  }
-
-  //添加资源监听者
-  addAssetsListener(actionName: string, handle: AssetSendedCallback) {
-    let listen = this.listeners.find((item) => item.actionName == actionName);
-    if (listen) {
-      listen.callback = handle;
-      return listen.id;
-    }
-    listen = new AssetListener(actionName, handle);
-    listen.type = "mat";
-    this.listeners.push(listen);
-  }
-
-  removeListener(id: string) {
-    let listen = this.listeners.find((item) => item.id == id);
-    if (!listen) {
-      return;
-    }
-    this.listeners.splice(this.listeners.indexOf(listen), 1);
-  }
-}

+ 0 - 9
src/comm/controllers/appMsgSendCtrl.ts

@@ -1,9 +0,0 @@
-/**
- * 应用间消息发送模块
- */
-
-import { Controller } from "../core/controller";
-
-export class AppMsgSendController extends Controller {
-
-}

+ 0 - 234
src/comm/controllers/appScreenMsgCtrl.ts

@@ -1,234 +0,0 @@
-/**
- *
- * 当前应用queen5接受的消息处理。
- *   目前文件依赖 设计为 当前文件 依赖于modules/ediotor/controllers下的文件,这样避免share页面打包
- *   当前模块,导致nats.ws文件被引用,引发 部分手机 share页面打不开的bug
- */
-
-import { cloneDeep } from "lodash";
-import { queenApi } from "queenjs";
-import { Controller } from "../core/controller";
-import { useCtx } from "../ctx";
-export class AppScreenMsgController extends Controller {
-  screenModule: any;
-  constructor() {
-    super();
-  }
-
-  onReady() {
-    // this.initScreen();
-  }
-
-  initScreen() {
-    const { recvCtrl } = useCtx();
-
-    recvCtrl.clearListeners();
-    recvCtrl.addAssetsListener("发送模型", async (shareId: string) => {
-      if (!shareId) {
-        return false;
-      }
-      this.downloadLocalPack(shareId);
-      return true;
-    });
-    recvCtrl.emitChange();
-  }
-  async downloadLocalPack(shareId: string) {
-    const { actions } = this.screenModule;
-    const assetDtl = await actions.getAssetDetail(shareId);
-    if (assetDtl) {
-      this.exportLocalWebPack(assetDtl);
-    }
-  }
-
-  loadfile(dir: string, url: string) {
-    if (!url) return;
-    const { deviceCtrl } = useCtx();
-    let names = url.split("/");
-    let name = names[names.length - 1];
-    const screenDir = deviceCtrl.appDataDir + "/screen";
-    const filepath = screenDir + dir + name;
-    return {
-      file: dir + name,
-      promise: new Promise((resolve) => {
-        deviceCtrl.DownloadFile(url, filepath).then((ok) => {
-          setTimeout(() => {
-            resolve({ name: name, ok: ok });
-          }, 0);
-        });
-      }),
-    };
-  }
-
-  async exportLocalWebPack(asset: any) {
-    queenApi.showLoading("资源下载中");
-    const { deviceCtrl } = useCtx();
-
-    let files: any = [];
-    let pack = cloneDeep(asset);
-    let source = pack.source;
-    let wk = `/assets/packs/${pack._id}/`;
-    let scope = this;
-
-    function getFileName(url: string) {
-      if (!url) return;
-
-      let names = url.split("/");
-      let name = names[names.length - 1];
-      return name;
-    }
-
-    //模型处理
-    let geom = source.geoms[0];
-    let osgbinUrl = geom.osgjs.url;
-    source.geoms = [geom];
-    let f = this.loadfile(wk, geom.osgjs.url);
-    files.push(f?.promise);
-    geom.osgjs.url = f?.file;
-
-    osgbinUrl = osgbinUrl.substring(0, osgbinUrl.length - 5) + "bin";
-    f = this.loadfile(wk, osgbinUrl);
-    files.push(f?.promise);
-    f = this.loadfile(wk, geom.shadow.url);
-    if (f) {
-      files.push(f?.promise);
-      geom.shadow.url = f.file;
-    }
-    delete geom.file;
-    delete geom.glb;
-
-    //材质处理
-    for (let i = 0; i < source.mats.length; i++) {
-      const m = source.mats[i];
-      //base
-      let f = scope.loadfile(wk, m.albedo.texture?.url);
-      if (f) {
-        files.push(f?.promise);
-        source.mats[i].albedo.texture.url = f.file;
-      }
-      //法线
-      if (m.normal?.texture) {
-        f = scope.loadfile(wk, m.normal.texture?.url);
-        if (f) {
-          files.push(f?.promise);
-          source.mats[i].normal.texture.url = f.file;
-        }
-      }
-      //粗糙度
-      if (m.roughness.texture) {
-        f = scope.loadfile(wk, m.roughness.texture.url);
-        if (f) {
-          files.push(f?.promise);
-          source.mats[i].roughness.texture.url = f.file;
-        }
-      }
-      //金属度
-      if (m.metalness.texture) {
-        f = scope.loadfile(wk, m.metalness.texture.url);
-        if (f) {
-          files.push(f?.promise);
-          source.mats[i].metalness.texture.url = f.file;
-        }
-      }
-      //透明度
-      if (m.opacity.texture) {
-        f = scope.loadfile(wk, m.opacity.texture.url);
-        if (f) {
-          files.push(f?.promise);
-          source.mats[i].opacity.texture.url = f.file;
-        }
-      }
-    }
-
-    //封面图
-    let thumbnail = pack.thumbnail;
-    const thumb = this.loadfile(wk, thumbnail.url);
-    files.push(thumb?.promise);
-    pack.thumbnail.url = thumb?.file;
-    //环境球
-    const wkenv3d = "/assets/env3d/";
-    const env3dIds: any = [];
-    source.scenes.forEach((item: any) => {
-      if (item.envId) {
-        const envItem = source.env3ds.find((e: any) => {
-          const id = e.userData.assetId || e.id;
-          return id == item.envId;
-        });
-        if (envItem) {
-          env3dIds.push(envItem.id);
-        }
-      }
-    });
-    source.env3ds.forEach((item: any) => {
-      const id = item.userData.assetId || item.id;
-      if (!env3dIds.includes(id)) {
-        return false;
-      }
-      const wk = wkenv3d + id + "/bg/";
-      let f = this.loadfile(wk, item.background?.image?.url);
-      if (f) {
-        item.background.image.url = f.file;
-        files.push(f.promise);
-      }
-      const twk = wkenv3d + id + "/t/";
-      item.config.textures.forEach((t: any) => {
-        t.images.forEach((im: any) => {
-          const f = this.loadfile(twk, im.file);
-          if (f) {
-            im.file = f.file;
-            files.push(f.promise);
-          }
-        });
-      });
-    });
-
-    const wk3 = wkenv3d + "bg/";
-    source.scenes.forEach((c: any) => {
-      //@ts-ignore
-      const url = c.background?.image?.url;
-      if (url) {
-        const f = this.loadfile(wk3, url);
-        if (f) {
-          //@ts-ignore
-          c.background.image.url = f.file;
-          files.push(f.promise);
-        }
-      }
-    });
-
-    const configFile = JSON.stringify(pack);
-    Promise.all(files).then(async (ressult) => {
-      queenApi.hideLoading();
-      const failRes = ressult.filter((e) => {
-        return e.ok == false;
-      });
-      if (failRes.length > 0) {
-        queenApi.messageError(`${failRes[0].name}等文件下载失败!`);
-        return;
-      }
-      const res = await deviceCtrl.WriteFileText(
-        `${deviceCtrl.appDataDir}/screen/assets/packs/${pack._id}.json`,
-        configFile
-      );
-      const currentCategory = localStorage.getItem("category") || "default";
-      const assetsJson = await deviceCtrl.ReadFileText(
-        `${deviceCtrl.appDataDir}/screen/assets_${currentCategory}.json`
-      );
-      if (assetsJson.text) {
-        const assets = JSON.parse(assetsJson.text);
-        const assetItem = {
-          _id: pack._id,
-          name: pack.name,
-          thumbnail: pack.thumbnail.url,
-          type: "3D",
-          meshId: `/assets/packs/${pack._id}.json`,
-        };
-        assets.push(assetItem);
-        await deviceCtrl.WriteFileText(
-          `${deviceCtrl.appDataDir}/screen/assets_${currentCategory}.json`,
-          JSON.stringify(assets)
-        );
-      }
-      this.screenModule.actions.getAssetList();
-    });
-  }
-}

+ 187 - 0
src/comm/controllers/appScreenMsgCtrl.tsx

@@ -0,0 +1,187 @@
+/**
+ *
+ * 当前应用queen5接受的消息处理。
+ *   目前文件依赖 设计为 当前文件 依赖于modules/ediotor/controllers下的文件,这样避免share页面打包
+ *   当前模块,导致nats.ws文件被引用,引发 部分手机 share页面打不开的bug
+ */
+
+import { queenApi } from "queenjs";
+
+import { AppMsgRecvController } from "@queenjs/assets";
+import { nanoid } from "nanoid";
+import { useCtx } from "../ctx";
+import SaveForm from "./components/SaveForm";
+export class AppScreenMsgController extends AppMsgRecvController {
+  screenModule: any;
+  constructor() {
+    super();
+  }
+
+  initScreen() {
+    this.clearListeners();
+    this.addProductListener("添加模型", async (data) => {
+      const { deviceCtrl } = useCtx();
+      console.log("添加模型", data);
+      if (data.payload.products.length != 1) return false;
+
+      const config = (await queenApi.dialog(
+        <SaveForm
+          thumbnail={data.payload.products[0].thumbnail.url}
+          name={data.payload.products[0].name}
+        />,
+        {
+          title: "添加模型",
+          width: "460px",
+          maskClosable: false,
+        }
+      )) as any;
+      console.log("config=>", config);
+
+      const id = nanoid();
+      let path = `/assets/${id}`;
+
+      const source = data.payload;
+      await this.downloadProductAssetFromHttp(path + "/", source);
+
+      let thumbnail = source.products[0].thumbnail;
+      if (thumbnail?.url) {
+        const thumb = this.loadfile(`/assets/${id}`, thumbnail.url);
+        await thumb?.promise;
+        source.products[0].thumbnail.url = thumb?.file;
+      }
+
+      const configFile = JSON.stringify(source);
+      await deviceCtrl.WriteFileText(
+        `${deviceCtrl.appDataDir}/screen/assets/${id}.json`,
+        configFile
+      );
+      const currentCategory = config.category;
+      const assetsJson = await deviceCtrl.ReadFileText(
+        `${deviceCtrl.appDataDir}/screen/assets_${currentCategory}.json`
+      );
+      if (assetsJson.text) {
+        const assets = JSON.parse(assetsJson.text);
+        const assetItem = {
+          _id: id,
+          name: config.name,
+          thumbnail: thumbnail.url,
+          type: "3D",
+          meshId: `/assets/${id}.json`,
+        };
+        assets.push(assetItem);
+        await deviceCtrl.WriteFileText(
+          `${deviceCtrl.appDataDir}/screen/assets_${currentCategory}.json`,
+          JSON.stringify(assets)
+        );
+      }
+      this.screenModule.actions.getAssetList();
+      return true;
+    });
+
+    this.emitChange();
+  }
+
+  loadfile(dir: string, url: string) {
+    if (!url) return;
+    const { deviceCtrl } = useCtx();
+    let names = url.split("/");
+    let name = names[names.length - 1];
+    const screenDir = deviceCtrl.appDataDir + "/screen";
+    const filepath = screenDir + dir + name;
+    return {
+      file: dir + name,
+      promise: new Promise((resolve) => {
+        deviceCtrl.DownloadFile(url, filepath).then((ok) => {
+          setTimeout(() => {
+            resolve({ name: name, ok: ok });
+          }, 0);
+        });
+      }),
+    };
+  }
+  async downloadProductAssetFromHttp(
+    path: string,
+    source: { mats: any[]; geoms: any[]; products: any[] }
+  ) {
+    queenApi.showLoading("资源下载中");
+    let files: any = [];
+
+    this.downGeomsHttp(path, source.geoms, files);
+
+    this.downMatsHttp(path, source.mats, files);
+
+    const result = await Promise.all(files);
+    queenApi.hideLoading();
+    const failRes = result.filter((e) => {
+      return e.ok == false;
+    });
+    console.log(result, failRes);
+
+    return source;
+  }
+  downGeomsHttp(path: string, geoms: any[], files: any[]) {
+    geoms.map((geom) => {
+      const osgjsUrl = geom.osgjs.url;
+      let osjsf = this.loadfile(path, osgjsUrl);
+      if (osjsf) {
+        files.push(osjsf?.promise);
+        geom.osgjs.url = osjsf?.file;
+      }
+
+      const osgbinUrl = osgjsUrl.substring(0, osgjsUrl.length - 5) + "bin";
+      let binf = this.loadfile(path, osgbinUrl);
+      if (binf) {
+        files.push(binf?.promise);
+      }
+      if (geom?.shadow?.url) {
+        let shadowf = this.loadfile(path, geom.shadow.url);
+        if (shadowf) {
+          files.push(shadowf.promise);
+          geom.shadow.url = shadowf.file;
+        }
+      }
+    });
+  }
+  downMatsHttp(path: string, mats: any[], files: any[]) {
+    //材质处理
+    mats.forEach((mItem) => {
+      const abf = this.loadfile(path, mItem.albedo.texture?.url);
+      //base
+      if (abf) {
+        files.push(abf?.promise);
+        mItem.albedo.texture.url = abf.file;
+      }
+      //法线
+      if (mItem?.normal?.texture) {
+        const norf = this.loadfile(path, mItem.normal.texture?.url);
+        if (norf) {
+          files.push(norf?.promise);
+          mItem.normal.texture.url = norf.file;
+        }
+      }
+      //粗糙度
+      if (mItem?.roughness?.texture) {
+        const rouf = this.loadfile(path, mItem.roughness.texture?.url);
+        if (rouf) {
+          files.push(rouf?.promise);
+          mItem.roughness.texture.url = rouf.file;
+        }
+      }
+      //金属度
+      if (mItem?.metalness?.texture) {
+        const metaf = this.loadfile(path, mItem.metalness.texture?.url);
+        if (metaf) {
+          files.push(metaf?.promise);
+          mItem.metalness.texture.url = metaf.file;
+        }
+      }
+      if (mItem?.opacity?.texture) {
+        const opaf = this.loadfile(path, mItem.opacity.texture?.url);
+        if (opaf) {
+          files.push(opaf?.promise);
+          mItem.opacity.texture.url = opaf.file;
+        }
+      }
+    });
+  }
+}

+ 98 - 0
src/comm/controllers/components/SaveForm.tsx

@@ -0,0 +1,98 @@
+import { useCtx } from "@/comm/ctx";
+import { css } from "@linaria/core";
+import { Button, Form, Input, Select } from "ant-design-vue";
+import { useModal } from "queenjs";
+
+import { defineComponent, reactive } from "vue";
+import { string } from "vue-types";
+const layout = {
+  wrapperCol: { span: 24 },
+};
+
+export default defineComponent({
+  props: {
+    thumbnail: string(),
+    name: string(),
+  },
+
+  setup(props) {
+    const modal = useModal();
+
+    const { storeCtrl } = useCtx();
+    const formState = reactive({
+      thumbnail: props.thumbnail,
+      name: props.name,
+      category: "default",
+    });
+    const rules = reactive({
+      name: [
+        { required: true, message: "名称不能为空", trigger: "change" },
+        {
+          min: 2,
+          max: 20,
+          message: "长度为2~20位字符",
+          trigger: "change",
+        },
+      ],
+    });
+
+    const { validate, validateInfos } = Form.useForm(formState, rules);
+
+    const changeCategory = (v: any) => {
+      formState.category = v;
+    };
+
+    const saveSubmit = async () => {
+      await validate();
+      modal.submit(formState);
+    };
+
+    return () => {
+      return (
+        <div class={EditFormStyle}>
+          <div class={"thumbnail mb-24px"}>
+            <img src={formState.thumbnail} />
+          </div>
+          <div class="mb-8px">选择保存位置</div>
+          <div class={"select_box"}>
+            <Select
+              class={"w-full"}
+              value={formState.category}
+              options={storeCtrl.state.category}
+              onChange={(v: any) => changeCategory(v)}
+            />
+          </div>
+          <div class={"mt-24px"}>
+            <Form {...layout} name="basic">
+              <Form.Item {...validateInfos.name}>
+                <Input
+                  prefix="填写名称"
+                  v-model={[formState.name, "value"]}
+                  maxlength={20}
+                />
+              </Form.Item>
+            </Form>
+          </div>
+          <div class={btsStyle}>
+            <Button onClick={() => modal.cancel()}>取消</Button>
+            <Button
+              type="primary"
+              danger={false}
+              style={{ marginLeft: "10px" }}
+              onClick={() => saveSubmit()}
+            >
+              保存
+            </Button>
+          </div>
+        </div>
+      );
+    };
+  },
+});
+
+const EditFormStyle = css``;
+
+const btsStyle = css`
+  display: flex;
+  justify-content: end;
+`;

+ 2 - 2
src/comm/controllers/index.ts

@@ -1,11 +1,11 @@
 import { DeviceController } from "./deviceCtrl";
 import { NatsController } from "./natsCtrl";
-import { AppMsgRecvController } from "./appMsgRecvCtrl";
 import { AppScreenMsgController } from "./appScreenMsgCtrl";
+import { StoreController } from "./storeCtrl";
 
 export {
   DeviceController,
   NatsController,
-  AppMsgRecvController,
   AppScreenMsgController,
+  StoreController,
 };

+ 29 - 0
src/comm/controllers/storeCtrl.ts

@@ -0,0 +1,29 @@
+import { Controller } from "../core/controller";
+import { RxValue } from "../core/rxValue";
+import { useCtx } from "../ctx";
+
+export class StoreController extends Controller {
+  state = RxValue.create({
+    category: [
+      {
+        label: "默认",
+        value: "default",
+      },
+    ],
+  });
+  onReady(): void {
+    this.initCategory();
+  }
+  async initCategory() {
+    const { deviceCtrl } = useCtx();
+    const appDataDir = deviceCtrl.appDataDir;
+    const category = await deviceCtrl.ReadFileText(
+      `${appDataDir}/screen/category.json`
+    );
+
+    if (!category.error && category.text) {
+      const assets = JSON.parse(category.text);
+      this.state.category = assets;
+    }
+  }
+}

+ 4 - 4
src/comm/ctx/config.ts

@@ -2,8 +2,8 @@ import * as Controls from "../controllers";
 const ctx = {
   deviceCtrl: new Controls.DeviceController(),
   natsCtrl: new Controls.NatsController(),
-  recvCtrl: new Controls.AppMsgRecvController(),
-  msgCtrl:new Controls.AppScreenMsgController(),
-}
+  msgCtrl: new Controls.AppScreenMsgController(),
+  storeCtrl: new Controls.StoreController(),
+};
 
-export {ctx};
+export { ctx };

+ 1 - 1
src/comm/ctx/index.ts

@@ -1,6 +1,6 @@
 import { ctx } from "./config";
 
-let initOrder = ["deviceCtrl", "natsCtrl", "recvCtrl", "msgCtrl"];
+let initOrder = ["deviceCtrl", "natsCtrl", "msgCtrl"];
 export async function SetInitOrder<K extends keyof typeof ctx>(ctrls: K[]) {
   initOrder = ctrls;
 }

+ 5 - 19
src/pages/website/routes/backend/List.tsx

@@ -12,15 +12,9 @@ export default defineComponent({
   setup() {
     const ctx = useList();
     const { store, showModal, actions } = ctx;
-    const { deviceCtrl } = useCtx();
+    const { deviceCtrl, storeCtrl } = useCtx();
     const state = reactive({
       select: "default",
-      optsions: [
-        {
-          label: "默认",
-          value: "default",
-        },
-      ],
     });
     const del = async (item: any) => {
       await queenApi.showConfirm({
@@ -73,16 +67,8 @@ export default defineComponent({
     ];
     actions.getAssetList();
     const initShowCategory = async () => {
-      const appDataDir = deviceCtrl.appDataDir;
-      const category = await deviceCtrl.ReadFileText(
-        `${appDataDir}/screen/category.json`
-      );
       const currentCategory = localStorage.getItem("category") || "default";
       state.select = currentCategory;
-      if (!category.error && category.text) {
-        const assets = JSON.parse(category.text);
-        state.optsions = assets;
-      }
     };
     const { msgCtrl } = useCtx();
     msgCtrl.screenModule = ctx;
@@ -94,11 +80,11 @@ export default defineComponent({
       actions.getAssetList();
     };
     const createShowCategory = async () => {
-      const { deviceCtrl } = useCtx();
+      const { deviceCtrl, storeCtrl } = useCtx();
       const res: any = await showModal(<CategoryModal />, {
         title: "新建分类",
       });
-      const options = [...state.optsions];
+      const options = [...storeCtrl.state.category];
       const optionItem = options.find((e) => {
         return e.value == res.value;
       });
@@ -106,7 +92,7 @@ export default defineComponent({
         return;
       }
       options.push(res);
-      state.optsions = options;
+      storeCtrl.state.category = options;
       const appDataDir = deviceCtrl.appDataDir;
       await deviceCtrl.WriteFileText(
         `${appDataDir}/screen/category.json`,
@@ -122,7 +108,7 @@ export default defineComponent({
               <Select
                 class={"select"}
                 value={state.select}
-                options={state.optsions}
+                options={storeCtrl.state.category}
                 onChange={setShowCategory}
               ></Select>
               <Button

+ 5 - 0
yarn.lock

@@ -1239,6 +1239,11 @@
   resolved "http://124.70.149.18:4873/@queenjs-modules%2fqueentree/-/queentree-0.0.8.tgz#2a471100a1d09e72dcbc54daa453fc4c275a71b7"
   integrity sha512-+B9WbIUgm656hR9bougNDLAq2p7foaEhybfyjLHu2DIqmyNooKkzNth8e/R/R4QjbCI6Who6Pg6q0H9txnjLcA==
 
+"@queenjs/assets@^0.0.22":
+  version "0.0.22"
+  resolved "http://124.70.149.18:4873/@queenjs%2fassets/-/assets-0.0.22.tgz#32fe79422b50d9d6b328b5824ed2fcbcffdcf2ee"
+  integrity sha512-dnzz13ks5XiUsWLfqHHhdFAtVjasL3YZaeGPgcAM7mJGkByXMjA0rVE5uB0eTZeGyOurewRI16hNLx59RXw0UQ==
+
 "@queenjs/components@0.0.23":
   version "0.0.23"
   resolved "http://124.70.149.18:4873/@queenjs%2fcomponents/-/components-0.0.23.tgz#2451ddc94a99d19d37baf2d1a6d371131aa07485"