chenlei 1 month ago
commit
77ec2f9bda
100 changed files with 2546 additions and 0 deletions
  1. 22 0
      .gitignore
  2. 2 0
      .npmrc
  3. 8 0
      .prettierignore
  4. 3 0
      .vscode/extensions.json
  5. 10 0
      .vscode/settings.json
  6. 1 0
      README.md
  7. 16 0
      package.json
  8. 18 0
      packages/base/package.json
  9. 162 0
      packages/base/src/apng.js
  10. 3 0
      packages/base/src/index.js
  11. 217 0
      packages/base/src/usePreloader.js
  12. 27 0
      packages/base/src/utils.js
  13. BIN
      packages/base/story/刘云彪.png
  14. BIN
      packages/base/story/半个荞面馍故事场景画.png
  15. BIN
      packages/base/story/梁兴初.png
  16. BIN
      packages/base/story/毛泽东与陕义堂阿訇.png
  17. BIN
      packages/base/story/第一支骑兵侦察部队.png
  18. BIN
      packages/base/story/老红军范昌标晚年留影.png
  19. 30 0
      packages/mobile/.gitignore
  20. 13 0
      packages/mobile/index.html
  21. 8 0
      packages/mobile/jsconfig.json
  22. 30 0
      packages/mobile/package.json
  23. 22 0
      packages/mobile/postcss.config.js
  24. BIN
      packages/mobile/public/favicon.ico
  25. 16 0
      packages/mobile/src/App.vue
  26. BIN
      packages/mobile/src/assets/fonts/SOURCEHANSERIFCN-BOLD.OTF
  27. BIN
      packages/mobile/src/assets/fonts/SOURCEHANSERIFCN-REGULAR.OTF
  28. 112 0
      packages/mobile/src/assets/main.css
  29. 7 0
      packages/mobile/src/assets/utils.scss
  30. 20 0
      packages/mobile/src/main.js
  31. 14 0
      packages/mobile/src/router/index.js
  32. 12 0
      packages/mobile/src/stores/counter.js
  33. 0 0
      packages/mobile/src/views/Home/index.scss
  34. 15 0
      packages/mobile/src/views/Home/index.vue
  35. 29 0
      packages/mobile/vite.config.js
  36. 30 0
      packages/pc/.gitignore
  37. 13 0
      packages/pc/index.html
  38. 8 0
      packages/pc/jsconfig.json
  39. 32 0
      packages/pc/package.json
  40. 23 0
      packages/pc/postcss.config.js
  41. BIN
      packages/pc/public/favicon.ico
  42. 23 0
      packages/pc/src/App.vue
  43. 7 0
      packages/pc/src/assets/elements.scss
  44. BIN
      packages/pc/src/assets/fonts/SOURCEHANSERIFCN-BOLD.OTF
  45. BIN
      packages/pc/src/assets/fonts/SOURCEHANSERIFCN-REGULAR.OTF
  46. BIN
      packages/pc/src/assets/images/back-min.png
  47. BIN
      packages/pc/src/assets/images/bg-min.jpg
  48. BIN
      packages/pc/src/assets/images/logo-min.png
  49. 111 0
      packages/pc/src/assets/main.css
  50. 7 0
      packages/pc/src/assets/utils.scss
  51. 36 0
      packages/pc/src/components/VideoPopup.vue
  52. 18 0
      packages/pc/src/main.js
  53. 39 0
      packages/pc/src/router/index.js
  54. 26 0
      packages/pc/src/stores/base.js
  55. 12 0
      packages/pc/src/stores/counter.js
  56. 130 0
      packages/pc/src/views/Antique/components/Popup.vue
  57. 40 0
      packages/pc/src/views/Antique/constants.js
  58. BIN
      packages/pc/src/views/Antique/images/close.png
  59. BIN
      packages/pc/src/views/Antique/images/item-bg-min.png
  60. BIN
      packages/pc/src/views/Antique/images/item-hover-min.png
  61. BIN
      packages/pc/src/views/Antique/images/left.png
  62. BIN
      packages/pc/src/views/Antique/images/right.png
  63. BIN
      packages/pc/src/views/Antique/images/search-bg.png
  64. BIN
      packages/pc/src/views/Antique/images/search-close.png
  65. BIN
      packages/pc/src/views/Antique/images/search-min.png
  66. BIN
      packages/pc/src/views/Antique/images/tab-bg-min.png
  67. BIN
      packages/pc/src/views/Antique/images/tab1-active-min.png
  68. BIN
      packages/pc/src/views/Antique/images/tab1-min.png
  69. BIN
      packages/pc/src/views/Antique/images/tab2-active-min.png
  70. BIN
      packages/pc/src/views/Antique/images/tab2-min.png
  71. 178 0
      packages/pc/src/views/Antique/index.scss
  72. 75 0
      packages/pc/src/views/Antique/index.vue
  73. 81 0
      packages/pc/src/views/History/components/Audio.vue
  74. 48 0
      packages/pc/src/views/History/components/Doc.vue
  75. 48 0
      packages/pc/src/views/History/components/Image.vue
  76. 41 0
      packages/pc/src/views/History/components/Panel.vue
  77. 77 0
      packages/pc/src/views/History/components/Video.vue
  78. 47 0
      packages/pc/src/views/History/constants.js
  79. BIN
      packages/pc/src/views/History/images/bg-min.png
  80. BIN
      packages/pc/src/views/History/images/pause-icon-min.png
  81. BIN
      packages/pc/src/views/History/images/play-icon-min.png
  82. BIN
      packages/pc/src/views/History/images/play-min.png
  83. BIN
      packages/pc/src/views/History/images/tab1-active-min.png
  84. BIN
      packages/pc/src/views/History/images/tab1-min.png
  85. BIN
      packages/pc/src/views/History/images/tab2-active-min.png
  86. BIN
      packages/pc/src/views/History/images/tab2-min.png
  87. BIN
      packages/pc/src/views/History/images/tab3-active-min.png
  88. BIN
      packages/pc/src/views/History/images/tab3-min.png
  89. BIN
      packages/pc/src/views/History/images/tab4-active-min.png
  90. BIN
      packages/pc/src/views/History/images/tab4-min.png
  91. 76 0
      packages/pc/src/views/History/index.scss
  92. 139 0
      packages/pc/src/views/History/index.vue
  93. BIN
      packages/pc/src/views/Home/components/Biographies/images/bg-min.jpg
  94. BIN
      packages/pc/src/views/Home/components/Biographies/images/title-min.png
  95. 90 0
      packages/pc/src/views/Home/components/Biographies/index.vue
  96. 159 0
      packages/pc/src/views/Home/components/SharePopup.vue
  97. 115 0
      packages/pc/src/views/Home/constants.js
  98. BIN
      packages/pc/src/views/Home/images/bg-min.jpg
  99. BIN
      packages/pc/src/views/Home/images/c1-min.png
  100. 0 0
      packages/pc/src/views/Home/images/c2-min.png

+ 22 - 0
.gitignore

@@ -0,0 +1,22 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+node_modules
+npm-debug.log*
+yarn-error.log
+yarn.lock
+package-lock.json
+
+# production
+/es
+docs-dist
+
+# misc
+.DS_Store
+
+# ide
+/.idea
+
+dist
+dist-node
+build

+ 2 - 0
.npmrc

@@ -0,0 +1,2 @@
+registry=https://registry.npmmirror.com/
+@dage:registry=http://192.168.20.245:4873/

+ 8 - 0
.prettierignore

