chenlei преди 7 месеца
родител
ревизия
61d69c8859
променени са 38 файла, в които са добавени 10466 реда и са изтрити 1999 реда
  1. 1 0
      packages/pc/package.json
  2. BIN
      packages/pc/public/model/13.398九旒冕.fbx
  3. BIN
      packages/pc/public/model/戗金云龙纹朱漆木匣/12.34戗金云龙纹朱漆木匣.FBX
  4. BIN
      packages/pc/public/model/戗金云龙纹朱漆木匣/Thumbs.db
  5. BIN
      packages/pc/public/model/水晶鹿镇纸/2.938.FBX
  6. BIN
      packages/pc/public/model/白玉圭/2.937白玉圭.fbx
  7. BIN
      packages/pc/public/model/白玉花形杯/2.940白玉花形杯.fbx
  8. BIN
      packages/pc/public/model/镶宝石金带饰/东7.258.fbx.FBX
  9. BIN
      packages/pc/public/model/青白釉云龙纹盖罐/5.1053.fbx.FBX
  10. 3797 0
      packages/pc/public/modelLook/assets/index-BcLjdLAW.js
  11. 3797 0
      packages/pc/public/modelLook/assets/index-P16QZIgH.js
  12. BIN
      packages/pc/public/modelLook/assets/tinyusdz-DnE9aqql.wasm
  13. 25 0
      packages/pc/public/modelLook/index.html
  14. 105 0
      packages/pc/src/components/Game6/TagModel.js
  15. 319 0
      packages/pc/src/components/Game6/components/Model.vue
  16. BIN
      packages/pc/src/components/Game6/images/biaoqian_left.png
  17. BIN
      packages/pc/src/components/Game6/images/btn_small_active.png
  18. BIN
      packages/pc/src/components/Game6/images/btn_small_noraml.png
  19. BIN
      packages/pc/src/components/Game6/images/img_chonger.png
  20. BIN
      packages/pc/src/components/Game6/images/img_guanwu.png
  21. BIN
      packages/pc/src/components/Game6/images/img_jinchi.png
  22. BIN
      packages/pc/src/components/Game6/images/img_jinzan.png
  23. BIN
      packages/pc/src/components/Game6/images/img_liu.png
  24. BIN
      packages/pc/src/components/Game6/images/img_yanban.png
  25. BIN
      packages/pc/src/components/Game6/images/img_yuheng.png
  26. 89 0
      packages/pc/src/components/Game6/index.scss
  27. 68 0
      packages/pc/src/components/Game6/index.vue
  28. BIN
      packages/pc/src/components/Tabbar/images/icon-1.png
  29. BIN
      packages/pc/src/components/Tabbar/images/icon-2.png
  30. BIN
      packages/pc/src/components/Tabbar/images/icon-3.png
  31. BIN
      packages/pc/src/components/Tabbar/images/icon_luwang_active.png
  32. 7 5
      packages/pc/src/components/Tabbar/index.vue
  33. 7 0
      packages/pc/src/views/Collection/constants.ts
  34. 6 1
      packages/pc/src/views/Collection/detail.scss
  35. 36 6
      packages/pc/src/views/Collection/detail.vue
  36. 25 0
      packages/pc/src/views/Scroll/index.scss
  37. 32 1
      packages/pc/src/views/Scroll/index.vue
  38. 2152 1986
      pnpm-lock.yaml

+ 1 - 0
packages/pc/package.json

@@ -19,6 +19,7 @@
     "pinia": "^2.2.6",
     "postcss-px-to-viewport": "^1.1.1",
     "swiper": "^11.1.15",
+    "three": "^0.182.0",
     "vue": "^3.5.13",
     "vue-lazyload-next": "^0.0.2",
     "vue-qrcode": "^2.2.2",

BIN
packages/pc/public/model/13.398九旒冕.fbx


BIN
packages/pc/public/model/戗金云龙纹朱漆木匣/12.34戗金云龙纹朱漆木匣.FBX


BIN
packages/pc/public/model/戗金云龙纹朱漆木匣/Thumbs.db


BIN
packages/pc/public/model/水晶鹿镇纸/2.938.FBX


BIN
packages/pc/public/model/白玉圭/2.937白玉圭.fbx


