chenlei 3 miesięcy temu
rodzic
commit
4900123218
7 zmienionych plików z 387 dodań i 263 usunięć
  1. 4 1
      app.html
  2. 5 2
      index.html
  3. BIN
      public/banners/8/2.png
  4. 3 3
      src/assets/data.js
  5. 163 166
      src/components/SearchPopup.vue
  6. 179 65
      src/pages/Scene/index.vue
  7. 33 26
      src/utils/index.js

+ 4 - 1
app.html

@@ -3,7 +3,10 @@
   <head>
     <meta charset="UTF-8" />
     <link rel="icon" href="/favicon.ico" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta
+      name="viewport"
+      content="width=device-width, initial-scale=1.0, maximum-scale=1"
+    />
     <title>博物中国</title>
   </head>
   <body>

+ 5 - 2
index.html

@@ -3,7 +3,10 @@
   <head>
     <meta charset="UTF-8" />
     <link rel="icon" href="/favicon.ico" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta
+      name="viewport"
+      content="width=device-width, initial-scale=1.0, maximum-scale=1"
+    />
     <title>博物中国</title>
     <style>
       html,
@@ -14,7 +17,7 @@
       .app-frame-shell {
         box-sizing: border-box;
         /* 大写 PX:避免 postcss-px-to-viewport 把壳层宽度换算成 vw */
-        max-width: 640PX;
+        max-width: 640px;
         height: 100%;
         min-height: 100%;
         margin: 0 auto;

BIN
public/banners/8/2.png


+ 3 - 3
src/assets/data.js

@@ -950,7 +950,7 @@ export const infoTemo = {
       id: 23,
       name: "如意度量 五彩意境--西藏唐卡艺术主题展勉唐篇",
       partOf: "西藏博物馆",
-      link: `${serverUrlTemp}/bwCN/scene/SG-ooxdHQTAxfH/scene/index.html#/?m=SG-ooxdHQTAxfH`,
+      link: `${serverUrlTemp}/bwCN/scene/SG-ooxdHQTAxfH/scene/index.html?m=SG-ooxdHQTAxfH`,
       code: "SG-ooxdHQTAxfH",
       oldNum: 0,
       newNum: 0,
@@ -996,7 +996,7 @@ export const infoTemo = {
       id: 22,
       name: "祥开万象--故宫与西藏文物联展",
       partOf: "西藏博物馆",
-      link: `${serverUrlTemp}/bwCN/scene/SG-O5Vt5xTC2ek/scene/index.html#/?m=SG-O5Vt5xTC2ek`,
+      link: `${serverUrlTemp}/bwCN/scene/SG-O5Vt5xTC2ek/scene/index.html?m=SG-O5Vt5xTC2ek`,
       code: "SG-O5Vt5xTC2ek",
       oldNum: 0,
       newNum: 0,
@@ -1288,7 +1288,7 @@ export const infoTemo = {
       id: 15,
       name: "元代的杭州和泉州",
       partOf: "杭州博物馆",
-      link: `${serverUrlTemp}/bwCN/scene/SG-KMDgP6uBEsn/index.html#/scene`,
+      link: `${serverUrlTemp}/bwCN/scene/SG-KMDgP6uBEsn/index.html?m=SG-KMDgP6uBEsn#/scene`,
       code: "SG-KMDgP6uBEsn",
       oldNum: 0,
       newNum: 0,

+ 163 - 166
src/components/SearchPopup.vue

@@ -1,166 +1,163 @@
-<template>
-  <VanPopup
-    v-model:show="show"
-    class="search"
-    :close-on-click-overlay="false"
-    :overlay-style="{
-      background: 'rgba(239, 232, 210, 0.90)',
-      backdropFilter: 'blur(10px)',
-    }"
-  >
-    <div class="search-top">
-      <i class="search-close" @click="show = false" />
-      <div class="search-input">
-        <input
-          ref="inputRef"
-          v-model.trim="keyword"
-          type="search"
-          enterkeyhint="search"
-          placeholder="请输入展览名称"
-          @keydown.enter.prevent="blurInput"
-        />
-        <i class="search-icon" aria-hidden="true" />
-      </div>
-    </div>
-
-    <div class="search-panel">
-      <template v-if="filteredList.length">
-        <Card v-for="item in filteredList" :key="item.id" :item="item" />
-      </template>
-      <p v-else class="search-empty">
-        {{ keyword.trim() ? "未找到相关展览" : "请输入展览名称进行搜索" }}
-      </p>
-    </div>
-  </VanPopup>
-</template>
-
-<script setup>
-import { computed, ref, watch, nextTick } from "vue";
-import Card from "./Card.vue";
-import { infoTemo } from "@/assets/data.js";
-
-const props = defineProps({
-  visible: {
-    type: Boolean,
-    default: false,
-  },
-  /** 与首页列表同源,便于展示最新浏览量;不传则用本地数据 */
-  items: {
-    type: Array,
-    default: null,
-  },
-});
-const emit = defineEmits(["update:visible"]);
-const show = computed({
-  get() {
-    return props.visible;
-  },
-  set(value) {
-    emit("update:visible", value);
-  },
-});
-
-const inputRef = ref(null);
-const keyword = ref("");
-
-const sourceList = computed(() =>
-  props.items?.length ? props.items : infoTemo.swArr,
-);
-
-const filteredList = computed(() => {
-  const q = keyword.value.trim().toLowerCase();
-  const list = sourceList.value;
-  if (!q) return [];
-  return list.filter((item) => {
-    const name = String(item.name ?? "").toLowerCase();
-    const part = String(item.partOf ?? "").toLowerCase();
-    return name.includes(q) || part.includes(q);
-  });
-});
-
-const blurInput = () => {
-  inputRef.value?.blur();
-};
-
-watch(show, (open) => {
-  if (open) {
-    nextTick(() => inputRef.value?.focus());
-  } else {
-    keyword.value = "";
-  }
-});
-</script>
-
-<style lang="scss" scoped>
-.search {
-  display: flex;
-  flex-direction: column;
-  padding-top: 35px;
-  width: 100%;
-  max-width: 100%;
-  height: 100%;
-  background: none;
-
-  &-top {
-    display: flex;
-    align-items: center;
-    gap: 22px;
-    padding: 0 25px;
-  }
-  &-close {
-    width: 16px;
-    height: 36px;
-    background: url("@/assets/images/back.png") no-repeat center / contain;
-  }
-  &-input {
-    flex: 1;
-    display: flex;
-    align-items: center;
-    gap: 20px;
-    padding: 0 27px 0 19px;
-    height: 60px;
-    border: 1px solid #76685e;
-    background: #f9f7ef;
-    border-radius: 7px;
-
-    input {
-      flex: 1;
-      color: #76685e;
-      font-size: 24px;
-      background: none;
-      border: none;
-      outline: none;
-    }
-  }
-  &-icon {
-    width: 32px;
-    height: 34px;
-    background: url("@/assets/images/search.png") no-repeat center / contain;
-  }
-  &-panel {
-    margin-top: 32px;
-    padding: 0 25px 35px;
-    flex: 1;
-    min-height: 0;
-    display: flex;
-    flex-wrap: wrap;
-    align-items: flex-start;
-    gap: 33px;
-    overflow-y: auto;
-
-    :deep(.card) {
-      width: calc(50% - 16.5px);
-    }
-  }
-
-  &-empty {
-    grid-column: 1 / -1;
-    margin: 80px 0 0;
-    width: 100%;
-    text-align: center;
-    color: #76685e;
-    font-size: 26px;
-    line-height: 1.5;
-  }
-}
-</style>
+<template>
+  <VanPopup
+    v-model:show="show"
+    class="search"
+    :close-on-click-overlay="false"
+    :overlay-style="{
+      background: 'rgba(239, 232, 210, 0.90)',
+      backdropFilter: 'blur(10px)',
+    }"
+  >
+    <div class="search-top">
+      <i class="search-close" @click="show = false" />
+      <div class="search-input">
+        <input
+          ref="inputRef"
+          v-model.trim="keyword"
+          type="search"
+          enterkeyhint="search"
+          placeholder="请输入展览名称"
+          @keydown.enter.prevent="blurInput"
+        />
+        <i class="search-icon" aria-hidden="true" />
+      </div>
+    </div>
+
+    <div class="search-panel">
+      <template v-if="filteredList.length">
+        <Card v-for="item in filteredList" :key="item.id" :item="item" />
+      </template>
+      <p v-else class="search-empty">
+        {{ keyword.trim() ? "未找到相关展览" : "请输入展览名称进行搜索" }}
+      </p>
+    </div>
+  </VanPopup>
+</template>
+
+<script setup>
+import { computed, ref, watch, nextTick } from "vue";
+import Card from "./Card.vue";
+import { infoTemo } from "@/assets/data.js";
+
+const props = defineProps({
+  visible: {
+    type: Boolean,
+    default: false,
+  },
+  /** 与首页列表同源,便于展示最新浏览量;不传则用本地数据 */
+  items: {
+    type: Array,
+    default: null,
+  },
+});
+const emit = defineEmits(["update:visible"]);
+const show = computed({
+  get() {
+    return props.visible;
+  },
+  set(value) {
+    emit("update:visible", value);
+  },
+});
+
+const inputRef = ref(null);
+const keyword = ref("");
+
+const sourceList = computed(() =>
+  props.items?.length ? props.items : infoTemo.swArr,
+);
+
+const filteredList = computed(() => {
+  const q = keyword.value.trim().toLowerCase();
+  const list = sourceList.value;
+  if (!q) return [];
+  return list.filter((item) => {
+    const name = String(item.name ?? "").toLowerCase();
+    const part = String(item.partOf ?? "").toLowerCase();
+    return name.includes(q) || part.includes(q);
+  });
+});
+
+const blurInput = () => {
+  inputRef.value?.blur();
+};
+
+watch(show, (open) => {
+  if (open) {
+    nextTick(() => inputRef.value?.focus());
+  } else {
+    keyword.value = "";
+  }
+});
+</script>
+
+<style lang="scss" scoped>
+.search {
+  display: flex;
+  flex-direction: column;
+  padding-top: 35px;
+  width: 100%;
+  max-width: 100%;
+  height: 100%;
+  background: none;
+
+  &-top {
+    display: flex;
+    align-items: center;
+    gap: 22px;
+    padding: 0 25px;
+  }
+  &-close {
+    width: 16px;
+    height: 36px;
+    background: url("@/assets/images/back.png") no-repeat center / contain;
+  }
+  &-input {
+    flex: 1;
+    display: flex;
+    align-items: center;
+    gap: 20px;
+    padding: 0 27px 0 19px;
+    height: 60px;
+    border: 1px solid #76685e;
+    background: #f9f7ef;
+    border-radius: 7px;
+
+    input {
+      flex: 1;
+      color: #76685e;
+      font-size: 24px;
+      background: none;
+      border: none;
+      outline: none;
+    }
+  }
+  &-icon {
+    width: 32px;
+    height: 34px;
+    background: url("@/assets/images/search.png") no-repeat center / contain;
+  }
+  &-panel {
+    margin-top: 32px;
+    padding: 0 25px 35px;
+    display: flex;
+    flex-wrap: wrap;
+    gap: 33px;
+    overflow-y: auto;
+
+    :deep(.card) {
+      width: calc(50% - 16.5px);
+    }
+  }
+
+  &-empty {
+    grid-column: 1 / -1;
+    margin: 80px 0 0;
+    width: 100%;
+    text-align: center;
+    color: #76685e;
+    font-size: 26px;
+    line-height: 1.5;
+  }
+}
+</style>

+ 179 - 65
src/pages/Scene/index.vue

@@ -1,65 +1,179 @@
-<template>
-  <div class="scene-page">
-    <iframe
-      class="scene-iframe"
-      :src="iframeSrc"
-      frameborder="0"
-      title="场景"
-    />
-    <img
-      src="@/assets/images/back1.png"
-      alt="返回"
-      class="scene-back-btn"
-      @click="goBack"
-    />
-  </div>
-</template>
-
-<script setup>
-import { computed } from "vue";
-import { useRouter, useRoute } from "vue-router";
-
-const router = useRouter();
-const route = useRoute();
-
-const iframeSrc = computed(() => {
-  const url = new URL(decodeURIComponent(route.query.url));
-  url.searchParams.set("play", "1");
-  return url;
-});
-
-const goBack = () => {
-  router.back();
-};
-</script>
-
-<style scoped lang="scss">
-@use "@/assets/utils.scss";
-
-.scene-page {
-  position: fixed;
-  inset: 0;
-  width: 100vw;
-  height: 100vh;
-  z-index: 0;
-}
-
-.scene-iframe {
-  position: absolute;
-  inset: 0;
-  width: 100%;
-  height: 100%;
-  border: none;
-}
-
-// prettier-ignore
-.scene-back-btn {
-  position: absolute;
-    top: 20px;
-    left: 20px;
-    width: 67px;
-    height: 67px;
-    cursor: pointer;
-    z-index: 999;
-}
-</style>
+<template>
+  <div class="scene-page">
+    <iframe
+      ref="sceneIframeRef"
+      class="scene-iframe"
+      :src="iframeSrc"
+      frameborder="0"
+      title="场景"
+      @load="onIframeLoad"
+    />
+    <img
+      src="@/assets/images/back1.png"
+      alt="返回"
+      class="scene-back-btn"
+      @click="goBack"
+    />
+  </div>
+</template>
+
+<script setup>
+import { computed, ref } from "vue";
+import { useRouter, useRoute } from "vue-router";
+
+const router = useRouter();
+const route = useRoute();
+
+const sceneIframeRef = ref(null);
+
+const HIDE_MODEL_TITLE_STYLE_ID = "bw-cn-hide-model-title";
+const NESTED_IFRAME_WIRED_ATTR = "data-bw-hide-model-title-wired";
+
+function injectHideModelTitle(doc) {
+  if (!doc?.head) return;
+  if (doc.getElementById(HIDE_MODEL_TITLE_STYLE_ID)) return;
+  const style = doc.createElement("style");
+  style.id = HIDE_MODEL_TITLE_STYLE_ID;
+  style.textContent = "#model-title { display: none !important; }";
+  doc.head.appendChild(style);
+}
+
+/** 可读子文档时返回 document;跨域或无权访问时为 null(不会抛错) */
+function getAccessibleIframeDocument(iframe) {
+  if (!iframe) return null;
+  try {
+    const a = iframe.contentDocument;
+    if (a) return a;
+  } catch {
+    return null;
+  }
+  try {
+    return iframe.contentWindow?.document ?? null;
+  } catch {
+    return null;
+  }
+}
+
+const NESTED_DOC_RETRY_MS = [0, 10, 30, 80, 160, 320];
+
+/** 子 iframe 在 load 瞬间 contentDocument 可能仍为 null,短重试;持续为 null 则多为跨域/sandbox */
+function tryWireNestedWhenReady(nested, attempt = 0) {
+  const innerDoc = getAccessibleIframeDocument(nested);
+  if (innerDoc?.head) {
+    tryWireAndInject(innerDoc);
+    return;
+  }
+  if (attempt >= NESTED_DOC_RETRY_MS.length) return;
+  setTimeout(
+    () => tryWireNestedWhenReady(nested, attempt + 1),
+    NESTED_DOC_RETRY_MS[attempt],
+  );
+}
+
+/**
+ * 同域可访问的 document 上隐藏 .model-title,并递归处理内部的 iframe.scene-iframe(model-title 可能在内层)。
+ * 跨域子文档无法访问时会静默跳过。
+ */
+function tryWireAndInject(doc) {
+  if (!doc) return;
+  try {
+    if (!doc.head) return;
+  } catch {
+    return;
+  }
+
+  injectHideModelTitle(doc);
+
+  let nestedList;
+  try {
+    nestedList = doc.querySelectorAll("iframe.scene-iframe");
+  } catch {
+    return;
+  }
+
+  nestedList.forEach((nested) => {
+    if (nested.getAttribute(NESTED_IFRAME_WIRED_ATTR) === "1") return;
+    nested.setAttribute(NESTED_IFRAME_WIRED_ATTR, "1");
+
+    const onNestedLoad = () => {
+      tryWireNestedWhenReady(nested);
+    };
+
+    nested.addEventListener("load", onNestedLoad);
+    onNestedLoad();
+  });
+}
+
+function onIframeLoad() {
+  const iframe = sceneIframeRef.value;
+  if (!iframe) return;
+  let doc;
+  try {
+    doc = iframe.contentDocument;
+  } catch {
+    return;
+  }
+  tryWireAndInject(doc);
+}
+
+const iframeSrc = computed(() => {
+  const raw = decodeURIComponent(route.query.url);
+  const url = new URL(raw);
+  const hash = url.hash;
+
+  if (hash.includes("?")) {
+    const q = hash.indexOf("?");
+    const hashPathOnly = hash.slice(0, q);
+    // #/?mask=1&back=1:参数留在 hash 内,只在页面 search 上加 play
+    if (hashPathOnly === "#/") {
+      url.searchParams.set("play", "1");
+    } else {
+      const hashQuery = new URLSearchParams(hash.slice(q + 1));
+      for (const [key, value] of hashQuery) {
+        url.searchParams.set(key, value);
+      }
+      url.searchParams.set("play", "1");
+      url.hash = hashPathOnly;
+    }
+  } else {
+    url.searchParams.set("play", "1");
+  }
+
+  return url.href;
+});
+
+const goBack = () => {
+  router.back();
+};
+</script>
+
+<style scoped lang="scss">
+@use "@/assets/utils.scss";
+
+.scene-page {
+  position: fixed;
+  inset: 0;
+  width: 100vw;
+  height: 100vh;
+  z-index: 0;
+}
+
+.scene-iframe {
+  position: absolute;
+  inset: 0;
+  width: 100%;
+  height: 100%;
+  border: none;
+}
+
+// prettier-ignore
+.scene-back-btn {
+  position: absolute;
+    top: 20px;
+    left: 20px;
+    width: 67px;
+    height: 67px;
+    cursor: pointer;
+    z-index: 999;
+}
+</style>

+ 33 - 26
src/utils/index.js

@@ -1,26 +1,33 @@
-import router from "@/router";
-
-/** 与 assets/utils.scss 中 @function vh-calc 一致,供 :style 动态绑定 */
-export function vhCalc(num) {
-  if (num === 0) return "0";
-  return `calc(100vh * (${num} / var(--design-height)))`;
-}
-
-/** 将 img.style 中的数值字段转为 vh-calc 结果 */
-export function vhCalcStyle(style) {
-  if (!style || typeof style !== "object") return {};
-  const out = {};
-  for (const [key, val] of Object.entries(style)) {
-    out[key] = typeof val === "number" ? vhCalc(val) : val;
-  }
-  return out;
-}
-
-export const openPage = (item, verify = true) => {
-  if (verify && item.bannerConfig) {
-    router.push({ name: "Banner", params: { id: item.id } });
-    return;
-  }
-
-  router.push({ name: "Scene", query: { url: encodeURIComponent(item.link) } });
-};
+import router from "@/router";
+
+/** 与 assets/utils.scss 中 @function vh-calc 一致,供 :style 动态绑定 */
+export function vhCalc(num) {
+  if (num === 0) return "0";
+  return `calc(100vh * (${num} / var(--design-height)))`;
+}
+
+/** 将 img.style 中的数值字段转为 vh-calc 结果 */
+export function vhCalcStyle(style) {
+  if (!style || typeof style !== "object") return {};
+  const out = {};
+  for (const [key, val] of Object.entries(style)) {
+    out[key] = typeof val === "number" ? vhCalc(val) : val;
+  }
+  return out;
+}
+
+export const openPage = (item, verify = true) => {
+  if (verify && item.bannerConfig) {
+    router.push({ name: "Banner", params: { id: item.id } });
+    return;
+  }
+
+  const returnTo = router.currentRoute.value.fullPath;
+  router.push({
+    name: "Scene",
+    query: {
+      url: encodeURIComponent(item.link),
+      return: encodeURIComponent(returnTo),
+    },
+  });
+};