@@ -0,0 +1,8 @@
+build/
+public/
+**/*.png
+**/*.svg
+**/*.jpg
+.DS_Store
+.history
+package.json

+ 3 - 0
.vscode/extensions.json

@@ -0,0 +1,3 @@
+{
+  "recommendations": ["Vue.volar"]
+}

+ 10 - 0
.vscode/settings.json

@@ -0,0 +1,10 @@
+{
+  "explorer.fileNesting.enabled": true,
+  "explorer.fileNesting.patterns": {
+    "tsconfig.json": "tsconfig.*.json, env.d.ts",
+    "vite.config.*": "jsconfig*, vitest.config.*, cypress.config.*, playwright.config.*",
+    "package.json": "package-lock.json, pnpm*, .yarnrc*, yarn*, .eslint*, eslint*, .prettier*, prettier*, .editorconfig"
+  },
+  "editor.formatOnSave": true,
+  "editor.defaultFormatter": "esbenp.prettier-vscode"
+}

+ 1 - 0
README.md

@@ -0,0 +1 @@
+WHS2411062-1 贺龙纪念馆-红色基因库项目

+ 16 - 0
package.json

@@ -0,0 +1,16 @@
+{
+  "name": "guyuan-museum-web",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "pinia": "^2.2.6",
+    "tslib": "^2.8.0"
+  }
+}

+ 18 - 0
packages/base/package.json

@@ -0,0 +1,18 @@
+{
+  "name": "@guyuan/base",
+  "version": "1.0.0",
+  "description": "",
+  "sideEffects": false,
+  "module": "src/index.js",
+  "main": "src/index.js",
+  "files": [
+    "src"
+  ],
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "apng-js": "^1.1.5",
+    "lodash": "^4.17.21"
+  }
+}

+ 162 - 0
packages/base/src/apng.js

@@ -0,0 +1,162 @@
+import parseAPNG from "apng-js";
+
+export function getImgBuffer(url) {
+  return new Promise(async (resolve) => {
+    const blob = await fetch(url).then((res) => res.blob());
+    const reader = new FileReader();
+    reader.readAsArrayBuffer(blob);
+    reader.onload = () => {
+      resolve(reader.result);
+    };
+  });
+}
+
+export async function getApng(url, options = {}) {
+  const imgBuffer = await getImgBuffer(url);
+  const apng = parseAPNG(imgBuffer);
+  Object.keys(options).forEach((key) => {
+    apng[key] = options[key];
+  });
+  return apng;
+}
+
+// 计算适配后的绘制区域
+function getDrawRect(
+  imageWidth,
+  imageHeight,
+  canvasWidth,
+  canvasHeight,
+  objectFit = "cover"
+) {
+  const imageRatio = imageWidth / imageHeight;
+  const canvasRatio = canvasWidth / canvasHeight;
+  let drawWidth,
+    drawHeight,
+    offsetX = 0,
+    offsetY = 0;
+
+  switch (objectFit) {
+    case "cover":
+      if (imageRatio > canvasRatio) {
+        drawWidth = canvasWidth;
+        drawHeight = canvasWidth / imageRatio;
+        offsetY = (canvasHeight - drawHeight) / 2;
+      } else {
+        drawHeight = canvasHeight;
+        drawWidth = canvasHeight * imageRatio;
+        offsetX = (canvasWidth - drawWidth) / 2;
+      }
+      break;
+    case "contain":
+      if (imageRatio > canvasRatio) {
+        drawHeight = canvasHeight;
+        drawWidth = canvasHeight * imageRatio;
+        offsetX = (canvasWidth - drawWidth) / 2;
+      } else {
+        drawWidth = canvasWidth;
+        drawHeight = canvasWidth / imageRatio;
+        offsetY = (canvasHeight - drawHeight) / 2;
+      }
+      break;
+    case "fill":
+      drawWidth = canvasWidth;
+      drawHeight = canvasHeight;
+      break;
+    case "none":
+      drawWidth = imageWidth;
+      drawHeight = imageHeight;
+      offsetX = (canvasWidth - drawWidth) / 2;
+      offsetY = (canvasHeight - drawHeight) / 2;
+      break;
+    case "scale-down":
+      if (imageWidth > canvasWidth || imageHeight > canvasHeight) {
+        return getDrawRect(
+          imageWidth,
+          imageHeight,
+          canvasWidth,
+          canvasHeight,
+          "contain"
+        );
+      } else {
+        return getDrawRect(
+          imageWidth,
+          imageHeight,
+          canvasWidth,
+          canvasHeight,
+          "none"
+        );
+      }
+    default:
+      return getDrawRect(
+        imageWidth,
+        imageHeight,
+        canvasWidth,
+        canvasHeight,
+        "cover"
+      );
+  }
+
+  return {
+    x: offsetX,
+    y: offsetY,
+    width: drawWidth,
+    height: drawHeight,
+  };
+}
+
+export async function playApng(
+  apngFile,
+  canvasClass,
+  appendParent,
+  canvasWidth = 375,
+  canvasHeight = 375,
+  canvasStyles,
+  objectFit = "cover" // 新增objectFit参数,默认cover
+) {
+  let apngObj = null;
+  let apngPlayer = null;
+  apngObj = await getApng(apngFile);
+
+  if (!apngObj) {
+    return;
+  }
+
+  const container = document.getElementById(appendParent);
+  const canvas = document.createElement("canvas");
+  canvas.width = canvasWidth;
+  canvas.height = canvasHeight;
+  canvas.setAttribute("class", canvasClass);
+  Object.assign(canvas.style, canvasStyles);
+  container.appendChild(canvas);
+
+  const ctx = canvas.getContext("2d", { willReadFrequently: true });
+  apngPlayer = await apngObj.getPlayer(ctx);
+
+  // 重写绘制方法以支持objectFit
+  const originalDrawFrame = apngPlayer.drawFrame;
+  apngPlayer.drawFrame = function (frame, ctx) {
+    ctx.clearRect(0, 0, canvasWidth, canvasHeight);
+    const { x, y, width, height } = getDrawRect(
+      apngObj.width, // APNG原始宽度
+      apngObj.height, // APNG原始高度
+      canvasWidth,
+      canvasHeight,
+      objectFit
+    );
+    ctx.drawImage(frame.image, x, y, width, height);
+  };
+
+  apngPlayer.play();
+  apngPlayer.on("end", () => {
+    apngPlayer.play();
+  });
+
+  // 返回播放器实例,方便后续控制
+  return apngPlayer;
+}
+
+export default {
+  getImgBuffer,
+  getApng,
+  playApng,
+};

+ 3 - 0
packages/base/src/index.js

@@ -0,0 +1,3 @@
+export * from "./utils";
+export * from "./usePreloader";
+export { default as Apng } from "./apng";

+ 217 - 0
packages/base/src/usePreloader.js

@@ -0,0 +1,217 @@
+import { throttle } from "lodash";
+import { ref } from "vue";
+
+export const PRELOADER_STATUS = {
+  ERROR: "error",
+  LOADING: "loading",
+  WAITING: "waiting",
+  DONE: "done",
+  ABORT: "abort",
+};
+
+export const getMediaType = (url) => {
+  const MEDIA_TYPES = {
+    mp4: "video/mp4",
+    webm: "video/webm",
+    ogg: "video/ogg",
+    mp3: "audio/mp3",
+    wav: "audio/wav",
+  };
+  const urlExtension = url.split(".").pop()?.toLowerCase() || "";
+
+  if (MEDIA_TYPES.hasOwnProperty(urlExtension)) {
+    return MEDIA_TYPES[urlExtension];
+  } else {
+    return null;
+  }
+};
+
+export const parseUrlName = (url) => {
+  const regex = /\/([^/]+)\.\w+$/;
+  const match = url.match(regex);
+  if (match && match.length >= 2) {
+    return match[1].split(".")[0];
+  } else {
+    return "";
+  }
+};
+
+export function usePreloader(params) {
+  let fetchControllerStack = [];
+  const decimals = params.decimals ? params.decimals * 10 : 1;
+  const _list = params.list;
+  const _retry = params.retry ?? 3;
+
+  /**
+   * 总字节长度
+   */
+  let totalLength = 0;
+  const fileSizeStack = new Map();
+  /**
+   * 已接收字节长度
+   */
+  let downloadLength = 0;
+  /**
+   * 进度
+   */
+  const percent = ref("0");
+  const status = ref(PRELOADER_STATUS.WAITING);
+  /**
+   * 缓存的媒体资源本地url
+   */
+  const mediaUrlMap = new Map();
+
+  const setThrottlePercent = throttle((val) => (percent.value = val), 200);
+
+  const calculateSum = (map) => {
+    let sum = 0;
+    for (const value of map.values()) {
+      sum += value;
+    }
+    return sum;
+  };
+
+  const handlePreload = (url, retry = _retry, loadedLength = 0) => {
+    const controller = new AbortController();
+    const signal = controller.signal;
+    const mediaType = getMediaType(url);
+    let loadingLength = 0;
+
+    return new Promise((res, rej) => {
+      fetch(url, { signal })
+        .then((response) => {
+          const contentLength = Number(response.headers.get("Content-Length"));
+
+          fileSizeStack.set(url, contentLength);
+
+          if (response.ok) {
+            return response.body;
+          }
+        })
+        .then(async (body) => {
+          if (!body) {
+            rej(`request.body is not find`);
+            return;
+          }
+
+          let diffed = false;
+          const chunks = [];
+          const reader = body.getReader();
+          const loop = true;
+
+          while (loop) {
+            const { value, done } = await reader.read();
+
+            if (done) break;
+
+            loadingLength += value.length;
+            if (loadingLength >= loadedLength) {
+              if (!diffed && loadedLength) {
+                const diff = loadingLength - loadedLength;
+                console.log(diff);
+                downloadLength += diff;
+                diffed = true;
+              }
+
+              downloadLength += value.length;
+            }
+
+            if (mediaType) {
+              chunks.push(value);
+            }
+
+            if (fileSizeStack.size === _list.length) {
+              const sum = calculateSum(fileSizeStack);
+              const percent = Math.round(
+                (downloadLength / sum) * 100 * decimals
+              );
+
+              setThrottlePercent((percent / decimals).toFixed(params.decimals));
+            }
+          }
+
+          if (mediaType) {
+            const blob = new Blob(chunks, { type: mediaType });
+            mediaUrlMap.set(parseUrlName(url), URL.createObjectURL(blob));
+          }
+
+          res(true);
+        })
+        .catch((err) => {
+          if (err.name === "AbortError") {
+            rej(err);
+            return;
+          }
+
+          if (retry) {
+            console.log("尝试重新预加载");
+            setTimeout(() => {
+              handlePreload(url, retry - 1, loadingLength)
+                .then(res)
+                .catch(rej);
+            }, 2000);
+          } else {
+            if (mediaType) {
+              mediaUrlMap.set(parseUrlName(url), url);
+            }
+
+            rej(err);
+          }
+        });
+
+      fetchControllerStack.push(controller);
+    });
+  };
+
+  const resetParams = () => {
+    abort();
+
+    totalLength = 0;
+    downloadLength = 0;
+    fileSizeStack.clear();
+    mediaUrlMap.clear();
+    setThrottlePercent("0");
+  };
+
+  const start = () => {
+    if (status.value === PRELOADER_STATUS.LOADING) return;
+
+    if (status.value !== PRELOADER_STATUS.WAITING) {
+      resetParams();
+    }
+
+    status.value = PRELOADER_STATUS.LOADING;
+
+    Promise.all(_list.map((url) => handlePreload(url)))
+      .then(() => {
+        params.success?.();
+        status.value = PRELOADER_STATUS.DONE;
+      })
+      .catch((err) => {
+        if (err.name === "AbortError") return;
+
+        abort();
+        params.error?.(err);
+        status.value = PRELOADER_STATUS.ERROR;
+      });
+  };
+
+  const abort = () => {
+    fetchControllerStack.forEach((controller) => {
+      controller.abort();
+    });
+    fetchControllerStack = [];
+
+    status.value = PRELOADER_STATUS.ABORT;
+  };
+
+  return {
+    status,
+    percent,
+    totalLength,
+    downloadLength,
+    mediaUrlMap,
+    start,
+    abort,
+  };
+}

+ 27 - 0
packages/base/src/utils.js