BIN
packages/pc/public/model/白玉花形杯/2.940白玉花形杯.fbx


BIN
packages/pc/public/model/镶宝石金带饰/东7.258.fbx.FBX


BIN
packages/pc/public/model/青白釉云龙纹盖罐/5.1053.fbx.FBX


Файловите разлики са ограничени, защото са твърде много
+ 3797 - 0
packages/pc/public/modelLook/assets/index-BcLjdLAW.js


Файловите разлики са ограничени, защото са твърде много
+ 3797 - 0
packages/pc/public/modelLook/assets/index-P16QZIgH.js


BIN
packages/pc/public/modelLook/assets/tinyusdz-DnE9aqql.wasm


+ 25 - 0
packages/pc/public/modelLook/index.html

@@ -0,0 +1,25 @@
+<!doctype html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta name="color-scheme" content="light" />
+    <title>GLB 加载演示</title>
+    <style>
+      html,
+      body,
+      #root {
+        height: 100%;
+        background: transparent;
+      }
+      body {
+        margin: 0;
+        overscroll-behavior: none;
+      }
+    </style>
+    <script type="module" crossorigin src="./assets/index-BcLjdLAW.js"></script>
+  </head>
+  <body>
+    <div id="root"></div>
+  </body>
+</html>

+ 105 - 0
packages/pc/src/components/Game6/TagModel.js

@@ -0,0 +1,105 @@
+import {
+  DoubleSide,
+  Mesh,
+  PlaneGeometry,
+  ShaderMaterial,
+  TextureLoader,
+} from "three";
+
+export class TagModel {
+  showTime;
+  hideTime;
+
+  mesh;
+  progress = 0;
+
+  constructor(params) {
+    const planeGeometry = new PlaneGeometry(
+      params.width,
+      params.height,
+      20,
+      20,
+    );
+    const shaderMaterial = new ShaderMaterial({
+      uniforms: {
+        uTexture: { value: new TextureLoader().load(params.url) },
+        uMaskTexture: { value: new TextureLoader().load(params.maskUrl) },
+        uProgress: { value: 0 },
+      },
+      transparent: true,
+      vertexShader: `
+        varying vec2 vUv;
+        void main() {
+          vUv = uv;
+          gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
+        }
+      `,
+      fragmentShader: `
+        varying vec2 vUv;
+        uniform sampler2D uTexture;
+        uniform float uProgress;
+        void main() {
+          vec4 tex = texture2D(uTexture, vUv);
+          vec4 color = tex;
+          gl_FragColor = color;
+        }
+      `,
+      side: DoubleSide,
+      opacity: 1,
+    });
+
+    this.mesh = new Mesh(planeGeometry, shaderMaterial);
+    this.mesh.rotation.set(
+      params.rotation.x,
+      params.rotation.y,
+      params.rotation.z,
+    );
+    this.mesh.position.copy(params.position);
+    this.mesh.scale.set(params.scale, params.scale, params.scale);
+    this.mesh.name = params.name ?? "";
+
+    this.show();
+  }
+
+  show() {
+    this.clearTimer();
+
+    this.showTime = setInterval(() => {
+      const material = this.mesh.material;
+
+      this.progress += 0.05;
+      material.uniforms.uProgress.value = this.progress;
+
+      if (this.progress >= 1) {
+        this.progress = 1;
+        clearInterval(this.showTime);
+      }
+    }, 20);
+  }
+
+  hide() {
+    this.clearTimer();
+
+    this.hideTime = setInterval(() => {
+      const material = this.mesh.material;
+
+      this.progress -= 0.05;
+      material.uniforms.uProgress.value = this.progress;
+
+      if (this.progress <= 0) {
+        this.progress = 0;
+        clearInterval(this.hideTime);
+      }
+    }, 20);
+  }
+
+  clearTimer() {
+    if (this.hideTime) {
+      clearInterval(this.hideTime);
+    }
+
+    if (this.showTime) {
+      clearInterval(this.showTime);
+    }
+  }
+}

+ 319 - 0
packages/pc/src/components/Game6/components/Model.vue