@@ -0,0 +1,27 @@
+export const isDevelopment = import.meta.env.MODE === "development";
+
+export const BASE_URL = `${window.origin}/project/helong-memorial-hall`;
+
+export const getEnvImagePath = (path) => {
+  return isDevelopment
+    ? `http://192.168.0.18:8080${path}`
+    : `${BASE_URL}/base${path}`;
+};
+
+export function isMobile() {
+  const userAgent = navigator.userAgent.toLowerCase();
+  return /iphone|ipod|android|windows phone|blackberry|mobile/i.test(userAgent);
+}
+
+export function checkDeviceAndRedirect() {
+  const isMobileDevice = isMobile();
+  const currentPath = window.location.pathname;
+  const isInMobilePath = currentPath.includes("/mobile/");
+  const isInPCPath = currentPath.includes("/pc/");
+
+  if (isMobileDevice && !isInMobilePath) {
+    window.location.href = currentPath.replace("/pc/", "/mobile/");
+  } else if (!isMobileDevice && !isInPCPath) {
+    window.location.href = currentPath.replace("/mobile/", "/pc/");
+  }
+}

BIN
packages/base/story/刘云彪.png


BIN
packages/base/story/半个荞面馍故事场景画.png


BIN
packages/base/story/梁兴初.png


BIN
packages/base/story/毛泽东与陕义堂阿訇.png


BIN
packages/base/story/第一支骑兵侦察部队.png


BIN
packages/base/story/老红军范昌标晚年留影.png


+ 30 - 0
packages/mobile/.gitignore

@@ -0,0 +1,30 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+.DS_Store
+dist
+dist-ssr
+coverage
+*.local
+
+/cypress/videos/
+/cypress/screenshots/
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+*.tsbuildinfo

+ 13 - 0
packages/mobile/index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" href="/favicon.ico" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>宁夏固原博物馆-红色基因库项目</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.js"></script>
+  </body>
+</html>

+ 8 - 0
packages/mobile/jsconfig.json

@@ -0,0 +1,8 @@
+{
+  "compilerOptions": {
+    "paths": {
+      "@/*": ["./src/*"]
+    }
+  },
+  "exclude": ["node_modules", "dist"]
+}

+ 30 - 0
packages/mobile/package.json

@@ -0,0 +1,30 @@
+{
+  "name": "@guyuan/mobile",
+  "version": "0.0.0",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "@guyuan/base": "workspace:*",
+    "pinia": "^2.2.6",
+    "swiper": "^11.1.15",
+    "vant": "^4.9.15",
+    "vue": "^3.5.13",
+    "vue-router": "^4.4.5"
+  },
+  "devDependencies": {
+    "@vant/auto-import-resolver": "^1.2.1",
+    "@vitejs/plugin-vue": "^5.2.1",
+    "autoprefixer": "^10.4.20",
+    "postcss-px-to-viewport": "^1.1.1",
+    "sass": "^1.82.0",
+    "unplugin-auto-import": "^0.18.6",
+    "unplugin-vue-components": "^0.27.5",
+    "vite": "^6.0.1",
+    "vite-plugin-vue-devtools": "^7.6.5"
+  }
+}

+ 22 - 0
packages/mobile/postcss.config.js

@@ -0,0 +1,22 @@
+export default {
+  plugins: {
+    autoprefixer: {},
+    "postcss-px-to-viewport": {
+      unitToConvert: "px", // 需要转换的单位,默认为"px"
+      viewportWidth: 750, // 设计稿的视口宽度
+      unitPrecision: 5, // 单位转换后保留的精度
+      propList: ["*"], // 能转化为vw的属性列表
+      viewportUnit: "vw", // 希望使用的视口单位
+      fontViewportUnit: "vw", // 字体使用的视口单位
+      selectorBlackList: [], // 需要忽略的CSS选择器,不会转为视口单位,使用原有的px等单位。
+      minPixelValue: 1, // 设置最小的转换数值,如果为1的话,只有大于1的值会被转换
+      mediaQuery: true, // 媒体查询里的单位是否需要转换单位
+      replace: true, //  是否直接更换属性值,而不添加备用属性
+      exclude: /node_modules/i, // 忽略某些文件夹下的文件或特定文件,例如 'node_modules' 下的文件
+      include: undefined, // 如果设置了include,那将只有匹配到的文件才会被转换
+      landscape: false, // 是否添加根据 landscapeWidth 生成的媒体查询条件 @media (orientation: landscape)
+      landscapeUnit: "vw", // 横屏时使用的单位
+      landscapeWidth: 750, // 横屏时使用的视口宽度
+    },
+  },
+};

BIN
packages/mobile/public/favicon.ico


+ 16 - 0
packages/mobile/src/App.vue

@@ -0,0 +1,16 @@
+<script setup>
+import { RouterView } from "vue-router";
+</script>
+
+<template>
+  <RouterView />
+</template>
+
+<style>
+:root:root {
+  --van-primary-color: #981b23;
+  --van-base-font: "SourceHanSerifSC-Regular";
+  --design-width: 750;
+  --design-height: 1424;
+}
+</style>

BIN
packages/mobile/src/assets/fonts/SOURCEHANSERIFCN-BOLD.OTF


BIN
packages/mobile/src/assets/fonts/SOURCEHANSERIFCN-REGULAR.OTF


+ 112 - 0
packages/mobile/src/assets/main.css

@@ -0,0 +1,112 @@
+:root {
+  --z-index-normal: 1;
+  --z-index-top: 1000;
+  --z-index-popper: 2000;
+  --z-hot-popper: 3000;
+}
+
+body,
+ol,
+ul,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+p,
+th,
+td,
+dl,
+dd,
+form,
+fieldset,
+legend,
+input,
+textarea,
+select {
+  margin: 0;
+  padding: 0;
+}
+* {
+  box-sizing: border-box;
+  user-select: none;
+}
+body {
+  color: #333333;
+  text-align: justify;
+  font-family: "SourceHanSerifSC-Regular";
+  -webkit-tap-highlight-color: transparent;
+}
+a {
+  color: #fff;
+  cursor: pointer;
+  text-decoration: none;
+}
+em {
+  font-style: normal;
+}
+li {
+  list-style: none;
+}
+img {
+  border: 0;
+  vertical-align: middle;
+}
+table {
+  border-collapse: collapse;
+  border-spacing: 0;
+}
+p {
+  word-wrap: break-word;
+}
+iframe {
+  border: none;
+}
+
+@font-face {
+  font-family: "SourceHanSerifSC-Bold";
+  src: url("./fonts/SOURCEHANSERIFCN-BOLD.otf");
+}
+@font-face {
+  font-family: "SourceHanSerifSC-Regular";
+  src: url("./fonts/SOURCEHANSERIFCN-REGULAR.otf");
+}
+
+.limit-line {
+  display: -webkit-box;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  -webkit-line-clamp: 1;
+  -webkit-box-orient: vertical;
+  word-break: break-all;
+  word-wrap: break-word;
+}
+
+.line-2 {
+  -webkit-line-clamp: 2;
+}
+
+.line-3 {
+  -webkit-line-clamp: 3;
+}
+
+.hidden {
+  display: none !important;
+  visibility: hidden !important;
+}
+
+.darkGlass {
+  background-color: rgba(0, 0, 0, 0.5);
+}
+
+.message-outer {
+  position: absolute;
+  display: table;
+  height: 100%;
+  width: 100%;
+
+  * {
+    transition: all 0.3s;
+  }
+}

+ 7 - 0
packages/mobile/src/assets/utils.scss

@@ -0,0 +1,7 @@
+@function vh-calc($num) {
+  @return calc(100vh * ($num / var(--design-height)));
+}
+
+@function vw-calc($num) {
+  @return calc(100vw * ($num / var(--design-width)));
+}

+ 20 - 0
packages/mobile/src/main.js

@@ -0,0 +1,20 @@
+import "./assets/main.css";
+
+import { createApp } from "vue";
+import { createPinia } from "pinia";
+import { Lazyload } from "vant";
+
+import App from "./App.vue";
+import router from "./router";
+
+import { isDevelopment, checkDeviceAndRedirect } from "@guyuan/base";
+
+!isDevelopment && checkDeviceAndRedirect();
+
+const app = createApp(App);
+
+app.use(createPinia());
+app.use(router);
+app.use(Lazyload);
+
+app.mount("#app");

+ 14 - 0
packages/mobile/src/router/index.js

@@ -0,0 +1,14 @@
+import { createRouter, createWebHashHistory } from "vue-router";
+
+const router = createRouter({
+  history: createWebHashHistory(import.meta.env.BASE_URL),
+  routes: [
+    {
+      path: "/",
+      name: "home",
+      component: () => import("../views/Home/index.vue"),
+    },
+  ],
+});
+
+export default router;

+ 12 - 0
packages/mobile/src/stores/counter.js

@@ -0,0 +1,12 @@
+import { ref, computed } from 'vue'
+import { defineStore } from 'pinia'
+
+export const useCounterStore = defineStore('counter', () => {
+  const count = ref(0)
+  const doubleCount = computed(() => count.value * 2)
+  function increment() {
+    count.value++
+  }
+
+  return { count, doubleCount, increment }
+})

+ 0 - 0
packages/mobile/src/views/Home/index.scss


+ 15 - 0
packages/mobile/src/views/Home/index.vue

@@ -0,0 +1,15 @@
+<template>
+  <div v-if="!loading" class="loading"></div>
+
+  <div v-else class="home"></div>
+</template>
+
+<script setup>
+import { ref } from "vue";
+
+const loading = ref(false);
+</script>
+
+<style lang="scss" scoped>
+@use "./index.scss";
+</style>

+ 29 - 0
packages/mobile/vite.config.js

@@ -0,0 +1,29 @@
+import { fileURLToPath, URL } from "node:url";
+
+import { defineConfig } from "vite";
+import vue from "@vitejs/plugin-vue";
+import AutoImport from "unplugin-auto-import/vite";
+import Components from "unplugin-vue-components/vite";
+import { VantResolver } from "@vant/auto-import-resolver";
+
+// https://vite.dev/config/
+export default defineConfig({
+  base: "./",
+  server: {
+    host: "0.0.0.0",
+  },
+  plugins: [
+    vue(),
+    AutoImport({
+      resolvers: [VantResolver()],
+    }),
+    Components({
+      resolvers: [VantResolver()],
+    }),
+  ],
+  resolve: {
+    alias: {
+      "@": fileURLToPath(new URL("./src", import.meta.url)),
+    },
+  },
+});

+ 30 - 0
packages/pc/.gitignore

@@ -0,0 +1,30 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+.DS_Store
+dist
+dist-ssr
+coverage
+*.local
+
+/cypress/videos/
+/cypress/screenshots/
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+*.tsbuildinfo

+ 13 - 0
packages/pc/index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" href="/favicon.ico" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>宁夏固原博物馆-红色基因库项目</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.js"></script>
+  </body>
+</html>

+ 8 - 0
packages/pc/jsconfig.json

@@ -0,0 +1,8 @@
+{
+  "compilerOptions": {
+    "paths": {
+      "@/*": ["./src/*"]
+    }
+  },
+  "exclude": ["node_modules", "dist"]
+}

+ 32 - 0
packages/pc/package.json

@@ -0,0 +1,32 @@
+{
+  "name": "@guyuan/pc",
+  "version": "0.0.0",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "@element-plus/icons-vue": "^2.3.1",
+    "@guyuan/base": "workspace:*",
+    "@vueuse/core": "^12.0.0",
+    "autoprefixer": "^10.4.20",
+    "clipboard": "^2.0.11",
+    "element-plus": "^2.9.0",
+    "pinia": "^2.2.6",
+    "postcss-px-to-viewport": "^1.1.1",
+    "swiper": "^11.1.15",
+    "vue": "^3.5.13",
+    "vue-qrcode": "^2.2.2",
+    "vue-router": "^4.4.5"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-vue": "^5.2.1",
+    "sass": "^1.82.0",
+    "unplugin-auto-import": "^0.18.6",
+    "unplugin-vue-components": "^0.27.5",
+    "vite": "^6.0.1"
+  }
+}

+ 23 - 0
packages/pc/postcss.config.js

@@ -0,0 +1,23 @@
+export default {
+  plugins: {
+    autoprefixer: {},
+    "postcss-px-to-viewport": {
+      unitToConvert: "px", // 需要转换的单位,默认为"px"
+      viewportWidth: 1920, // 设计稿的视口宽度
+      unitPrecision: 5, // 单位转换后保留的精度
+      propList: ["*"], // 能转化为vw的属性列表
+      viewportUnit: "vw", // 希望使用的视口单位
+      fontViewportUnit: "vw", // 字体使用的视口单位
+      selectorBlackList: [], // 需要忽略的CSS选择器,不会转为视口单位,使用原有的px等单位。
+      minPixelValue: 1, // 设置最小的转换数值,如果为1的话,只有大于1的值会被转换
+      mediaQuery: true, // 媒体查询里的单位是否需要转换单位
+      replace: true, //  是否直接更换属性值,而不添加备用属性
+      exclude: undefined, // 忽略某些文件夹下的文件或特定文件,例如 'node_modules' 下的文件
+      include: undefined, // 如果设置了include,那将只有匹配到的文件才会被转换
+      landscape: false, // 是否添加根据 landscapeWidth 生成的媒体查询条件 @media (orientation: landscape)
+      landscapeUnit: "vw", // 横屏时使用的单位
+      landscapeWidth: 1920, // 横屏时使用的视口宽度
+      exclude: [/node_modules/],
+    },
+  },
+};

BIN
packages/pc/public/favicon.ico


+ 23 - 0
packages/pc/src/App.vue

@@ -0,0 +1,23 @@
+<script setup>
+import { RouterView } from "vue-router";
+</script>
+
+<template>
+  <RouterView />
+</template>
+
+<style>
+:root {
+  --design-width: 1920;
+  --design-height: 1080;
+}
+
+.el-image {
+  width: 100%;
+  height: 100%;
+}
+
+#app {
+  font-size: 18px;
+}
+</style>

+ 7 - 0
packages/pc/src/assets/elements.scss

@@ -0,0 +1,7 @@
+@forward "element-plus/theme-chalk/src/common/var.scss" with (
+  $colors: (
+    "primary": (
+      "base": #610415,
+    ),
+  )
+);

BIN
packages/pc/src/assets/fonts/SOURCEHANSERIFCN-BOLD.OTF


BIN
packages/pc/src/assets/fonts/SOURCEHANSERIFCN-REGULAR.OTF


BIN
packages/pc/src/assets/images/back-min.png


BIN
packages/pc/src/assets/images/bg-min.jpg


BIN
packages/pc/src/assets/images/logo-min.png


+ 111 - 0
packages/pc/src/assets/main.css

@@ -0,0 +1,111 @@
+:root {
+  --z-index-normal: 1;
+  --z-index-top: 1000;
+  --z-index-popper: 2000;
+  --z-hot-popper: 3000;
+}
+
+body,
+ol,
+ul,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+p,
+th,
+td,
+dl,
+dd,
+form,
+fieldset,
+legend,
+input,
+textarea,
+select {
+  margin: 0;
+  padding: 0;
+}
+* {
+  box-sizing: border-box;
+}
+body {
+  color: #383838;
+  text-align: justify;
+  font-family: Microsoft YaHei, Microsoft YaHei;
+  -webkit-tap-highlight-color: transparent;
+}
+a {
+  color: #fff;
+  cursor: pointer;
+  text-decoration: none;
+}
+em {
+  font-style: normal;
+}
+li {
+  list-style: none;
+}
+img {
+  border: 0;
+  vertical-align: middle;
+}
+table {
+  border-collapse: collapse;
+  border-spacing: 0;
+}
+p {
+  word-wrap: break-word;
+}
+iframe {
+  border: none;
+}
+
+@font-face {
+  font-family: "SourceHanSerifSC-Bold";
+  src: url("@/assets/fonts/SOURCEHANSERIFCN-BOLD.otf");
+}
+@font-face {
+  font-family: "SourceHanSerifSC-Regular";
+  src: url("@/assets/fonts/SOURCEHANSERIFCN-REGULAR.otf");
+}
+
+.limit-line {
+  display: -webkit-box;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  -webkit-line-clamp: 1;
+  -webkit-box-orient: vertical;
+  word-break: break-all;
+  word-wrap: break-word;
+}
+
+.line-2 {
+  -webkit-line-clamp: 2;
+}
+
+.line-3 {
+  -webkit-line-clamp: 3;
+}
+
+.hidden {
+  display: none !important;
+  visibility: hidden !important;
+}
+
+.darkGlass {
+  background-color: rgba(0, 0, 0, 0.5);
+}
+
+.message-outer {
+  position: absolute;
+  display: table;
+  height: 100%;
+  width: 100%;
+
+  * {
+    transition: all 0.3s;
+  }
+}

+ 7 - 0
packages/pc/src/assets/utils.scss

@@ -0,0 +1,7 @@
+@function vh-calc($num) {
+  @return calc(100vh * ($num / var(--design-height)));
+}
+
+@function vw-calc($num) {
+  @return calc(100vw * ($num / var(--design-width)));
+}

+ 36 - 0
packages/pc/src/components/VideoPopup.vue

@@ -0,0 +1,36 @@
+<template>
+  <el-dialog
+    class="video-popup"
+    v-model="show"
+    :title="title"
+    destroy-on-close
+    append-to-body
+  >
+    <video controls autoplay :src="src" />
+  </el-dialog>
+</template>
+
+<script setup>
+import { computed } from "vue";
+
+const props = defineProps(["visible", "title", "src"]);
+const emits = defineEmits(["update:visible"]);
+
+const show = computed({
+  get() {
+    return props.visible;
+  },
+  set(v) {
+    emits("update:visible", v);
+  },
+});
+</script>
+
+<style lang="scss" scoped>
+.video-popup {
+  video {
+    width: 100%;
+    height: 100%;
+  }
+}
+</style>

+ 18 - 0
packages/pc/src/main.js

@@ -0,0 +1,18 @@
+import "./assets/main.css";
+
+import { createApp } from "vue";
+import { createPinia } from "pinia";
+
+import App from "./App.vue";
+import router from "./router";
+
+import { isDevelopment, checkDeviceAndRedirect } from "@guyuan/base";
+
+!isDevelopment && checkDeviceAndRedirect();
+
+const app = createApp(App);
+
+app.use(createPinia());
+app.use(router);
+
+app.mount("#app");

+ 39 - 0
packages/pc/src/router/index.js

@@ -0,0 +1,39 @@
+import { createRouter, createWebHashHistory } from "vue-router";
+
+const router = createRouter({
+  history: createWebHashHistory(import.meta.env.BASE_URL),
+  routes: [
+    {
+      path: "/",
+      name: "loading",
+      component: () => import("../views/Loading/index.vue"),
+    },
+    {
+      path: "/index",
+      name: "home",
+      component: () => import("../views/Home/index.vue"),
+    },
+    {
+      path: "/antique",
+      name: "antique",
+      component: () => import("../views/Antique/index.vue"),
+    },
+    {
+      path: "/history",
+      name: "history",
+      component: () => import("../views/History/index.vue"),
+    },
+    {
+      path: "/iframe",
+      name: "iframe",
+      component: () => import("../views/Iframe/index.vue"),
+    },
+    {
+      path: "/story",
+      name: "story",
+      component: () => import("../views/Story/index.vue"),
+    },
+  ],
+});
+
+export default router;

+ 26 - 0
packages/pc/src/stores/base.js

@@ -0,0 +1,26 @@
+import { ref } from "vue";
+import { defineStore } from "pinia";
+import { parseUrlName } from "@guyuan/base";
+import Img1 from "../views/Home/images/bg-min.jpg";
+import Img2 from "../views/Home/images/title-min.png";
+import Img3 from "../views/Home/images/tab-bg.png";
+import Img4 from "../views/Home/images/step.png";
+import Img5 from "../views/Home/images/map-min.png";
+import Img6 from "../views/Home/components/Biographies/images/bg-min.jpg";
+import Img7 from "../views/Home/images/line-active.png";
+
+export const RESOURCE_LIST = [Img1, Img2, Img3, Img4, Img5, Img6, Img7];
+
+export const useBaseStore = defineStore("base", () => {
+  const mediaUrlMap = ref(new Map());
+
+  RESOURCE_LIST.forEach((url) => {
+    mediaUrlMap.value.set(parseUrlName(url), url);
+  });
+
+  const setMediaUrlMap = (list) => {
+    mediaUrlMap.value = list;
+  };
+
+  return { mediaUrlMap, setMediaUrlMap };
+});