@@ -0,0 +1,319 @@
+<template>
+  <div ref="container" class="model-container">
+    <div v-if="showProgress" class="loading-overlay">
+      <div class="loading-box">
+        <div class="bar">
+          <div class="fill" :style="{ width: progress + '%' }"></div>
+        </div>
+        <p class="percent">{{ progress }}%</p>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onUnmounted } from "vue";
+import * as THREE from "three";
+import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader";
+import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
+import { TagModel } from "../TagModel";
+import TagChonger from "../images/img_chonger.png";
+import TagGuanwu from "../images/img_guanwu.png";
+import TagJinchi from "../images/img_jinchi.png";
+import TagJinzan from "../images/img_jinzan.png";
+import TagLiu from "../images/img_liu.png";
+import TagYanban from "../images/img_yanban.png";
+import TagYuheng from "../images/img_yuheng.png";
+
+const props = defineProps({
+  activeName: { type: String, default: "" },
+});
+const container = ref(null);
+
+const progress = ref(0);
+const showProgress = ref(true);
+const tagArr = [];
+
+let renderer, scene, camera, controls, mixer, clock, rafId;
+// 存放需要做呼吸灯效果的 mesh(冠武对应 fbx.children[0] 下的所有网格)
+let breathMeshes = [];
+
+// Scale and controls limits
+const INITIAL_SCALE_MULTIPLIER = 1.6;
+const MIN_SCALE = 0.5; // minimum model scale
+const MAX_SCALE = 4.0; // maximum model scale
+const CONTROLS_MIN_DISTANCE = 80; // min zoom distance
+const CONTROLS_MAX_DISTANCE = 900; // max zoom distance
+const MESH_MAP = {
+  冠武: [0],
+  九旒: [4],
+  綖板: [3],
+};
+
+function onWindowResize() {
+  if (!container.value || !renderer || !camera) return;
+  const w = container.value.clientWidth;
+  const h = container.value.clientHeight;
+  camera.aspect = w / h;
+  camera.updateProjectionMatrix();
+  renderer.setSize(w, h);
+}
+
+const handleTag = (visible) => {
+  if (!tagArr.length) {
+    const temp = [];
+    const tagStack = [
+      {
+        url: TagChonger,
+        width: 64,
+        height: 223,
+        position: new THREE.Vector3(35, -33, 7),
+        scale: 0.2,
+        rotation: { x: 0, y: 0, z: 0 },
+      },
+      {
+        url: TagGuanwu,
+        width: 64,
+        height: 223,
+        position: new THREE.Vector3(23, -40, 20),
+        scale: 0.2,
+        rotation: { x: 0, y: 0, z: 0 },
+      },
+      {
+        url: TagJinchi,
+        width: 64,
+        height: 320,
+        position: new THREE.Vector3(0, -43, 30),
+        scale: 0.2,
+        rotation: { x: 0, y: 0, z: 0 },
+      },
+      {
+        url: TagJinzan,
+        width: 216,
+        height: 56,
+        position: new THREE.Vector3(67, 16, 5),
+        scale: 0.2,
+        rotation: { x: 0, y: 0, z: 0 },
+      },
+      {
+        url: TagLiu,
+        width: 101,
+        height: 56,
+        position: new THREE.Vector3(-45, -3, 71),
+        scale: 0.2,
+        rotation: { x: 0, y: 0, z: 0 },
+      },
+      {
+        url: TagYanban,
+        width: 64,
+        height: 139,
+        position: new THREE.Vector3(0, 47, 30),
+        scale: 0.2,
+        rotation: { x: 0, y: 0, z: 0 },
+      },
+      {
+        url: TagYuheng,
+        width: 64,
+        height: 139,
+        position: new THREE.Vector3(35, 43, 5),
+        scale: 0.2,
+        rotation: { x: 0, y: 0, z: 0 },
+      },
+    ];
+
+    tagStack.forEach((tag) => {
+      const tagIns = new TagModel(tag);
+
+      temp.push(tagIns);
+      scene?.add(tagIns.mesh);
+    });
+
+    tagArr.push(...temp);
+  }
+
+  tagArr.forEach((tag) => {
+    visible ? tag.show() : tag.hide();
+  });
+};
+
+onMounted(() => {
+  clock = new THREE.Clock();
+
+  scene = new THREE.Scene();
+
+  const w = container.value.clientWidth || 400;
+  const h = container.value.clientHeight || 300;
+  camera = new THREE.PerspectiveCamera(45, w / h, 0.1, 2000);
+  camera.position.set(160, 60, 300);
+
+  renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
+  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
+  renderer.setSize(w, h);
+  renderer.outputEncoding = THREE.sRGBEncoding;
+  container.value.appendChild(renderer.domElement);
+
+  const ambient = new THREE.AmbientLight(0xffffff, 1.0);
+  scene.add(ambient);
+
+  const dir = new THREE.DirectionalLight(0xffffff, 0.8);
+  dir.position.set(100, 200, 100);
+  scene.add(dir);
+
+  controls = new OrbitControls(camera, renderer.domElement);
+  controls.enableDamping = true;
+  controls.target.set(0, 0, 0);
+  controls.minDistance = CONTROLS_MIN_DISTANCE;
+  controls.maxDistance = CONTROLS_MAX_DISTANCE;
+
+  const loader = new FBXLoader();
+  loader.load(
+    encodeURI("./model/13.398九旒冕.fbx"),
+    (fbx) => {
+      fbx.traverse((c) => {
+        if (c.isMesh) c.castShadow = true;
+      });
+
+      fbx.position.set(0, 0, 0);
+      fbx.rotation.set(0, 0, 0);
+      const box = new THREE.Box3().setFromObject(fbx);
+      const size = box.getSize(new THREE.Vector3()).length();
+      if (size > 0) {
+        const scale = 120 / size;
+        // apply an initial multiplier then clamp to allowed range
+        const target = Math.max(
+          MIN_SCALE,
+          Math.min(MAX_SCALE, scale * INITIAL_SCALE_MULTIPLIER),
+        );
+        fbx.scale.setScalar(target);
+      }
+
+      breathMeshes.push(fbx.children[0]);
+
+      scene.add(fbx);
+
+      if (fbx.animations && fbx.animations.length) {
+        mixer = new THREE.AnimationMixer(fbx);
+        fbx.animations.forEach((clip) => mixer.clipAction(clip).play());
+      }
+
+      progress.value = 100;
+      // hide after a short delay so user sees 100%
+      setTimeout(() => {
+        showProgress.value = false;
+        handleTag(true);
+      }, 400);
+    },
+    // onProgress
+    (xhr) => {
+      if (xhr && xhr.lengthComputable) {
+        const p = Math.round((xhr.loaded / xhr.total) * 100);
+        progress.value = Math.min(100, Math.max(0, p));
+      }
+    },
+    // onError
+    (err) => {
+      console.error("FBX load error", err);
+      showProgress.value = false;
+    },
+  );
+
+  function animate() {
+    const delta = clock.getDelta();
+    if (mixer) mixer.update(delta);
+
+    // 呼吸灯效果
+    if (breathMeshes && breathMeshes.length) {
+      const t = clock.getElapsedTime();
+
+      breathMeshes.forEach((mesh) => {
+        let materials = mesh.material;
+        if (!materials) return;
+        if (!Array.isArray(materials)) materials = [materials];
+
+        materials.forEach((mat, index) => {
+          if (!mat) return;
+          // 使用 emissive/emissiveIntensity 做发光呼吸
+          if (!mat.emissive) {
+            mat.emissive = new THREE.Color(0x000000);
+          }
+          if (
+            Array.isArray(MESH_MAP[props.activeName]) &&
+            MESH_MAP[props.activeName].includes(index)
+          ) {
+            mat.emissive.setHex(0xffcc66);
+            mat.emissiveIntensity = 0.4 + 0.3 * Math.sin(t * 2.5);
+          } else {
+            mat.emissiveIntensity = 0;
+          }
+        });
+      });
+    }
+
+    controls.update();
+    renderer.render(scene, camera);
+    rafId = requestAnimationFrame(animate);
+  }
+
+  animate();
+  window.addEventListener("resize", onWindowResize);
+});
+
+onUnmounted(() => {
+  window.removeEventListener("resize", onWindowResize);
+  if (rafId) cancelAnimationFrame(rafId);
+  if (controls) controls.dispose();
+  if (mixer) mixer.stopAllAction();
+  if (renderer) {
+    renderer.forceContextLoss();
+    const canvas = renderer.domElement;
+    if (canvas && canvas.parentNode) canvas.parentNode.removeChild(canvas);
+    renderer.dispose();
+  }
+});
+</script>
+
+<style scoped>
+.model-container {
+  width: 100%;
+  height: 100%;
+  display: block;
+  position: relative;
+}
+
+.loading-overlay {
+  position: absolute;
+  inset: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: rgba(0, 0, 0, 0.35);
+  z-index: 40;
+}
+
+.loading-box {
+  width: 70%;
+  max-width: 420px;
+  text-align: center;
+  color: #fff;
+}
+
+.bar {
+  height: 10px;
+  width: 100%;
+  background: rgba(255, 255, 255, 0.15);
+  border-radius: 6px;
+  overflow: hidden;
+}
+
+.fill {
+  height: 100%;
+  background: linear-gradient(90deg, #ffd37a, #ff9e5a);
+  width: 0%;
+  transition: width 120ms linear;
+}
+
+.percent {
+  margin-top: 8px;
+  font-size: 13px;
+}
+</style>

BIN
packages/pc/src/components/Game6/images/biaoqian_left.png


BIN
packages/pc/src/components/Game6/images/btn_small_active.png


BIN
packages/pc/src/components/Game6/images/btn_small_noraml.png


BIN
packages/pc/src/components/Game6/images/img_chonger.png


BIN
packages/pc/src/components/Game6/images/img_guanwu.png


BIN
packages/pc/src/components/Game6/images/img_jinchi.png


BIN
packages/pc/src/components/Game6/images/img_jinzan.png


BIN
packages/pc/src/components/Game6/images/img_liu.png


BIN
packages/pc/src/components/Game6/images/img_yanban.png


BIN
packages/pc/src/components/Game6/images/img_yuheng.png


+ 89 - 0
packages/pc/src/components/Game6/index.scss

@@ -0,0 +1,89 @@
+@use "../../assets/utils.scss";
+
+.game6 {
+  position: fixed;
+  inset: 0;
+  height: 100%;
+  overflow: hidden;
+  padding: 0 80px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: rgba(0, 0, 0, 0.7);
+  backdrop-filter: blur(10px);
+  z-index: 999;
+
+  &-iframe {
+    flex: 1;
+    padding: utils.vh-calc(80) 0;
+    height: 100%;
+  }
+  &-content {
+    margin: 0 50px;
+    flex: 0 0 380px;
+
+    &-title {
+      position: relative;
+      margin-bottom: utils.vh-calc(20);
+      display: inline-block;
+      font-size: utils.vh-calc(40);
+      line-height: 1.75;
+      letter-spacing: 5px;
+      color: #ffffff;
+
+      &::after {
+        content: "";
+        position: absolute;
+        left: 50%;
+        bottom: 0;
+        width: 80%;
+        height: utils.vh-calc(4);
+        transform: translateX(-50%);
+        background: url("../Game2/images/img_line_01.png") repeat center / cover;
+      }
+      &::before {
+        content: "";
+        position: absolute;
+        top: 0;
+        right: utils.vh-calc(-5);
+        width: utils.vh-calc(32);
+        height: utils.vh-calc(27);
+        background: url("../Game2/images/Subtract.png") no-repeat center /
+          contain;
+      }
+    }
+    &-age {
+      margin: 0 0 utils.vh-calc(5);
+      font-size: utils.vh-calc(20);
+      color: #efdbac;
+    }
+    &-desc {
+      font-size: utils.vh-calc(20);
+      line-height: 1.5;
+      letter-spacing: 4px;
+      color: #ffffff;
+    }
+  }
+  &-list {
+    display: flex;
+    flex-direction: column;
+    gap: utils.vh-calc(45);
+
+    li {
+      width: utils.vh-calc(189);
+      height: utils.vh-calc(48);
+      text-align: center;
+      line-height: utils.vh-calc(48);
+      font-size: utils.vh-calc(20);
+      color: #cdad6e;
+      cursor: pointer;
+      transition: all linear 0.2s;
+      background: url("./images/btn_small_noraml.png") no-repeat center / cover;
+
+      &.active {
+        color: #efdbac;
+        background-image: url("./images/btn_small_active.png");
+      }
+    }
+  }
+}

Файловите разлики са ограничени, защото са твърде много
+ 68 - 0
packages/pc/src/components/Game6/index.vue


BIN
packages/pc/src/components/Tabbar/images/icon-1.png


BIN
packages/pc/src/components/Tabbar/images/icon-2.png


BIN
packages/pc/src/components/Tabbar/images/icon-3.png


BIN
packages/pc/src/components/Tabbar/images/icon_luwang_active.png


+ 7 - 5
packages/pc/src/components/Tabbar/index.vue

@@ -6,7 +6,7 @@
     <li
       v-for="(item, index) in FOOTER_LIST"
       :key="index"
-      @click="handleFooter(item.path)"
+      @click="handleFooter(item)"
     >
       <img :src="item.icon" />
     </li>
@@ -24,7 +24,7 @@ const router = useRouter();
 const FOOTER_LIST = [
   {
     icon: Icon1,
-    path: "",
+    link: "https://houseoss.4dkankan.com/project/shandongMuseum/mztm/scene/index.html#/?m=SG-hDFyy3xb7ri-04",
   },
   {
     icon: Icon2,
@@ -36,9 +36,11 @@ const FOOTER_LIST = [
   },
 ];
 
-const handleFooter = (path) => {
-  if (path) {
-    router.push(path);
+const handleFooter = (item) => {
+  if (item.link) {
+    window.location.href = item.link;
+  } else if (item.path) {
+    router.push(item.path);
   }
 };
 </script>

+ 7 - 0
packages/pc/src/views/Collection/constants.ts

@@ -20,6 +20,7 @@ export const CYFP_LIST = [
     img1: `${BASE_URL}images/500/九旒冕.png`,
     img2: `${BASE_URL}images/500/九旒冕-线稿.png`,
     imgList: [`${BASE_URL}九旒冕.png`],
+    model: `${BASE_URL}model/13.398九旒冕.fbx`,
     name: "九旒冕",
     desc2: "冕体为细竹丝编制的六边形网格状冠胎,原表敷漆纱。",
     age: "明洪武",
@@ -31,6 +32,7 @@ export const CYFP_LIST = [
     img1: `${BASE_URL}images/500/戗金云龙纹朱漆木箱.png`,
     img2: `${BASE_URL}images/500/戗金云龙纹朱漆木箱-线稿.png`,
     imgList: [`${BASE_URL}戗金云龙纹朱漆木箱.png`],
+    model: `${BASE_URL}model/戗金云龙纹朱漆木匣/12.34戗金云龙纹朱漆木匣.FBX`,
     name: "戗金云龙纹朱漆木匣",
     desc2: "箱内分三层,上部为一朱漆木盒,上扣盝顶箱盖",
     desc: `
@@ -53,6 +55,7 @@ export const CYFP_LIST = [
     img2: `${BASE_URL}images/500/白玉圭-线稿.png`,
     imgList: [`${BASE_URL}白玉圭.png`],
     name: "白玉圭",
+    model: `${BASE_URL}model/白玉圭/2.937白玉圭.fbx`,
     desc2: "玉圭是亲王穿冕服、皮弁服时手持的礼器。",
     desc: `
       <p>玉圭是亲王穿冕服、皮弁服时手持的礼器。此白玉圭为扁平状,厚薄均匀,上端突起作尖角形,边缘折角平直。玉质莹润,表面磨制光滑。出土时盛于戗金云龙纹朱漆木匣内。</p>
@@ -62,6 +65,7 @@ export const CYFP_LIST = [
     img1: `${BASE_URL}images/500/镶宝石金带饰.png`,
     img2: `${BASE_URL}images/500/镶宝石金带饰-线稿.png`,
     imgList: [`${BASE_URL}镶宝石金带饰.png`],
+    model: `${BASE_URL}model/镶宝石金带饰/东7.258.fbx.FBX`,
     name: "镶宝石金带饰",
     desc2: "金质托体,整体呈如意云头形。",
     desc: `
@@ -135,6 +139,7 @@ export const ARYZ_LIST = [
     img2: `${BASE_URL}images/500/白玉花型杯-线稿.png`,
     imgList: [`${BASE_URL}白玉花型杯.png`],
     name: "白玉花形杯",
+    model: `${BASE_URL}model/白玉花形杯/2.940白玉花形杯.fbx`,
     desc2: "白玉花形杯杯体为和田白玉,采用圆雕与镂雕相结合的手法",
     desc: `
       <p>白玉花形杯杯体为和田白玉,采用圆雕与镂雕相结合的手法,将杯子雕刻成一朵盛开的花朵,五瓣相连,边缘叠压,内底中心浮雕的五瓣小花为蕊。杯柄和杯托镂雕成曼妙相连的玉枝玉叶,叶脉舒展,纹络清晰可见。这件玉杯应为玉洗,玉质细腻莹润,雕工精细,造型典雅优美,为我国古代玉笔洗之佳品。</p>
@@ -144,6 +149,7 @@ export const ARYZ_LIST = [
     img1: `${BASE_URL}images/500/水晶鹿镇纸.png`,
     img2: `${BASE_URL}images/500/水晶鹿镇纸-线稿.png`,
     imgList: [`${BASE_URL}水晶鹿镇纸.png`],
+    model: `${BASE_URL}model/水晶鹿镇纸/2.938.FBX`,
     name: "水晶鹿镇纸",
     desc2: "",
     desc: `
@@ -159,6 +165,7 @@ export const LFKB_LIST = [
     img1: `${BASE_URL}images/500/青白釉云龙纹盖罐.png`,
     img2: `${BASE_URL}images/500/青白釉云龙纹盖罐-线稿.png`,
     imgList: [`${BASE_URL}青白釉云龙纹盖罐.png`],
+    model: `${BASE_URL}model/青白釉云龙纹盖罐/5.1053.fbx.FBX`,
     name: "青白釉云龙纹盖罐",
     desc2: "圆唇,直口,短颈,圆鼓腹,平底,矮圈足。盖呈倒荷叶形,宝珠纽。",
     desc: `

+ 6 - 1
packages/pc/src/views/Collection/detail.scss

@@ -17,6 +17,10 @@
     &.collapsed {
       width: 100%;
     }
+    &-model {
+      width: 100%;
+      height: 100%;
+    }
     &-swiper {
       width: 100%;
       height: 100%;
@@ -62,7 +66,8 @@
       utils.vh-calc(80);
     width: utils.vh-calc(517);
     background: url("./images/img_pop.png") no-repeat center left / cover;
-    transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
+    transition:
+      transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
       padding 0.3s cubic-bezier(0.4, 0, 0.2, 1);
     z-index: 1;
 

+ 36 - 6
packages/pc/src/views/Collection/detail.vue

@@ -5,6 +5,7 @@
     <div class="detail-content" :class="{ collapsed: isCollapsed }">
       <!-- 图片模式 -->
       <Swiper
+        v-if="curMode === 'img'"
         class="detail-content-swiper"
         :modules="[Pagination]"
         :pagination="{ clickable: true }"
@@ -23,20 +24,41 @@
         </SwiperSlide>
       </Swiper>
 
+      <!-- 三维模型 -->
+      <iframe
+        v-if="curMode === 'model'"
+        id="modelIframe"
+        class="detail-content-model"
+        :src="
+          detail.modelLink ||
+          `modelLook/index.html?src=${detail.model}&min=0.3&max=3`
+        "
+        frameBorder="0"
+        title="model"
+      ></iframe>
+
       <ul class="detail-tools">
         <li>
-          <img v-if="curMode !== 'img'" src="./images/icon_img.png" />
-          <img v-else-if="detail?.model" src="./images/icon_3d.png" />
+          <img
+            v-if="curMode !== 'img'"
+            src="./images/icon_img.png"
+            @click="curMode = 'img'"
+          />
+          <img
+            v-else-if="detail?.model"
+            src="./images/icon_3d.png"
+            @click="curMode = 'model'"
+          />
         </li>
         <template v-if="curMode === 'model'">
           <li>
-            <img src="./images/Frame.png" />
+            <img src="./images/Frame.png" @click="handleMode('zoomIn')" />
           </li>
           <li>
-            <img src="./images/Frame-1.png" />
+            <img src="./images/Frame-1.png" @click="handleMode('zoomOut')" />
           </li>
           <li>
-            <img src="./images/Frame-2.png" />
+            <img src="./images/Frame-2.png" @click="handleMode('resetView')" />
           </li>
         </template>
       </ul>
@@ -69,11 +91,19 @@ const LIST = [CYFP_LIST, ARYZ_LIST, LFKB_LIST];
 const detail = LIST[route.params.id - 1][route.params.sid];
 
 const isCollapsed = ref(false);
-const curMode = ref("img");
+const curMode = ref(detail?.model || detail?.modelLink ? "model" : "img");
 
 const toggleSidebar = () => {
   isCollapsed.value = !isCollapsed.value;
 };
+
+const handleMode = (fuName) => {
+  const dom = document.querySelector("#modelIframe");
+  console.log(dom.contentWindow);
+  if (dom && dom.contentWindow && dom.contentWindow.sceneFc[fuName]) {
+    dom.contentWindow.sceneFc[fuName]();
+  }
+};
 </script>
 
 <style scoped lang="scss">

+ 25 - 0
packages/pc/src/views/Scroll/index.scss

@@ -1,5 +1,14 @@
 @use "@/assets/utils.scss";
 
+.scroll-tabbar {
+  left: utils.vh-calc(120);
+  gap: 30px;
+
+  li {
+    width: utils.vh-calc(75);
+    height: utils.vh-calc(88);
+  }
+}
 .scroll {
   --design-height: 1040;
   position: absolute;
@@ -24,6 +33,22 @@
       color: #47342a;
       text-indent: 2em;
 
+      .model1 {
+        position: relative;
+
+        .hot1 {
+          top: utils.vh-calc(280);
+          left: utils.vh-calc(870);
+        }
+        .hot2 {
+          top: utils.vh-calc(440);
+          left: utils.vh-calc(880);
+        }
+        .hot3 {
+          top: utils.vh-calc(650);
+          left: utils.vh-calc(760);
+        }
+      }
       .imgBg6 {
         position: absolute;
         top: utils.vh-calc(-25);

+ 32 - 1
packages/pc/src/views/Scroll/index.vue

@@ -43,6 +43,31 @@
             x-webkit-airplay="true"
             x5-video-player-type="h5-page"
           ></video>
+          <img
+            class="hot hot1"
+            src="./images/point-min.png"
+            @click="showGame6 = true"
+          />
+          <img
+            class="hot hot2"
+            src="./images/point-min.png"
+            @click="
+              $router.push({
+                name: 'collectionDetail',
+                params: { id: 1, sid: 7 },
+              })
+            "
+          />
+          <img
+            class="hot hot3"
+            src="./images/point-min.png"
+            @click="
+              $router.push({
+                name: 'collectionDetail',
+                params: { id: 1, sid: 8 },
+              })
+            "
+          />
         </div>
 
         <img class="title2" v-lazy="imgTitle2" />
@@ -244,13 +269,17 @@
     <Game5 v-if="showGame5" @close="showGame5 = false" />
   </Transition>
 
+  <Transition name="fade">
+    <Game6 v-if="showGame6" @close="showGame6 = false" />
+  </Transition>
+
   <VideoAni
     v-if="showVideo"
     :src="mediaUrlMap.get('3')"
     @ended="$router.push({ name: 'end' })"
   />
 
-  <Tabbar />
+  <Tabbar class="scroll-tabbar" />
 </template>
 
 <script setup>
@@ -283,6 +312,7 @@ import Game2 from "@/components/Game2/index.vue";
 import Game3 from "@/components/Game3/index.vue";
 import Game4 from "@/components/Game4/index.vue";
 import Game5 from "@/components/Game5/index.vue";
+import Game6 from "@/components/Game6/index.vue";
 import Tabbar from "@/components/Tabbar/index.vue";
 import VideoAni from "@/components/VideoAni/index.vue";
 
@@ -296,6 +326,7 @@ const showGame2 = ref(false);
 const showGame3 = ref(false);
 const showGame4 = ref(false);
 const showGame5 = ref(false);
+const showGame6 = ref(false);
 
 const handleSaveType = (type) => {
   model1Type.value = type + 1;

Файловите разлики са ограничени, защото са твърде много
+ 2152 - 1986
pnpm-lock.yaml