+ 12 - 0
packages/pc/src/stores/counter.js

@@ -0,0 +1,12 @@
+import { ref, computed } from 'vue'
+import { defineStore } from 'pinia'
+
+export const useCounterStore = defineStore('counter', () => {
+  const count = ref(0)
+  const doubleCount = computed(() => count.value * 2)
+  function increment() {
+    count.value++
+  }
+
+  return { count, doubleCount, increment }
+})

+ 130 - 0
packages/pc/src/views/Antique/components/Popup.vue

@@ -0,0 +1,130 @@
+<template>
+  <el-dialog
+    class="antique-popup"
+    body-class="antique-popup-body"
+    v-model="show"
+    append-to-body
+    destroy-on-close
+    :show-close="false"
+  >
+    <i class="antique-popup__close" @click="show = false" />
+
+    <div class="antique-popup-wrap">
+      <Swiper
+        :modules="[Navigation]"
+        class="antique-popup-swiper"
+        :slides-per-view="1"
+        :centered-slides="true"
+        :navigation="true"
+      >
+        <SwiperSlide>
+          <el-image
+            src="https://houseoss.4dkankan.com/project/guyuan/pc/assets/c1-min-Cebn0ppc.png"
+            fit="scale-down"
+          />
+        </SwiperSlide>
+        <SwiperSlide>
+          <el-image
+            src="https://houseoss.4dkankan.com/project/guyuan/pc/assets/c1-min-Cebn0ppc.png"
+            fit="scale-down"
+          />
+        </SwiperSlide>
+      </Swiper>
+    </div>
+
+    <div class="antique-popup-info">
+      <h2>一级解放勋章</h2>
+      <p>
+        1955 年 9 月,韩练成被授予中将军衔、一级解放勋章。授衔
+        前,周恩来曾经征求他的意见。
+      </p>
+    </div>
+  </el-dialog>
+</template>
+
+<script setup>
+import { computed } from "vue";
+import { Swiper, SwiperSlide } from "swiper/vue";
+import { Navigation } from "swiper/modules";
+import "swiper/css";
+import "swiper/css/navigation";
+
+const props = defineProps(["visible"]);
+const emits = defineEmits(["update:visible"]);
+
+const show = computed({
+  get() {
+    return props.visible;
+  },
+  set(v) {
+    emits("update:visible", v);
+  },
+});
+</script>
+
+<style lang="scss">
+@use "@/assets/utils.scss";
+
+.antique-popup {
+  --el-dialog-width: 100%;
+  --el-dialog-padding-primary: 0;
+  --el-dialog-bg-color: rgba(0, 0, 0, 0.9);
+  margin: 0;
+  height: 100%;
+
+  &__close {
+    position: absolute;
+    top: utils.vh-calc(41);
+    right: utils.vh-calc(37);
+    width: utils.vh-calc(46);
+    height: utils.vh-calc(46);
+    cursor: pointer;
+    background: url("../images/close.png") no-repeat center / contain;
+  }
+  &-body {
+    display: flex;
+    padding: utils.vh-calc(40) utils.vh-calc(100);
+    gap: 10px;
+    height: 100%;
+  }
+  &-wrap {
+    flex: 1;
+    width: 0;
+  }
+  &-swiper {
+    height: 100%;
+
+    .swiper-button-next,
+    .swiper-button-prev {
+      width: utils.vh-calc(24);
+      height: utils.vh-calc(24);
+
+      &::after {
+        display: none;
+      }
+    }
+    .swiper-button-prev {
+      background: url("../images/left.png") no-repeat center / contain;
+    }
+    .swiper-button-next {
+      background: url("../images/right.png") no-repeat center / contain;
+    }
+  }
+  &-info {
+    padding: utils.vh-calc(120) utils.vh-calc(40);
+    width: utils.vh-calc(416);
+
+    h2 {
+      color: #fcd97e;
+      font-size: utils.vh-calc(32);
+      letter-spacing: 1px;
+    }
+    p {
+      margin-top: utils.vh-calc(16);
+      color: #d6d6d6;
+      line-height: utils.vh-calc(43);
+      font-size: utils.vh-calc(24);
+    }
+  }
+}
+</style>

+ 40 - 0
packages/pc/src/views/Antique/constants.js

@@ -0,0 +1,40 @@
+// 三维文物
+export const ANTIQUE_THREE = [
+  {
+    label: "类别1",
+    children: [
+      {
+        label: "一级解放",
+        // 封面
+        thumb: "/base/images/xxx",
+        // 模型链接
+        link: "https://xxxx",
+        desc: "1955 年 9 月,韩练成被授予中将军衔、一级解放勋章。授衔 前,周恩来曾经征求他的意见。",
+      },
+    ],
+  },
+  {
+    label: "类别2",
+    children: [],
+  },
+];
+
+// 二维文物
+export const ANTIQUE_TWO = [
+  {
+    label: "类别3",
+    children: [
+      {
+        label: "一级解放勋章",
+        thumb: "/base/images/xxx",
+        // 图片列表
+        imgs: ["/base/images/xxx"],
+        desc: "1955 年 9 月,韩练成被授予中将军衔、一级解放勋章。授衔 前,周恩来曾经征求他的意见。",
+      },
+    ],
+  },
+  {
+    label: "类别2",
+    children: [],
+  },
+];

BIN
packages/pc/src/views/Antique/images/close.png


BIN
packages/pc/src/views/Antique/images/item-bg-min.png


BIN
packages/pc/src/views/Antique/images/item-hover-min.png


BIN
packages/pc/src/views/Antique/images/left.png


BIN
packages/pc/src/views/Antique/images/right.png


BIN
packages/pc/src/views/Antique/images/search-bg.png


BIN
packages/pc/src/views/Antique/images/search-close.png


BIN
packages/pc/src/views/Antique/images/search-min.png


BIN
packages/pc/src/views/Antique/images/tab-bg-min.png


BIN
packages/pc/src/views/Antique/images/tab1-active-min.png


BIN
packages/pc/src/views/Antique/images/tab1-min.png


BIN
packages/pc/src/views/Antique/images/tab2-active-min.png


BIN
packages/pc/src/views/Antique/images/tab2-min.png


+ 178 - 0
packages/pc/src/views/Antique/index.scss

@@ -0,0 +1,178 @@
+@use "@/assets/utils.scss";
+
+.antique {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  min-height: 100%;
+  padding-bottom: utils.vw-calc(30);
+  background: url("@/assets/images/bg-min.jpg") repeat center / cover;
+
+  &__close {
+    position: absolute;
+    top: utils.vh-calc(28);
+    left: utils.vw-calc(31);
+    width: utils.vw-calc(38);
+    height: utils.vw-calc(38);
+    cursor: pointer;
+    background: url("@/assets/images/back-min.png") no-repeat center / contain;
+  }
+  &-tabs {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-top: utils.vh-calc(110);
+    gap: utils.vw-calc(15);
+
+    div {
+      width: utils.vw-calc(251);
+      height: utils.vw-calc(57);
+      cursor: pointer;
+
+      &.active {
+        position: relative;
+        top: utils.vw-calc(4);
+        width: utils.vw-calc(257);
+        height: utils.vw-calc(63);
+      }
+      &.two {
+        background: url("./images/tab1-min.png") no-repeat center / contain;
+
+        &.active {
+          background-image: url("./images/tab1-active-min.png");
+        }
+      }
+      &.three {
+        background: url("./images/tab2-min.png") no-repeat center / contain;
+
+        &.active {
+          background-image: url("./images/tab2-active-min.png");
+        }
+      }
+    }
+  }
+  &-main {
+    margin: 8px auto 0;
+    padding-bottom: utils.vh-calc(30);
+    width: utils.vw-calc(1666);
+    background: rgba(0, 0, 0, 0.16);
+  }
+  &-head {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    position: relative;
+    left: -2px;
+    margin-bottom: utils.vh-calc(10);
+    padding: 0 utils.vw-calc(40) utils.vw-calc(20);
+    width: utils.vw-calc(1672);
+    height: utils.vw-calc(111);
+    background: url("./images/tab-bg-min.png") no-repeat center / cover;
+  }
+  &-tabs2 {
+    display: flex;
+    align-items: center;
+    gap: utils.vw-calc(30);
+
+    li {
+      position: relative;
+      color: #343a40;
+      font-size: utils.vw-calc(18);
+      cursor: pointer;
+
+      &.active {
+        color: #741a1a;
+        font-weight: bold;
+
+        &::after {
+          content: "";
+          position: absolute;
+          left: 0;
+          right: 0;
+          bottom: utils.vw-calc(-9);
+          height: utils.vw-calc(4);
+          background: #741a1a;
+        }
+      }
+    }
+  }
+  &-search {
+    display: flex;
+    align-items: center;
+    padding: 0 utils.vw-calc(6);
+    width: utils.vw-calc(359);
+    height: utils.vw-calc(33);
+    background: url("./images/search-bg.png") no-repeat center / contain;
+
+    &::before {
+      content: "";
+      width: utils.vw-calc(24);
+      height: utils.vw-calc(25);
+      background: url("./images/search-min.png") no-repeat center / contain;
+    }
+    &::after {
+      content: "";
+      width: utils.vw-calc(24);
+      height: utils.vw-calc(24);
+      cursor: pointer;
+      background: url("./images/search-close.png") no-repeat center / contain;
+    }
+    input {
+      flex: 1;
+      height: 80%;
+      border: none;
+      background: transparent;
+
+      &::placeholder {
+        color: rgba(0, 0, 0, 0.4);
+      }
+      &:focus {
+        outline: none;
+      }
+    }
+  }
+  &-list {
+    display: flex;
+    flex-wrap: wrap;
+    padding: 0 utils.vw-calc(25);
+  }
+  &-item {
+    position: relative;
+    margin: utils.vw-calc(15) utils.vw-calc(5.5);
+    width: utils.vw-calc(312);
+    height: utils.vw-calc(312);
+    cursor: pointer;
+    background: url("./images/item-bg-min.png") no-repeat center / contain;
+
+    &:hover {
+      .antique-item-hover {
+        opacity: 1;
+      }
+    }
+    p {
+      position: absolute;
+      left: utils.vw-calc(16);
+      right: utils.vw-calc(16);
+      bottom: utils.vw-calc(10);
+      line-height: utils.vw-calc(43);
+      font-size: utils.vw-calc(16);
+      color: #d6d6d6;
+    }
+    &-hover {
+      position: absolute;
+      inset: utils.vw-calc(-8);
+      padding: utils.vw-calc(30) 0;
+      color: #f6b550;
+      font-size: utils.vw-calc(24);
+      line-height: utils.vw-calc(40);
+      transition: opacity linear 0.2s;
+      opacity: 0;
+      background: url("./images/item-hover-min.png") no-repeat center / contain;
+
+      .el-scrollbar {
+        padding: 0 utils.vw-calc(30);
+      }
+    }
+  }
+}

+ 75 - 0
packages/pc/src/views/Antique/index.vue

@@ -0,0 +1,75 @@
+<template>
+  <div class="antique">
+    <i class="antique__close" @click="$router.back" />
+
+    <div class="antique-tabs">
+      <div
+        v-for="(tab, index) in ['two', 'three']"
+        :key="tab"
+        :class="[tab, { active: tabIndex === index }]"
+        @click="tabIndex = index"
+      />
+    </div>
+
+    <div class="antique-main">
+      <div class="antique-head">
+        <ul class="antique-tabs2">
+          <li
+            v-for="(item, index) in tabs"
+            :key="item.label"
+            :class="{ active: tab2Index === index }"
+            @click="tab2Index = index"
+          >
+            {{ item.label }}
+          </li>
+        </ul>
+
+        <div class="antique-search">
+          <input placeholder="请输入文物名称" />
+        </div>
+      </div>
+
+      <ul class="antique-list">
+        <li
+          v-for="(item, index) in tabs[tab2Index].children"
+          :key="index"
+          class="antique-item"
+        >
+          <p class="limit-line">{{ item.label }}</p>
+
+          <div class="antique-item-hover">
+            <el-scrollbar scroll-y>
+              {{ item.desc }}
+            </el-scrollbar>
+          </div>
+        </li>
+      </ul>
+    </div>
+  </div>
+
+  <Popup v-model:visible="popupVisible" />
+</template>
+
+<script setup>
+import { ref, computed } from "vue";
+import { ANTIQUE_THREE, ANTIQUE_TWO } from "./constants";
+import Popup from "./components/Popup.vue";
+
+const tabIndex = ref(0);
+const tab2Index = ref(0);
+const popupVisible = ref(true);
+const tabs = computed(() => {
+  const list = [...(tabIndex.value === 0 ? ANTIQUE_THREE : ANTIQUE_TWO)];
+
+  list.unshift({
+    label: "全部类别",
+    children: list.flatMap((item) => item.children),
+  });
+
+  return list;
+});
+</script>
+
+<style lang="scss" scoped>
+@use "./index.scss";
+</style>

+ 81 - 0
packages/pc/src/views/History/components/Audio.vue

@@ -0,0 +1,81 @@
+<template>
+  <Panel title="音频">
+    <ul class="audio">
+      <li v-for="(item, index) in AUDIOS" :key="index" class="audio-item">
+        <el-image src="" />
+        <div class="audio-item-main">
+          <p class="audio-item__title">{{ item.label }}</p>
+          <div class="audio-item-progress">
+            <el-progress :show-text="false" :percentage="50" />
+            <i class="icon" />
+          </div>
+          <p class="audio-item__content">{{ item.content }}</p>
+        </div>
+      </li>
+    </ul>
+  </Panel>
+</template>
+
+<script setup>
+import Panel from "./Panel.vue";
+import { AUDIOS } from "../constants";
+</script>
+
+<style lang="scss" scoped>
+@use "@/assets/utils.scss";
+
+.audio {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: utils.vw-calc(46) utils.vw-calc(65);
+
+  &-item {
+    display: flex;
+    align-items: center;
+    gap: utils.vw-calc(30);
+    padding-right: utils.vw-calc(40);
+    height: utils.vw-calc(170);
+    background: rgba(40, 40, 40, 0.6);
+
+    .el-image {
+      width: utils.vw-calc(290);
+      height: 100%;
+    }
+    &-main {
+      flex: 1;
+      color: white;
+    }
+    &-progress {
+      display: flex;
+      align-items: center;
+      gap: utils.vw-calc(10);
+      padding: 0 utils.vw-calc(8) 0 utils.vw-calc(10);
+      margin: utils.vw-calc(10) 0;
+      width: utils.vw-calc(344);
+      height: utils.vw-calc(20);
+      border-radius: 10px;
+      background: #484848;
+
+      .el-progress {
+        --el-color-primary: #610415;
+        --el-border-color-lighter: rgba(196, 196, 196, 0.2);
+        flex: 1;
+      }
+      .icon {
+        width: utils.vw-calc(16);
+        height: utils.vw-calc(16);
+        background: url("../images/play-icon-min.png") no-repeat center /
+          contain;
+        cursor: pointer;
+      }
+    }
+    &__title {
+      font-size: utils.vw-calc(18);
+      font-weight: bold;
+    }
+    &__content {
+      font-size: utils.vw-calc(14);
+    }
+  }
+}
+</style>

+ 48 - 0
packages/pc/src/views/History/components/Doc.vue

@@ -0,0 +1,48 @@
+<template>
+  <Panel title="文献">
+    <ul class="doc">
+      <li v-for="(item, index) in DOCS" :key="index" class="doc-item">
+        <el-image :src="item.thumb" />
+
+        <p class="limit-line">{{ item.label }}</p>
+      </li>
+    </ul>
+  </Panel>
+</template>
+
+<script setup>
+import Panel from "./Panel.vue";
+import { DOCS } from "../constants";
+</script>
+
+<style lang="scss" scoped>
+@use "@/assets/utils.scss";
+
+.doc {
+  display: grid;
+  grid-template-columns: repeat(4, 1fr);
+  gap: utils.vw-calc(45) utils.vw-calc(27);
+
+  &-item {
+    position: relative;
+    height: utils.vw-calc(231);
+    cursor: pointer;
+
+    .el-image {
+      height: 100%;
+    }
+    p {
+      position: absolute;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      padding: 0 utils.vw-calc(20);
+      color: white;
+      font-weight: bold;
+      line-height: utils.vw-calc(31);
+      font-size: utils.vw-calc(18);
+      background: rgba(97, 4, 21, 0.8);
+    }
+  }
+}
+</style>

+ 48 - 0
packages/pc/src/views/History/components/Image.vue

@@ -0,0 +1,48 @@
+<template>
+  <Panel title="图片">
+    <ul class="image">
+      <li v-for="(item, index) in VIDEOS" :key="index" class="image-item">
+        <el-image :src="item.thumb" />
+
+        <p class="limit-line">{{ item.label }}</p>
+      </li>
+    </ul>
+  </Panel>
+</template>
+
+<script setup>
+import Panel from "./Panel.vue";
+import { VIDEOS } from "../constants";
+</script>
+
+<style lang="scss" scoped>
+@use "@/assets/utils.scss";
+
+.image {
+  display: grid;
+  grid-template-columns: repeat(4, 1fr);
+  gap: utils.vw-calc(45) utils.vw-calc(27);
+
+  &-item {
+    position: relative;
+    height: utils.vw-calc(231);
+    cursor: pointer;
+
+    .el-image {
+      height: 100%;
+    }
+    p {
+      position: absolute;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      padding: 0 utils.vw-calc(20);
+      color: white;
+      font-weight: bold;
+      line-height: utils.vw-calc(31);
+      font-size: utils.vw-calc(18);
+      background: rgba(97, 4, 21, 0.8);
+    }
+  }
+}
+</style>

+ 41 - 0
packages/pc/src/views/History/components/Panel.vue

@@ -0,0 +1,41 @@
+<template>
+  <div class="panel">
+    <p class="panel-title">{{ title }}</p>
+
+    <el-scrollbar scroll-y class="panel-contain">
+      <slot />
+    </el-scrollbar>
+  </div>
+</template>
+
+<script setup>
+defineProps(["title"]);
+</script>
+
+<style lang="scss" scoped>
+@use "@/assets/utils.scss";
+
+.panel {
+  position: relative;
+  width: utils.vw-calc(1672);
+  height: utils.vw-calc(813);
+  background: url("../images/bg-min.png") no-repeat center / contain;
+
+  &-title {
+    position: absolute;
+    top: utils.vw-calc(36);
+    left: utils.vw-calc(34);
+    font-size: utils.vw-calc(24);
+    font-weight: bold;
+    color: #343a40;
+  }
+  &-contain {
+    position: absolute;
+    top: utils.vw-calc(110);
+    left: utils.vw-calc(80);
+    right: utils.vw-calc(80);
+    bottom: utils.vw-calc(60);
+    height: auto;
+  }
+}
+</style>

+ 77 - 0
packages/pc/src/views/History/components/Video.vue

@@ -0,0 +1,77 @@
+<template>
+  <Panel title="视频">
+    <ul class="video">
+      <li
+        v-for="(item, index) in VIDEOS"
+        :key="index"
+        class="video-item"
+        @click="handleClick(item)"
+      >
+        <el-image :src="item.thumb" />
+
+        <p class="limit-line">{{ item.label }}</p>
+      </li>
+    </ul>
+
+    <VideoPopup
+      v-model:visible="videoVisible"
+      src="https://houseoss.4dkankan.com/project/guyuan/base/videos/video1.mp4"
+    />
+  </Panel>
+</template>
+
+<script setup>
+import { ref } from "vue";
+import Panel from "./Panel.vue";
+import { VIDEOS } from "../constants";
+import VideoPopup from "@/components/VideoPopup.vue";
+
+const videoVisible = ref(false);
+
+const handleClick = (item) => {
+  videoVisible.value = true;
+};
+</script>
+
+<style lang="scss" scoped>
+@use "@/assets/utils.scss";
+
+.video {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: utils.vw-calc(40) utils.vw-calc(70);
+
+  &-item {
+    position: relative;
+    height: utils.vw-calc(295);
+    cursor: pointer;
+
+    .el-image {
+      height: 100%;
+    }
+    p {
+      position: absolute;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      padding: 0 utils.vw-calc(20);
+      color: white;
+      font-weight: bold;
+      line-height: utils.vw-calc(40);
+      font-size: utils.vw-calc(18);
+      background: rgba(97, 4, 21, 0.8);
+    }
+    &::before {
+      content: "";
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      width: utils.vw-calc(100);
+      height: utils.vw-calc(100);
+      transform: translate(-50%, -50%);
+      background: url("../images/play-min.png") no-repeat center / contain;
+      z-index: 1;
+    }
+  }
+}
+</style>

+ 47 - 0
packages/pc/src/views/History/constants.js

@@ -0,0 +1,47 @@
+export const AUDIOS = [
+  {
+    label: "这是一段音频",
+    content:
+      "这是一段音频介绍这是一段音频介绍这是一段音频介绍这是一段音频介绍这是一段音频介绍这是一段音频介绍",
+    thumb: "/base/xxxxx",
+    src: "/base/audios/xxxx",
+  },
+  {
+    label: "这是一段音频",
+    content:
+      "这是一段音频介绍这是一段音频介绍这是一段音频介绍这是一段音频介绍这是一段音频介绍这是一段音频介绍",
+    thumb: "/base/xxxxx",
+    src: "/base/audios/xxxx",
+  },
+  {
+    label: "这是一段音频",
+    content:
+      "这是一段音频介绍这是一段音频介绍这是一段音频介绍这是一段音频介绍这是一段音频介绍这是一段音频介绍",
+    thumb: "/base/xxxxx",
+    src: "/base/audios/xxxx",
+  },
+];
+
+export const VIDEOS = [
+  {
+    label: "这是一段音频",
+    thumb: "/base/xxxxx",
+    src: "/base/videos/xxxx",
+  },
+];
+
+export const IMAGES = [
+  {
+    label: "这是一张图片",
+    thumb: "/base/xxxxx",
+    imgs: ["/base/Images/xxxx"],
+  },
+];
+
+export const DOCS = [
+  {
+    label: "这是一个文献",
+    thumb: "/base/xxxxx",
+    src: "/base/docs/xxxx",
+  },
+];

BIN
packages/pc/src/views/History/images/bg-min.png


BIN
packages/pc/src/views/History/images/pause-icon-min.png


BIN
packages/pc/src/views/History/images/play-icon-min.png


BIN
packages/pc/src/views/History/images/play-min.png


BIN
packages/pc/src/views/History/images/tab1-active-min.png


BIN
packages/pc/src/views/History/images/tab1-min.png


BIN
packages/pc/src/views/History/images/tab2-active-min.png


BIN
packages/pc/src/views/History/images/tab2-min.png


BIN
packages/pc/src/views/History/images/tab3-active-min.png


BIN
packages/pc/src/views/History/images/tab3-min.png


BIN
packages/pc/src/views/History/images/tab4-active-min.png


BIN
packages/pc/src/views/History/images/tab4-min.png


+ 76 - 0
packages/pc/src/views/History/index.scss

@@ -0,0 +1,76 @@
+@use "@/assets/utils.scss";
+
+.history {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  min-height: 100%;
+  padding-bottom: utils.vw-calc(30);
+  background: url("@/assets/images/bg-min.jpg") repeat center / cover;
+
+  &__close {
+    position: absolute;
+    top: utils.vh-calc(28);
+    left: utils.vw-calc(31);
+    width: utils.vw-calc(38);
+    height: utils.vw-calc(38);
+    cursor: pointer;
+    background: url("@/assets/images/back-min.png") no-repeat center / contain;
+  }
+  &-tabs {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-top: utils.vh-calc(110);
+    gap: utils.vw-calc(15);
+
+    div {
+      width: utils.vw-calc(251);
+      height: utils.vw-calc(57);
+      cursor: pointer;
+
+      &.active {
+        position: relative;
+        top: utils.vw-calc(4);
+        width: utils.vw-calc(257);
+        height: utils.vw-calc(63);
+      }
+      &.audio {
+        background: url("./images/tab1-min.png") no-repeat center / contain;
+
+        &.active {
+          background-image: url("./images/tab1-active-min.png");
+        }
+      }
+      &.video {
+        background: url("./images/tab2-min.png") no-repeat center / contain;
+
+        &.active {
+          background-image: url("./images/tab2-active-min.png");
+        }
+      }
+      &.image {
+        background: url("./images/tab3-min.png") no-repeat center / contain;
+
+        &.active {
+          background-image: url("./images/tab3-active-min.png");
+        }
+      }
+      &.doc {
+        background: url("./images/tab4-min.png") no-repeat center / contain;
+
+        &.active {
+          background-image: url("./images/tab4-active-min.png");
+        }
+      }
+    }
+  }
+  &-main {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: utils.vw-calc(15);
+    margin-top: utils.vw-calc(15);
+  }
+}

+ 139 - 0
packages/pc/src/views/History/index.vue

@@ -0,0 +1,139 @@
+<template>
+  <div class="history">
+    <i class="history__close" @click="$router.back" />
+
+    <div class="history-tabs">
+      <div
+        v-for="(tab, index) in TABS"
+        :key="tab"
+        :class="[tab, { active: tabIndex === index }]"
+        @click="onTabClick(index)"
+      />
+    </div>
+
+    <div class="history-main" ref="mainRef">
+      <!-- <AudioPanel /> -->
+      <div class="history-panel" ref="videoPanel">
+        <VideoPanel />
+      </div>
+
+      <div class="history-panel" ref="imagePanel">
+        <ImagePanel />
+      </div>
+      <!-- <DocPanel /> -->
+      <div style="height: 100vh" />
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onUnmounted, nextTick } from "vue";
+import AudioPanel from "./components/Audio.vue";
+import VideoPanel from "./components/Video.vue";
+import ImagePanel from "./components/Image.vue";
+import DocPanel from "./components/Doc.vue";
+
+const TABS = ["video", "image"];
+const tabIndex = ref(0);
+
+const mainRef = ref(null);
+const videoPanel = ref(null);
+const imagePanel = ref(null);
+
+const panelEls = () => [videoPanel.value, imagePanel.value];
+
+function scrollToPanel(index) {
+  const el = panelEls()[index];
+  if (!el) return;
+  // 对 document/viewport 场景,scrollIntoView 直接可用
+  el.scrollIntoView({ behavior: "smooth", block: "start" });
+}
+
+function onTabClick(index) {
+  tabIndex.value = index;
+  scrollToPanel(index);
+}
+
+let observer = null;
+
+// 根据 viewport(document)滚动计算可见比例并设置高亮
+function updateActiveByScroll() {
+  let bestIdx = -1;
+  let bestRatio = 0;
+  const vh = window.innerHeight || document.documentElement.clientHeight;
+
+  panelEls().forEach((el, idx) => {
+    if (!el) return;
+    const rect = el.getBoundingClientRect();
+    const elHeight = rect.height || 0;
+    const visible = Math.max(
+      0,
+      Math.min(rect.bottom, vh) - Math.max(rect.top, 0)
+    );
+    const ratio = elHeight > 0 ? visible / elHeight : 0;
+    if (ratio > bestRatio) {
+      bestRatio = ratio;
+      bestIdx = idx;
+    }
+  });
+
+  if (bestIdx >= 0) tabIndex.value = bestIdx;
+}
+
+let scrollHandler = null;
+let resizeHandler = null;
+
+onMounted(async () => {
+  await nextTick();
+  const roots = panelEls().filter(Boolean);
+  if (!roots.length) return;
+
+  // 使用 viewport 作为 root(document 的滚动)
+  observer = new IntersectionObserver(
+    (entries) => {
+      let best = null;
+      entries.forEach((e) => {
+        if (e.isIntersecting) {
+          if (!best || e.intersectionRatio > best.intersectionRatio) best = e;
+        }
+      });
+      if (best) {
+        const idx = panelEls().indexOf(best.target);
+        if (idx >= 0) tabIndex.value = idx;
+      }
+    },
+    {
+      root: null, // viewport
+      threshold: [0.25, 0.5, 0.75],
+    }
+  );
+
+  panelEls().forEach((el) => {
+    if (el) observer.observe(el);
+  });
+
+  // 绑定到 window 的滚动(document)和 resize
+  scrollHandler = () => {
+    updateActiveByScroll();
+  };
+  resizeHandler = () => {
+    updateActiveByScroll();
+  };
+
+  window.addEventListener("scroll", scrollHandler, { passive: true });
+  window.addEventListener("resize", resizeHandler, { passive: true });
+
+  // 初始运行一次以设置正确状态
+  updateActiveByScroll();
+});
+
+onUnmounted(() => {
+  if (observer) observer.disconnect();
+  if (scrollHandler) window.removeEventListener("scroll", scrollHandler);
+  if (resizeHandler) window.removeEventListener("resize", resizeHandler);
+});
+</script>
+
+<style lang="scss" scoped>
+@use "./index.scss";
+</style>

BIN
packages/pc/src/views/Home/components/Biographies/images/bg-min.jpg


BIN
packages/pc/src/views/Home/components/Biographies/images/title-min.png


+ 90 - 0
packages/pc/src/views/Home/components/Biographies/index.vue

@@ -0,0 +1,90 @@
+<template>
+  <div v-if="show" class="biographies">
+    <i class="biographies__close" @click="show = false" />
+
+    <div class="biographies-main">
+      <img
+        draggable="false"
+        class="biographies-title"
+        src="./images/title-min.png"
+      />
+      <p class="biographies-content">
+        韩练成,宁夏固原人,隐蔽战 线传奇将军之一。他少年从戎,信
+        仰坚定,始终对光明孜孜以求,从 冯玉祥的爱将到桂系战将、蒋介石
+        身边的亲信,再到成长为“没有办 理入党手续的共产党员”,关键时
+        刻凸显大智大勇,“为党、为革命立 了大功、立了奇功”;建国后,淡泊
+        名利,“要党员,不要上将”,为建设 新中国默默奉献,始终保持共产党
+        员的本色。 他被称为“隐形将军”。
+      </p>
+      <el-button type="primary">进入</el-button>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { computed } from "vue";
+
+const props = defineProps(["visible"]);
+const emits = defineEmits(["update:visible"]);
+const show = computed({
+  get() {
+    return props.visible;
+  },
+  set(v) {
+    emits("update:visible", v);
+  },
+});
+</script>
+
+<style lang="scss" scoped>
+@use "@/assets/utils.scss";
+
+.biographies {
+  position: absolute;
+  inset: 0;
+  background: url("./images/bg-min.jpg") no-repeat center right / cover;
+  z-index: 999;
+
+  &__close {
+    position: absolute;
+    top: utils.vh-calc(28);
+    left: utils.vw-calc(31);
+    width: utils.vh-calc(30);
+    height: utils.vh-calc(30);
+    cursor: pointer;
+    background: url("@/assets/images/back-min.png") no-repeat center / contain;
+  }
+  &-main {
+    position: absolute;
+    top: 50%;
+    left: utils.vw-calc(115);
+    display: flex;
+    flex-direction: column;
+    width: utils.vh-calc(575);
+    transform: translateY(-50%);
+  }
+  &-title {
+    width: utils.vh-calc(536);
+    height: utils.vh-calc(220);
+  }
+  &-content {
+    position: relative;
+    margin: utils.vh-calc(50) 0;
+    left: utils.vh-calc(38);
+    width: utils.vh-calc(536);
+    text-indent: 2em;
+    font-weight: bold;
+    color: #ffffff;
+    font-size: utils.vh-calc(18);
+    line-height: utils.vh-calc(47);
+  }
+  .el-button {
+    --el-border: none;
+    --el-color-primary: rgba(252, 217, 126, 0.4);
+    position: relative;
+    left: utils.vh-calc(38);
+    width: utils.vh-calc(245);
+    height: utils.vh-calc(51);
+  }
+}
+</style>

+ 159 - 0
packages/pc/src/views/Home/components/SharePopup.vue

@@ -0,0 +1,159 @@
+<template>
+  <el-dialog class="share-popup" v-model="show" title="分享" append-to-body>
+    <p>请使用手机扫描二维码或 复制分享链接</p>
+
+    <vue-qrcode
+      :value="url"
+      :width="isMobile ? 150 : 180"
+      class="share-popup__qrcode"
+      :color="{}"
+      type="image/jpeg"
+    />
+
+    <div class="share-popup-tools">
+      <div class="share-popup__btn" @click="copyUrl">复制分享链接</div>
+      <div v-if="_isMobile" class="share-popup__btn" @click="saveQRCode">
+        保存二维码
+      </div>
+    </div>
+  </el-dialog>
+</template>
+
+<script setup>
+import { computed } from "vue";
+import VueQrcode from "vue-qrcode";
+import clipboard from "clipboard";
+import { isMobile } from "@guyuan/base";
+
+const url = window.location.href;
+const _isMobile = isMobile();
+const props = defineProps(["visible"]);
+const emits = defineEmits(["update:visible"]);
+
+const show = computed({
+  get() {
+    return props.visible;
+  },
+  set(v) {
+    emits("update:visible", v);
+  },
+});
+
+const copyUrl = () => {
+  clipboard.copy(url);
+
+  ElNotification({
+    title: "提示",
+    type: "success",
+    message: "链接已复制",
+    position: "bottom-right",
+  });
+};
+
+const saveQRCode = () => {
+  const img = document.getElementsByClassName("share-popup__qrcode");
+  if (img && img.length) {
+    const a = document.createElement("a");
+    // @ts-ignore
+    a.href = img[0].src;
+    a.download = "三亚家园.jpg";
+    a.click();
+  }
+};
+</script>
+
+<style lang="scss">
+.share-popup {
+  --el-dialog-width: 420px;
+  --el-dialog-border-radius: 0;
+  --el-dialog-title-font-size: 24px;
+  height: 580px;
+  background: rgba(0, 0, 0, 0.7);
+  backdrop-filter: blur(4px);
+
+  .el-dialog__header {
+    padding: 50px 40px;
+  }
+  .el-dialog__title {
+    color: white;
+    font-weight: 700;
+  }
+  .el-dialog__body {
+    padding: 0 60px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+
+    p {
+      width: 220px;
+      color: white;
+      font-size: 20px;
+      text-align: center;
+    }
+  }
+  .el-dialog__headerbtn {
+    --el-color-info: white;
+    top: 35px;
+    right: 20px;
+    font-size: 26px;
+  }
+  &__qrcode {
+    margin: 25px 0 40px;
+    overflow: hidden;
+    border-radius: 10px;
+  }
+  &__btn {
+    flex: 1;
+    height: 50px;
+    line-height: 50px;
+    text-align: center;
+    color: white;
+    font-size: 20px;
+    background: var(--el-color-primary);
+    border-radius: 100px;
+    cursor: pointer;
+  }
+  &-tools {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    width: 100%;
+  }
+}
+
+@media only screen and (max-width: 600px) {
+  .share-popup {
+    --el-dialog-width: 314px;
+    --el-dialog-border-radius: 0;
+    --el-dialog-title-font-size: 16px;
+    height: 433px;
+
+    .el-dialog__header {
+      padding: 10px 30px 30px;
+    }
+    .el-dialog__body {
+      padding: 0 30px;
+
+      p {
+        width: 160px;
+        font-size: 16px;
+      }
+    }
+    .el-dialog__headerbtn {
+      top: 15px;
+      right: 6px;
+      font-size: 23px;
+    }
+    &__qrcode {
+      margin: 18px 0 37px;
+      border-radius: 3px;
+    }
+    &__btn {
+      height: 43px;
+      line-height: 43px;
+      font-size: 16px;
+    }
+  }
+}
+</style>

+ 115 - 0
packages/pc/src/views/Home/constants.js

@@ -0,0 +1,115 @@
+import M1Img from "./images/m1-min.png";
+import M2Img from "./images/m2-min.png";
+import M3Img from "./images/m3-min.png";
+import M4Img from "./images/m4-min.png";
+import M1ActImg from "./images/m1-active-min.png";
+import M2ActImg from "./images/m2-active-min.png";
+import M3ActImg from "./images/m3-active-min.png";
+import M4ActImg from "./images/m4-active-min.png";
+import C1Img from "./images/c1-min.png";
+import C2Img from "./images/c2-min.png";
+import C3Img from "./images/c3-min.png";
+import C4Img from "./images/c4-min.png";
+import C5Img from "./images/c5-min.png";
+import C6Img from "./images/c6-min.png";
+import T1Img from "./images/t1-min.png";
+import T2Img from "./images/t2-min.png";
+import T3Img from "./images/t3-min.png";
+import T4Img from "./images/t4-min.png";
+import T5Img from "./images/t5-min.png";
+import T6Img from "./images/t6-min.png";
+
+export const MIMAGES = [
+  {
+    default: M1Img,
+    active: M1ActImg,
+    href: "https://houseoss.4dkankan.com/project/guyuan/scene/index.html?m=SG-IDyBc0bleFF",
+  },
+  {
+    default: M2Img,
+    active: M2ActImg,
+    routeName: "antique",
+  },
+  {
+    default: M3Img,
+    active: M3ActImg,
+    routeName: "history",
+  },
+  {
+    type: "share",
+    default: M4Img,
+    active: M4ActImg,
+  },
+];
+
+export const POINTS = [
+  {
+    img: C1Img,
+    img2: T1Img,
+    content: `
+      <p>入境西吉县,</p>
+      <p>毛泽东夜访单南寺,</p>
+      <p>与马德海秉烛长谈,</p>
+      <p>宣讲民族政策及抗日主张,</p>
+      <p>军民鱼水情浓,</p>
+      <p>史称“单家集夜话”。</p>
+    `,
+  },
+  {
+    img: C2Img,
+    img2: T2Img,
+    content: `
+      <p>夜宿张易堡,</p>
+      <p>红军继续行军,</p>
+      <p>宿营张易堡,</p>
+      <p>严守纪律不扰民,</p>
+      <p>借门板铺地休息,</p>
+      <p>清晨归还并留铜元致谢,</p>
+      <p>百姓称“仁义之师”。</p>
+    `,
+  },
+  {
+    img: C3Img,
+    img2: T3Img,
+    content: `
+      <p>青石嘴突袭战,</p>
+      <p>红军突袭青石嘴,</p>
+      <p>半小时歼敌两个连,</p>
+      <p>缴获战马百余匹,</p>
+      <p>遂组建中央红军首支骑兵侦察连。</p>
+    `,
+  },
+  {
+    img: C4Img,
+    img2: T4Img,
+    content: `
+      <p>翻越六盘山后,</p>
+      <p>毛泽东登顶六盘山,</p>
+      <p>远望长城,</p>
+      <p>夜宿小岔沟土炕,</p>
+      <p>即兴吟成《长征谣》初稿,</p>
+      <p>“不到长城非好汉”由此诞生。</p>
+    `,
+  },
+  {
+    img: C5Img,
+    img2: T5Img,
+    content: `
+      <p>过境彭阳县,</p>
+      <p>午后抵乔家渠,</p>
+      <p>毛泽东宿乔生魁家,</p>
+      <p>土炕上铺地图部署陕北行军,</p>
+      <p>房东悄悄塞给战士两筐土豆,</p>
+      <p>军民泪别。</p>
+    `,
+  },
+  {
+    img: C6Img,
+    img2: T6Img,
+    content: `
+      <p>出境固原,</p>
+      <p>拂晓从彭阳长城塬出发,</p>
+      <p>踏上奔赴陕北抗日的新征程。</p>
+    `,
+  },
+];

BIN
packages/pc/src/views/Home/images/bg-min.jpg


BIN
packages/pc/src/views/Home/images/c1-min.png


+ 0 - 0
packages/pc/src/views/Home/images/c2-min.png


Some files were not shown because too many files changed in this diff