chenlei 8 meses atrás
commit
ed53c53e5f
100 arquivos alterados com 2575 adições e 0 exclusões
  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. 0 0
      packages/base/src/constants.js
  10. 3 0
      packages/base/src/index.js
  11. 217 0
      packages/base/src/usePreloader.js
  12. 19 0
      packages/base/src/utils.js
  13. 30 0
      packages/mobile/.gitignore
  14. 13 0
      packages/mobile/index.html
  15. 8 0
      packages/mobile/jsconfig.json
  16. 33 0
      packages/mobile/package.json
  17. 22 0
      packages/mobile/postcss.config.js
  18. BIN
      packages/mobile/public/favicon.ico
  19. 16 0
      packages/mobile/src/App.vue
  20. BIN
      packages/mobile/src/assets/fonts/SOURCEHANSERIFCN-BOLD.OTF
  21. BIN
      packages/mobile/src/assets/fonts/SOURCEHANSERIFCN-REGULAR.OTF
  22. 112 0
      packages/mobile/src/assets/main.css
  23. 7 0
      packages/mobile/src/assets/utils.scss
  24. 20 0
      packages/mobile/src/main.js
  25. 14 0
      packages/mobile/src/router/index.js
  26. 25 0
      packages/mobile/src/stores/base.js
  27. 12 0
      packages/mobile/src/stores/counter.js
  28. 107 0
      packages/mobile/src/views/Home/constants.js
  29. BIN
      packages/mobile/src/views/Home/images/bg-min.jpg
  30. BIN
      packages/mobile/src/views/Home/images/c1-min.png
  31. BIN
      packages/mobile/src/views/Home/images/c2-min.png
  32. BIN
      packages/mobile/src/views/Home/images/c3-min.png
  33. BIN
      packages/mobile/src/views/Home/images/c4-min.png
  34. BIN
      packages/mobile/src/views/Home/images/c5-min.png
  35. BIN
      packages/mobile/src/views/Home/images/c6-min.png
  36. BIN
      packages/mobile/src/views/Home/images/line-active.png
  37. BIN
      packages/mobile/src/views/Home/images/m1-min.png
  38. BIN
      packages/mobile/src/views/Home/images/m2-min.png
  39. BIN
      packages/mobile/src/views/Home/images/m3-min.png
  40. BIN
      packages/mobile/src/views/Home/images/m4-min.png
  41. BIN
      packages/mobile/src/views/Home/images/map-min.png
  42. BIN
      packages/mobile/src/views/Home/images/point-bg-min.png
  43. BIN
      packages/mobile/src/views/Home/images/step.png
  44. BIN
      packages/mobile/src/views/Home/images/t1-min.png
  45. BIN
      packages/mobile/src/views/Home/images/t2-min.png
  46. BIN
      packages/mobile/src/views/Home/images/t3-min.png
  47. BIN
      packages/mobile/src/views/Home/images/t4-min.png
  48. BIN
      packages/mobile/src/views/Home/images/t5-min.png
  49. BIN
      packages/mobile/src/views/Home/images/t6-min.png
  50. BIN
      packages/mobile/src/views/Home/images/tab-bg.png
  51. BIN
      packages/mobile/src/views/Home/images/title-min.png
  52. 210 0
      packages/mobile/src/views/Home/index.scss
  53. 95 0
      packages/mobile/src/views/Home/index.vue
  54. 29 0
      packages/mobile/vite.config.js
  55. 1 0
      packages/pc/.env
  56. 30 0
      packages/pc/.gitignore
  57. 105 0
      packages/pc/index.html
  58. 8 0
      packages/pc/jsconfig.json
  59. 33 0
      packages/pc/package.json
  60. 23 0
      packages/pc/postcss.config.js
  61. BIN
      packages/pc/public/favicon.ico
  62. 37 0
      packages/pc/src/App.vue
  63. 7 0
      packages/pc/src/assets/elements.scss
  64. BIN
      packages/pc/src/assets/fonts/FZJinLing.TTF
  65. BIN
      packages/pc/src/assets/fonts/SOURCEHANSERIFCN-BOLD.OTF
  66. BIN
      packages/pc/src/assets/fonts/SourceHanSerifCN-Light.otf
  67. BIN
      packages/pc/src/assets/fonts/SourceHanSerifCN-Medium.otf
  68. BIN
      packages/pc/src/assets/fonts/SourceHanSerifCN-Regular.otf
  69. BIN
      packages/pc/src/assets/fonts/SourceHanSerifCN-SemiBold.otf
  70. BIN
      packages/pc/src/assets/images/btn_close-min.png
  71. BIN
      packages/pc/src/assets/images/logo.png
  72. 111 0
      packages/pc/src/assets/main.css
  73. 7 0
      packages/pc/src/assets/utils.scss
  74. 15 0
      packages/pc/src/main.js
  75. 32 0
      packages/pc/src/router/index.js
  76. 20 0
      packages/pc/src/stores/base.js
  77. 12 0
      packages/pc/src/stores/counter.js
  78. 0 0
      packages/pc/src/utils/constants.js
  79. 150 0
      packages/pc/src/views/Collection/detail.scss
  80. 86 0
      packages/pc/src/views/Collection/detail.vue
  81. 484 0
      packages/pc/src/views/Collection/list.scss
  82. 112 0
      packages/pc/src/views/Collection/list.vue
  83. BIN
      packages/pc/src/views/Cover/images/img_left-min.png
  84. BIN
      packages/pc/src/views/Cover/images/img_right-min.png
  85. BIN
      packages/pc/src/views/Cover/images/img_tip-min.png
  86. 96 0
      packages/pc/src/views/Cover/index.scss
  87. 49 0
      packages/pc/src/views/Cover/index.vue
  88. BIN
      packages/pc/src/views/Home/images/bg_01-min.jpg
  89. BIN
      packages/pc/src/views/Home/images/btn-min.png
  90. BIN
      packages/pc/src/views/Home/images/icon-1.png
  91. BIN
      packages/pc/src/views/Home/images/icon-2.png
  92. BIN
      packages/pc/src/views/Home/images/icon-3.png
  93. 56 0
      packages/pc/src/views/Home/index.scss
  94. 29 0
      packages/pc/src/views/Home/index.vue
  95. BIN
      packages/pc/src/views/Tree/images/bg-min.jpg
  96. BIN
      packages/pc/src/views/Tree/images/label-bg-min.png
  97. BIN
      packages/pc/src/views/Tree/images/label2-bg-min.png
  98. BIN
      packages/pc/src/views/Tree/images/label3-bg-min.png
  99. BIN
      packages/pc/src/views/Tree/images/label4-min.png
  100. 0 0
      packages/pc/src/views/Tree/images/line.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 @@
+ZHS2507181-1 山东博物馆-全年临展打包

+ 16 - 0
package.json

@@ -0,0 +1,16 @@
+{
+  "name": "shandong-2025-exhibition",
+  "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": "@shandong/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"
+  }
+}

+ 0 - 0
packages/base/src/constants.js


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

@@ -0,0 +1,3 @@
+export * from "./constants";
+export * from "./usePreloader";
+export * from "./utils";

+ 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,
+  };
+}

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

@@ -0,0 +1,19 @@
+export const isDevelopment = import.meta.env.MODE === "development";
+
+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/");
+  }
+}

+ 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"]
+}

+ 33 - 0
packages/mobile/package.json

@@ -0,0 +1,33 @@
+{
+  "name": "@shandong/mobile",
+  "version": "0.0.0",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "@shandong/base": "workspace:*",
+    "clipboard": "^2.0.11",
+    "pinia": "^2.2.6",
+    "swiper": "^11.2.10",
+    "vant": "^4.9.15",
+    "vue": "^3.5.13",
+    "vue-draggable-resizable": "^3.0.0",
+    "vue-qrcode": "^2.2.2",
+    "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: 1448;
+}
+</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 "@shandong/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: "/index",
+      name: "home",
+      component: () => import("../views/Home/index.vue"),
+    },
+  ],
+});
+
+export default router;

+ 25 - 0
packages/mobile/src/stores/base.js

@@ -0,0 +1,25 @@
+import { ref } from "vue";
+import { defineStore } from "pinia";
+import { parseUrlName } from "@shandong/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 Img7 from "../views/Home/images/line-active.png";
+
+export const RESOURCE_LIST = [Img1, Img2, Img3, Img4, Img5, 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/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 }
+})

+ 107 - 0
packages/mobile/src/views/Home/constants.js

@@ -0,0 +1,107 @@
+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 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,
+    href: "https://houseoss.4dkankan.com/project/guyuan/scene/index.html?m=SG-IDyBc0bleFF",
+  },
+  {
+    default: M2Img,
+    routeName: "antique",
+  },
+  {
+    default: M3Img,
+    routeName: "history",
+  },
+  {
+    type: "share",
+    default: M4Img,
+  },
+];
+
+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/mobile/src/views/Home/images/bg-min.jpg


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


BIN
packages/mobile/src/views/Home/images/c2-min.png


BIN
packages/mobile/src/views/Home/images/c3-min.png


BIN
packages/mobile/src/views/Home/images/c4-min.png


BIN
packages/mobile/src/views/Home/images/c5-min.png


BIN
packages/mobile/src/views/Home/images/c6-min.png


BIN
packages/mobile/src/views/Home/images/line-active.png


BIN
packages/mobile/src/views/Home/images/m1-min.png


BIN
packages/mobile/src/views/Home/images/m2-min.png


BIN
packages/mobile/src/views/Home/images/m3-min.png


BIN
packages/mobile/src/views/Home/images/m4-min.png


BIN
packages/mobile/src/views/Home/images/map-min.png


BIN
packages/mobile/src/views/Home/images/point-bg-min.png


BIN
packages/mobile/src/views/Home/images/step.png


BIN
packages/mobile/src/views/Home/images/t1-min.png


BIN
packages/mobile/src/views/Home/images/t2-min.png


BIN
packages/mobile/src/views/Home/images/t3-min.png


BIN
packages/mobile/src/views/Home/images/t4-min.png


BIN
packages/mobile/src/views/Home/images/t5-min.png


BIN
packages/mobile/src/views/Home/images/t6-min.png


BIN
packages/mobile/src/views/Home/images/tab-bg.png


BIN
packages/mobile/src/views/Home/images/title-min.png


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

@@ -0,0 +1,210 @@
+@use "@/assets/utils.scss";
+
+.home {
+  position: absolute;
+  inset: 0;
+  overflow: hidden;
+  background: url("./images/bg-min.jpg") no-repeat center / cover;
+
+  &::after {
+    content: "";
+    position: absolute;
+    top: utils.vh-calc(42);
+    left: utils.vh-calc(8);
+    width: utils.vh-calc(158);
+    height: utils.vh-calc(112);
+    background: url("@/assets/images/logo-min.png") no-repeat center / contain;
+    z-index: 9;
+  }
+  &::before {
+    content: "";
+    position: absolute;
+    top: utils.vh-calc(0);
+    right: utils.vh-calc(26);
+    width: utils.vh-calc(187);
+    height: utils.vh-calc(510);
+    background: url("./images/title-min.png") no-repeat center / contain;
+    z-index: 9;
+  }
+  &-menu {
+    position: fixed;
+    left: utils.vh-calc(28);
+    bottom: utils.vh-calc(256);
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    padding: utils.vh-calc(10) 0;
+    width: utils.vh-calc(104);
+    height: utils.vh-calc(478);
+    border-radius: 4px;
+    background: rgba($color: #000000, $alpha: 0.2);
+
+    li {
+      img {
+        width: 100%;
+      }
+    }
+  }
+  &-tab {
+    position: fixed;
+    right: utils.vw-calc(30);
+    bottom: utils.vh-calc(35);
+    width: utils.vh-calc(429);
+    height: utils.vh-calc(248);
+    background: url("./images/tab-bg.png") no-repeat center / contain;
+    z-index: 5;
+
+    > div {
+      position: absolute;
+      cursor: pointer;
+    }
+    .story {
+      top: utils.vh-calc(34);
+      left: utils.vh-calc(100);
+      width: utils.vh-calc(200);
+      height: utils.vh-calc(85);
+    }
+    .biographies {
+      left: utils.vh-calc(100);
+      bottom: utils.vh-calc(35);
+      width: utils.vh-calc(220);
+      height: utils.vh-calc(80);
+    }
+  }
+  .vdr {
+    border: 0;
+  }
+  &__map {
+    width: 100%;
+    height: 100%;
+  }
+  &-point {
+    position: absolute;
+    width: utils.vw-calc(103);
+    height: utils.vw-calc(252);
+
+    &.point0 {
+      top: utils.vw-calc(780);
+      left: utils.vw-calc(460);
+    }
+    &.point1 {
+      top: utils.vw-calc(630);
+      left: utils.vw-calc(620);
+    }
+    &.point2 {
+      top: utils.vw-calc(560);
+      left: utils.vw-calc(340);
+    }
+    &.point3 {
+      top: utils.vw-calc(320);
+      left: utils.vw-calc(250);
+    }
+    &.point4 {
+      right: utils.vw-calc(110);
+      bottom: utils.vw-calc(340);
+    }
+    &.point5 {
+      right: utils.vw-calc(390);
+      bottom: utils.vw-calc(330);
+    }
+    img {
+      position: absolute;
+      left: 50%;
+      bottom: utils.vw-calc(85);
+      width: utils.vw-calc(103);
+      transform: translateX(-50%);
+    }
+    &::after {
+      content: "";
+      position: absolute;
+      left: 50%;
+      bottom: utils.vw-calc(0);
+      width: utils.vw-calc(100);
+      height: utils.vw-calc(100);
+      transform: translateX(-50%);
+      background: url("./images/step.png") no-repeat center / contain;
+    }
+    &.active {
+      z-index: 9;
+
+      img {
+        display: none;
+      }
+      &::after {
+        display: none;
+      }
+      &::before {
+        content: "";
+        position: absolute;
+        left: 50%;
+        bottom: utils.vw-calc(-70);
+        width: utils.vw-calc(360);
+        height: utils.vw-calc(600);
+        transform: translateX(-50%);
+        background: url("./images/line-active.png") no-repeat center / contain;
+      }
+    }
+  }
+}
+
+.point-panel {
+  display: flex;
+  align-items: center;
+  gap: 30px;
+  padding: 25px 70px 80px;
+  background: rgba(0, 0, 0, 0.8);
+  backdrop-filter: blur(20px);
+  box-shadow: 0 -8px 16px rgba(0, 0, 0, 0.6);
+
+  &-img {
+    position: relative;
+    width: utils.vw-calc(173);
+    height: utils.vw-calc(281);
+    background: url("./images/point-bg-min.png") no-repeat center / contain;
+
+    img {
+      position: relative;
+    }
+    .c0 {
+      top: utils.vw-calc(20);
+      left: utils.vw-calc(22);
+      width: utils.vw-calc(139);
+      height: utils.vw-calc(227);
+    }
+    .c1 {
+      top: utils.vw-calc(20);
+      left: utils.vw-calc(22);
+      width: utils.vw-calc(114);
+      height: utils.vw-calc(229);
+    }
+    .c2 {
+      top: utils.vw-calc(20);
+      left: utils.vw-calc(22);
+      width: utils.vw-calc(139);
+      height: utils.vw-calc(227);
+    }
+    .c3 {
+      top: utils.vw-calc(20);
+      left: utils.vw-calc(22);
+      width: utils.vw-calc(108);
+      height: utils.vw-calc(222);
+    }
+    .c4 {
+      top: utils.vw-calc(20);
+      left: utils.vw-calc(22);
+      width: utils.vw-calc(133);
+      height: utils.vw-calc(227);
+    }
+    .c5 {
+      top: utils.vw-calc(20);
+      left: utils.vw-calc(22);
+      width: utils.vw-calc(104);
+      height: utils.vw-calc(231);
+    }
+  }
+  &-content {
+    color: white;
+    font-size: 24px;
+    line-height: 56px;
+  }
+}

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

@@ -0,0 +1,95 @@
+<template>
+  <div class="home">
+    <VueDraggableResizable
+      :x="mapX"
+      :y="mapY"
+      :w="mapWidth"
+      :h="mapHeight"
+      :draggable="true"
+      :resizable="true"
+      :handles="[]"
+    >
+      <img class="home__map" src="./images/map-min.png" />
+
+      <div
+        v-for="(item, index) in POINTS"
+        :key="index"
+        class="home-point"
+        :class="[{ active: pointIndex === index }, `point${index}`]"
+        @click="handlePoint(index)"
+      >
+        <img :src="item.img2" />
+      </div>
+    </VueDraggableResizable>
+
+    <ul class="home-menu">
+      <li v-for="(item, index) in MIMAGES" :key="index">
+        <img :src="item.default" @click="handleMenu(index)" />
+      </li>
+    </ul>
+
+    <div class="home-tab">
+      <div class="story" />
+      <div class="biographies" @click="$router.push({ name: 'story' })" />
+    </div>
+  </div>
+
+  <SharePopup v-model:visible="shareVisible" />
+  <VanPopup
+    v-model:show="pointDetailVisible"
+    :overlay="true"
+    position="bottom"
+    class="point-panel"
+    :overlay-style="{
+      background: 'transparent',
+    }"
+    @closed="pointIndex = -1"
+  >
+    <template v-if="activePoint">
+      <div class="point-panel-img">
+        <img :class="`c${pointIndex}`" :src="activePoint.img" />
+      </div>
+      <div class="point-panel-content" v-html="activePoint.content" />
+    </template>
+  </VanPopup>
+</template>
+
+<script setup>
+import { ref, computed } from "vue";
+import VueDraggableResizable from "vue-draggable-resizable";
+import "vue-draggable-resizable/style.css";
+import { POINTS, MIMAGES } from "./constants";
+import SharePopup from "@/components/SharePopup.vue";
+import { useRouter } from "vue-router";
+
+const mapWidth = window.innerWidth * (1600 / 750);
+const mapHeight = window.innerWidth * (1215 / 750);
+const mapX = window.innerWidth * (30 / 750);
+const mapY = window.innerWidth * (80 / 750);
+
+const router = useRouter();
+const pointIndex = ref(-1);
+const shareVisible = ref(false);
+const pointDetailVisible = ref(false);
+const activePoint = computed(() => POINTS[pointIndex.value]);
+
+const handlePoint = (index) => {
+  pointIndex.value = index;
+  pointDetailVisible.value = true;
+};
+
+const handleMenu = (index) => {
+  const item = MIMAGES[index];
+  if (item.routeName) {
+    router.push({ name: item.routeName, query: item.query });
+  } else if (item.href) {
+    window.location.href = item.href;
+  } else if (item.type === "share") {
+    shareVisible.value = true;
+  }
+};
+</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)),
+    },
+  },
+});

+ 1 - 0
packages/pc/.env

@@ -0,0 +1 @@
+VITE_PUBLIC_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

+ 105 - 0
packages/pc/index.html

@@ -0,0 +1,105 @@
+<!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>
+    <style>
+      * {
+        margin: 0;
+        padding: 0;
+        box-sizing: border-box;
+      }
+
+      #app-container {
+        width: 100vw;
+        height: 100vh;
+        overflow-y: auto;
+      }
+
+      #app {
+        width: 100%;
+        height: 100%;
+      }
+
+      @media (max-width: 768px) and (orientation: portrait) {
+        body:not(.in-iframe) #app-container {
+          position: fixed;
+          top: 50%;
+          left: 50%;
+          width: 100vh;
+          height: 100vw;
+          transform: translate(-50%, -50%) rotate(90deg);
+          transform-origin: center center;
+        }
+
+        body:not(.in-iframe) #app {
+          display: none;
+        }
+
+        body:not(.in-iframe) #app-iframe {
+          display: block;
+          width: 100%;
+          height: 100%;
+          border: none;
+        }
+      }
+
+      body.in-iframe #app-container {
+        width: 100%;
+        height: 100%;
+      }
+
+      body.in-iframe #app-iframe {
+        display: none;
+      }
+
+      #app-iframe {
+        display: none;
+      }
+    </style>
+  </head>
+  <body>
+    <div id="app-container">
+      <div id="app"></div>
+      <iframe id="app-iframe"></iframe>
+    </div>
+    <script>
+      if (window.self !== window.top) {
+        document.body.classList.add("in-iframe");
+      }
+
+      if (window.self === window.top) {
+        function handleOrientation() {
+          const app = document.getElementById("app");
+          const iframe = document.getElementById("app-iframe");
+
+          if (
+            window.innerWidth <= 768 &&
+            window.innerHeight > window.innerWidth
+          ) {
+            app.style.display = "none";
+            iframe.style.display = "block";
+            if (!iframe.src) {
+              const url = new URL(window.location.href);
+              url.searchParams.set("iframe", "1");
+              iframe.src = url.toString();
+            }
+          } else {
+            app.style.display = "block";
+            iframe.style.display = "none";
+          }
+        }
+
+        handleOrientation();
+
+        window.addEventListener("resize", handleOrientation);
+        window.addEventListener("orientationchange", () => {
+          setTimeout(handleOrientation, 100);
+        });
+      }
+    </script>
+    <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"]
+}

+ 33 - 0
packages/pc/package.json

@@ -0,0 +1,33 @@
+{
+  "name": "@shandong/pc",
+  "version": "0.0.0",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "@shandong/base": "workspace:*",
+    "@element-plus/icons-vue": "^2.3.1",
+    "@vueuse/core": "^12.0.0",
+    "animate.css": "^4.1.1",
+    "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


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

@@ -0,0 +1,37 @@
+<script setup>
+import { RouterView } from "vue-router";
+</script>
+
+<template>
+  <RouterView />
+</template>
+
+<style lang="scss">
+@use "./assets/utils.scss";
+
+:root {
+  --design-width: 1921;
+  --design-height: 971;
+}
+
+.el-image {
+  width: 100%;
+  height: 100%;
+}
+
+#app {
+  font-size: 16px;
+}
+
+.back {
+  position: fixed;
+  top: utils.vh-calc(28);
+  left: 34px;
+  width: utils.vh-calc(99);
+  height: utils.vh-calc(42);
+  background: url("./assets/images/btn_close-min.png") no-repeat center /
+    contain;
+  cursor: pointer;
+  z-index: 1;
+}
+</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/FZJinLing.TTF


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


BIN
packages/pc/src/assets/fonts/SourceHanSerifCN-Light.otf


BIN
packages/pc/src/assets/fonts/SourceHanSerifCN-Medium.otf


BIN
packages/pc/src/assets/fonts/SourceHanSerifCN-Regular.otf


BIN
packages/pc/src/assets/fonts/SourceHanSerifCN-SemiBold.otf


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


BIN
packages/pc/src/assets/images/logo.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: "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("@/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)));
+}

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

@@ -0,0 +1,15 @@
+import "./assets/main.css";
+import "animate.css";
+
+import { createApp } from "vue";
+import { createPinia } from "pinia";
+
+import App from "./App.vue";
+import router from "./router";
+
+const app = createApp(App);
+
+app.use(createPinia());
+app.use(router);
+
+app.mount("#app");

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

@@ -0,0 +1,32 @@
+import { createRouter, createWebHashHistory } from "vue-router";
+
+const router = createRouter({
+  history: createWebHashHistory(import.meta.env.BASE_URL),
+  routes: [
+    {
+      path: "/",
+      name: "cover",
+      component: () => import("../views/Cover/index.vue"),
+    },
+    {
+      path: "/index",
+      name: "home",
+      component: () => import("../views/Home/index.vue"),
+    },
+    {
+      path: "/tree",
+      name: "tree",
+      component: () => import("../views/Tree/index.vue"),
+    },
+  ],
+  scrollBehavior(to, from, savedPosition) {
+    // 如果有保存的位置(浏览器前进/后退),则使用保存的位置
+    if (savedPosition) {
+      return savedPosition;
+    }
+    // 否则滚动到顶部
+    return { top: 0 };
+  },
+});
+
+export default router;

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

@@ -0,0 +1,20 @@
+import { ref } from "vue";
+import { defineStore } from "pinia";
+import { parseUrlName } from "@shandong/base";
+import Img1 from "../views/Home/images/bg_01-min.jpg";
+
+export const RESOURCE_LIST = [Img1];
+
+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 }
+})

+ 0 - 0
packages/pc/src/utils/constants.js


+ 150 - 0
packages/pc/src/views/Collection/detail.scss

@@ -0,0 +1,150 @@
+@use "@/assets/utils.scss";
+
+.step2-detail {
+  position: absolute;
+  inset: 0;
+  display: flex;
+  justify-content: stretch;
+  padding-top: 73px;
+  height: 100%;
+
+  &__back {
+    position: absolute;
+    top: 93px;
+    left: 20px;
+    width: utils.vh-calc(40);
+    height: utils.vh-calc(40);
+    cursor: pointer;
+    z-index: 1;
+    background: url("@/assets/images/back.png") no-repeat center / contain;
+  }
+  &-sidebar {
+    position: relative;
+    width: 584px;
+    flex-shrink: 0;
+    background: url("./images/detail/bg3-min.jpg") no-repeat center / cover;
+
+    &-title {
+      position: absolute;
+      top: utils.vh-calc(105);
+      left: 90px;
+      writing-mode: vertical-lr;
+      color: #d6d3ca;
+      letter-spacing: 2px;
+
+      &::before {
+        content: "";
+        position: absolute;
+        top: utils.vh-calc(-48);
+        left: utils.vh-calc(12);
+        width: utils.vh-calc(24);
+        height: utils.vh-calc(25);
+        background: url("./images/detail/seal.png") no-repeat center / contain;
+      }
+      p:first-child {
+        font-family: "SourceHanSerifSC-Bold";
+        font-size: utils.vh-calc(34);
+      }
+      p:last-child {
+        font-size: utils.vh-calc(21);
+      }
+    }
+    &-img {
+      position: absolute;
+      top: utils.vh-calc(139);
+      left: 210px;
+      width: 570px;
+      height: utils.vh-calc(680);
+      animation: fadeInLeft20 0.5s ease-out 0.5s both;
+      object-fit: contain;
+    }
+  }
+  &-swiper {
+    position: absolute;
+    left: 20px;
+    right: 20px;
+    bottom: utils.vh-calc(57);
+
+    :deep(.swiper-slide) {
+      width: 142px;
+      height: 92px;
+    }
+  }
+  &-content {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    padding: utils.vh-calc(90) 165px utils.vh-calc(110) 220px;
+    background: url("./images/detail/bg2-min.jpg") no-repeat center / cover;
+  }
+  &-epigraph {
+    position: relative;
+    margin-bottom: utils.vh-calc(90);
+    animation: fadeInLeft20 0.5s ease-out 1s both;
+
+    &::before {
+      content: "";
+      position: absolute;
+      top: utils.vh-calc(82);
+      left: utils.vh-calc(-413);
+      width: utils.vh-calc(394);
+      height: utils.vh-calc(275);
+      background: url("./images/detail/guid-min.png") no-repeat center / contain;
+    }
+    &-content {
+      margin-top: utils.vh-calc(40);
+      display: flex;
+      gap: utils.vh-calc(11);
+
+      > div {
+        background: rgba(255, 255, 255, 0.5);
+      }
+      &-img {
+        padding: utils.vh-calc(15);
+        width: utils.vh-calc(276);
+        height: utils.vh-calc(157);
+      }
+      &-text {
+        padding: utils.vh-calc(15) utils.vh-calc(20);
+        width: utils.vh-calc(298);
+        height: utils.vh-calc(238);
+        font-size: utils.vh-calc(12);
+        color: #313131;
+      }
+    }
+  }
+  &-label {
+    position: relative;
+    padding-left: utils.vh-calc(50);
+    font-size: utils.vh-calc(29);
+    color: #383838;
+    letter-spacing: 5px;
+    font-family: "SourceHanSerifSC-Bold";
+
+    &::before {
+      content: "";
+      position: absolute;
+      top: 50%;
+      left: utils.vh-calc(14);
+      width: utils.vh-calc(24);
+      height: utils.vh-calc(25);
+      transform: translateY(-50%);
+      background: url("./images/detail/seal.png") no-repeat center / contain;
+    }
+  }
+  &-desc {
+    flex: 1;
+    height: 0;
+    animation: fadeInLeft20 0.5s ease-out 1s both;
+
+    &-text {
+      padding-right: 40px;
+      margin-top: utils.vh-calc(38);
+      font-size: utils.vh-calc(16);
+      color: #444444;
+      line-height: utils.vh-calc(28);
+      text-indent: 2em;
+    }
+  }
+}

+ 86 - 0
packages/pc/src/views/Collection/detail.vue

@@ -0,0 +1,86 @@
+<template>
+  <div class="step2-detail">
+    <div class="step2-detail__back" @click="$router.back()" />
+
+    <div class="step2-detail-sidebar">
+      <div
+        class="step2-detail-sidebar-title animate__animated animate__duration-200 animate__fadeInLeft"
+      >
+        <p>{{ detail.realName }}</p>
+        <p>{{ detail.era }}</p>
+      </div>
+
+      <img
+        draggable="false"
+        class="step2-detail-sidebar-img"
+        :src="baseUrl + detail.img"
+      />
+
+      <swiper
+        :modules="[FreeMode, Mousewheel]"
+        :slides-per-view="'auto'"
+        :space-between="7"
+        :free-mode="true"
+        :mousewheel="{
+          enabled: true,
+          forceToAxis: false,
+        }"
+        class="step2-detail-swiper"
+      >
+        <swiper-slide v-for="(item, index) in detail.imgs" :key="item.thumb">
+          <el-image
+            :src="baseUrl + item.thumb"
+            fit="cover"
+            preview-teleported
+            :preview-src-list="detail.imgs.map((item) => baseUrl + item.img)"
+            :initial-index="index"
+          />
+        </swiper-slide>
+      </swiper>
+    </div>
+
+    <div class="step2-detail-content">
+      <div v-if="detail.epigraph" class="step2-detail-epigraph">
+        <p class="step2-detail-label">铭文详情</p>
+        <div class="step2-detail-epigraph-content">
+          <div class="step2-detail-epigraph-content-img">
+            <el-image
+              preview-teleported
+              fit="contain"
+              :src="baseUrl + detail.epigraph.thumb"
+              :preview-src-list="[baseUrl + detail.epigraph.img]"
+            />
+          </div>
+          <el-scrollbar class="step2-detail-epigraph-content-text">
+            <p>{{ detail.epigraph.text }}</p>
+          </el-scrollbar>
+        </div>
+      </div>
+
+      <div class="step2-detail-desc">
+        <p class="step2-detail-label">文字介绍</p>
+
+        <el-scrollbar class="step2-detail-desc-text">
+          <div v-html="detail.content" />
+        </el-scrollbar>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { useRoute } from "vue-router";
+import { IRON_BULL_LIST, MONSTER_LIST, BUILDING_LIST } from "@canal/base";
+import { Swiper, SwiperSlide } from "swiper/vue";
+import { FreeMode, Mousewheel } from "swiper/modules";
+import "swiper/css";
+
+const route = useRoute();
+const baseUrl = import.meta.env.VITE_PUBLIC_URL;
+const LIST = [IRON_BULL_LIST, MONSTER_LIST, BUILDING_LIST];
+const detail = LIST[route.params.id - 1][route.params.sid - 1];
+</script>
+
+<style scoped lang="scss">
+@use "./detail.scss";
+</style>

+ 484 - 0
packages/pc/src/views/Collection/list.scss

@@ -0,0 +1,484 @@
+@use "@/assets/utils.scss";
+
+.list {
+  position: absolute;
+  inset: 0;
+  display: flex;
+  flex-direction: column;
+  padding-top: 73px;
+  height: 100%;
+
+  &-content {
+    flex: 1;
+    background: linear-gradient(to bottom, #c5b79b, #f6efdb 80%);
+
+    &-swiper {
+      padding-right: utils.vh-calc(200);
+      width: 100%;
+      height: 100%;
+    }
+
+    &-slide {
+      position: relative;
+      width: utils.vh-calc(466);
+      height: 100%;
+      flex-shrink: 0;
+      border-left: 2px solid #bdac92;
+      cursor: pointer;
+      transition: background-image 0.2s ease-out;
+      background: url("./images/detail/bg-min.png") no-repeat center / cover;
+
+      &:first-child {
+        border-left: none;
+      }
+      &:hover {
+        z-index: 1;
+
+        &::after {
+          display: none;
+        }
+        .list-content-slide-mask {
+          display: block;
+        }
+      }
+      &:nth-child(even) {
+        .list-content-slide-title {
+          top: utils.vh-calc(138);
+        }
+        .list-content-slide-mask {
+          &-content {
+            top: unset;
+            bottom: utils.vh-calc(140);
+          }
+        }
+      }
+      &-mask {
+        display: none;
+        position: absolute;
+        inset: 0;
+        background-image: url("./images/detail/bg-hover-min.png");
+        z-index: 1;
+
+        .list-content-slide-title {
+          color: #d6d3ca;
+        }
+        &-content {
+          position: absolute;
+          top: utils.vh-calc(147);
+          left: 50%;
+          margin-left: utils.vh-calc(-177);
+          width: utils.vh-calc(354);
+          font-size: utils.vh-calc(16);
+          color: #eeebe1;
+          line-height: utils.vh-calc(28);
+          text-indent: 2em;
+          opacity: 0.72;
+        }
+        &::after {
+          content: "";
+          position: absolute;
+          animation: fadeInLeft20 ease-out 0.5s both;
+        }
+      }
+      &::after {
+        content: "";
+        position: absolute;
+        animation: fadeInLeft20 0.6s ease-out 0.2s both;
+      }
+      &-title {
+        position: absolute;
+        top: utils.vh-calc(490);
+        left: utils.vh-calc(20);
+        writing-mode: vertical-lr;
+        line-height: utils.vh-calc(40);
+        letter-spacing: 4px;
+
+        p:first-child {
+          font-size: utils.vh-calc(25);
+          font-family: "SourceHanSerifSC-Bold";
+        }
+        p:last-child {
+          position: relative;
+          top: utils.vh-calc(5);
+          font-size: utils.vh-calc(15);
+        }
+        &::after {
+          content: "";
+          position: absolute;
+          top: utils.vh-calc(-41);
+          left: utils.vh-calc(8);
+          width: utils.vh-calc(24);
+          height: utils.vh-calc(25);
+          background: url("./images/detail/seal.png") no-repeat center / contain;
+        }
+      }
+    }
+  }
+  &-tabbar {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    height: utils.vh-calc(113);
+    background: url("./images/detail/footer-bg-min.jpg") no-repeat center /
+      cover;
+
+    ul {
+      display: flex;
+      gap: 120px;
+
+      li {
+        width: 229px;
+        height: 64px;
+        font-size: 24px;
+        color: #5e4f38;
+        text-align: center;
+        line-height: 60px;
+        background: url("./images/detail/tab-min.png") no-repeat center /
+          contain;
+        cursor: pointer;
+
+        &.active {
+          color: #e4dcc8;
+          background-image: url("./images/detail/tab-active-min.png");
+        }
+      }
+    }
+  }
+}
+
+.iron {
+  &::before {
+    content: "";
+    position: absolute;
+    top: utils.vh-calc(88);
+    left: utils.vh-calc(40);
+    width: utils.vh-calc(58);
+    height: utils.vh-calc(246);
+    background: url("./images/detail/label1-min.png") no-repeat center / cover;
+    z-index: 9;
+  }
+
+  .list-content-slide {
+    &:nth-child(1) {
+      &::after,
+      .list-content-slide-mask::after {
+        left: utils.vh-calc(87);
+        bottom: utils.vh-calc(53);
+        width: utils.vh-calc(564);
+        height: utils.vh-calc(391);
+        background: url("./images/detail/1-1-min.png") no-repeat center /
+          contain;
+      }
+
+      .list-content-slide-mask {
+        &::after {
+          background-image: url("./images/detail/1-1-1-min.png");
+        }
+      }
+    }
+    &:nth-child(2) {
+      &::after,
+      .list-content-slide-mask::after {
+        top: utils.vh-calc(123);
+        left: utils.vh-calc(103);
+        width: utils.vh-calc(446);
+        height: utils.vh-calc(243);
+        background: url("./images/detail/1-2-min.png") no-repeat center /
+          contain;
+      }
+
+      .list-content-slide-mask {
+        &::after {
+          background-image: url("./images/detail/1-2-1-min.png");
+        }
+      }
+    }
+    &:nth-child(3) {
+      &::after,
+      .list-content-slide-mask::after {
+        left: utils.vh-calc(87);
+        bottom: utils.vh-calc(87);
+        width: utils.vh-calc(537);
+        height: utils.vh-calc(263);
+        background: url("./images/detail/1-3-min.png") no-repeat center /
+          contain;
+      }
+
+      .list-content-slide-mask {
+        &::after {
+          background-image: url("./images/detail/1-3-1-min.png");
+        }
+      }
+    }
+    &:nth-child(4) {
+      &::after,
+      .list-content-slide-mask::after {
+        top: utils.vh-calc(98);
+        left: utils.vh-calc(143);
+        width: utils.vh-calc(358);
+        height: utils.vh-calc(403);
+        background: url("./images/detail/1-4-min.png") no-repeat center /
+          contain;
+      }
+
+      .list-content-slide-mask {
+        &::after {
+          background-image: url("./images/detail/1-4-1-min.png");
+        }
+      }
+    }
+    &:nth-child(5) {
+      &::after,
+      .list-content-slide-mask::after {
+        left: utils.vh-calc(44);
+        bottom: utils.vh-calc(86);
+        width: utils.vh-calc(531);
+        height: utils.vh-calc(301);
+        background: url("./images/detail/1-5-min.png") no-repeat center /
+          contain;
+      }
+
+      .list-content-slide-mask {
+        &::after {
+          background-image: url("./images/detail/1-5-1-min.png");
+        }
+      }
+    }
+  }
+}
+
+.monster {
+  &::before {
+    content: "";
+    position: absolute;
+    top: utils.vh-calc(88);
+    left: utils.vh-calc(40);
+    width: utils.vh-calc(58);
+    height: utils.vh-calc(242);
+    background: url("./images/detail/label2-min.png") no-repeat center / cover;
+    z-index: 9;
+  }
+
+  .list-content-slide {
+    &:nth-child(1) {
+      &::after,
+      .list-content-slide-mask::after {
+        left: utils.vh-calc(70);
+        bottom: utils.vh-calc(70);
+        width: utils.vh-calc(543);
+        height: utils.vh-calc(236);
+        background: url("./images/detail/2-1-min.png") no-repeat center /
+          contain;
+      }
+
+      .list-content-slide-mask {
+        &::after {
+          background-image: url("./images/detail/2-1-1-min.png");
+        }
+      }
+    }
+    &:nth-child(2) {
+      &::after,
+      .list-content-slide-mask::after {
+        top: utils.vh-calc(123);
+        left: utils.vh-calc(90);
+        width: utils.vh-calc(448);
+        height: utils.vh-calc(344);
+        background: url("./images/detail/2-2-min.png") no-repeat center /
+          contain;
+      }
+
+      .list-content-slide-mask {
+        &::after {
+          background-image: url("./images/detail/2-2-1-min.png");
+        }
+      }
+    }
+    &:nth-child(3) {
+      &::after,
+      .list-content-slide-mask::after {
+        left: utils.vh-calc(78);
+        bottom: utils.vh-calc(61);
+        width: utils.vh-calc(480);
+        height: utils.vh-calc(235);
+        background: url("./images/detail/2-3-min.png") no-repeat center /
+          contain;
+      }
+
+      .list-content-slide-mask {
+        &::after {
+          background-image: url("./images/detail/2-3-1-min.png");
+        }
+      }
+    }
+    &:nth-child(4) {
+      &::after,
+      .list-content-slide-mask::after {
+        top: utils.vh-calc(208);
+        left: utils.vh-calc(62);
+        width: utils.vh-calc(507);
+        height: utils.vh-calc(207);
+        background: url("./images/detail/2-4-min.png") no-repeat center /
+          contain;
+      }
+
+      .list-content-slide-mask {
+        &::after {
+          background-image: url("./images/detail/2-4-1-min.png");
+        }
+      }
+    }
+    &:nth-child(5) {
+      &::after,
+      .list-content-slide-mask::after {
+        left: utils.vh-calc(80);
+        bottom: utils.vh-calc(125);
+        width: utils.vh-calc(509);
+        height: utils.vh-calc(202);
+        background: url("./images/detail/2-5-min.png") no-repeat center /
+          contain;
+      }
+
+      .list-content-slide-mask {
+        &::after {
+          background-image: url("./images/detail/2-5-1-min.png");
+        }
+      }
+    }
+    &:nth-child(6) {
+      &::after,
+      .list-content-slide-mask::after {
+        top: utils.vh-calc(178);
+        left: utils.vh-calc(77);
+        width: utils.vh-calc(469);
+        height: utils.vh-calc(203);
+        background: url("./images/detail/2-6-min.png") no-repeat center /
+          contain;
+      }
+
+      .list-content-slide-mask {
+        &::after {
+          background-image: url("./images/detail/2-6-1-min.png");
+        }
+      }
+    }
+    &:nth-child(7) {
+      &::after,
+      .list-content-slide-mask::after {
+        left: utils.vh-calc(100);
+        bottom: utils.vh-calc(109);
+        width: utils.vh-calc(434);
+        height: utils.vh-calc(256);
+        background: url("./images/detail/2-7-min.png") no-repeat center /
+          contain;
+      }
+
+      .list-content-slide-mask {
+        &::after {
+          background-image: url("./images/detail/2-7-1-min.png");
+        }
+      }
+    }
+    &:nth-child(8) {
+      &::after,
+      .list-content-slide-mask::after {
+        top: utils.vh-calc(148);
+        left: utils.vh-calc(79);
+        width: utils.vh-calc(490);
+        height: utils.vh-calc(234);
+        background: url("./images/detail/2-8-min.png") no-repeat center /
+          contain;
+      }
+
+      .list-content-slide-mask {
+        &::after {
+          background-image: url("./images/detail/2-8-1-min.png");
+        }
+      }
+    }
+    &:nth-child(9) {
+      &::after,
+      .list-content-slide-mask::after {
+        left: utils.vh-calc(95);
+        bottom: utils.vh-calc(78);
+        width: utils.vh-calc(434);
+        height: utils.vh-calc(340);
+        background: url("./images/detail/2-9-min.png") no-repeat center /
+          contain;
+      }
+
+      .list-content-slide-mask {
+        &::after {
+          background-image: url("./images/detail/2-9-1-min.png");
+        }
+      }
+    }
+  }
+}
+
+.building {
+  &::before {
+    content: "";
+    position: absolute;
+    top: utils.vh-calc(88);
+    left: utils.vh-calc(40);
+    width: utils.vh-calc(140);
+    height: utils.vh-calc(255);
+    background: url("./images/detail/label3-min.png") no-repeat center / cover;
+    z-index: 9;
+  }
+
+  .list-content-slide {
+    &:nth-child(1) {
+      &::after,
+      .list-content-slide-mask::after {
+        left: utils.vh-calc(86);
+        bottom: utils.vh-calc(205);
+        width: utils.vh-calc(505);
+        height: utils.vh-calc(31);
+        background: url("./images/detail/3-1-min.png") no-repeat center /
+          contain;
+      }
+
+      .list-content-slide-mask {
+        &::after {
+          background-image: url("./images/detail/3-1-1-min.png");
+        }
+      }
+    }
+    &:nth-child(2) {
+      &::after,
+      .list-content-slide-mask::after {
+        top: utils.vh-calc(37);
+        left: utils.vh-calc(1);
+        width: utils.vh-calc(535);
+        height: utils.vh-calc(401);
+        background: url("./images/detail/3-2-min.png") no-repeat center /
+          contain;
+      }
+
+      .list-content-slide-mask {
+        &::after {
+          background-image: url("./images/detail/3-2-1-min.png");
+        }
+      }
+    }
+    &:nth-child(3) {
+      &::after,
+      .list-content-slide-mask::after {
+        left: utils.vh-calc(56);
+        bottom: utils.vh-calc(133);
+        width: utils.vh-calc(519);
+        height: utils.vh-calc(248);
+        background: url("./images/detail/3-3-min.png") no-repeat center /
+          contain;
+      }
+
+      .list-content-slide-mask {
+        &::after {
+          background-image: url("./images/detail/3-3-1-min.png");
+        }
+      }
+    }
+  }
+}

+ 112 - 0
packages/pc/src/views/Collection/list.vue

@@ -0,0 +1,112 @@
+<template>
+  <div class="list" :class="CLASS_MAP[$route.params.id]?.class">
+    <div class="list-content">
+      <swiper
+        :modules="modules"
+        :slides-per-view="'auto'"
+        :space-between="0"
+        :mousewheel="{
+          enabled: true,
+          forceToAxis: false,
+        }"
+        :free-mode="true"
+        class="list-content-swiper"
+        @swiper="onSwiper"
+      >
+        <swiper-slide
+          v-for="(item, index) in CLASS_MAP[$route.params.id]?.list"
+          :key="item.name"
+          class="list-content-slide"
+          @click="
+            $router.push({
+              name: 'step2Detail',
+              params: { id: $route.params.id, sid: index + 1 },
+            })
+          "
+        >
+          <div
+            class="list-content-slide-title animate__animated animate__duration-200 animate__fadeInLeft"
+          >
+            <p>{{ item.realName }}</p>
+            <p>{{ item.era }}</p>
+          </div>
+
+          <div class="list-content-slide-mask">
+            <div class="list-content-slide-title">
+              <p>{{ item.realName }}</p>
+              <p>{{ item.era }}</p>
+            </div>
+
+            <div class="list-content-slide-mask-content">
+              <p>{{ item.desc }}</p>
+              <p>...</p>
+            </div>
+          </div>
+        </swiper-slide>
+      </swiper>
+    </div>
+
+    <div class="list-tabbar">
+      <ul>
+        <li
+          v-for="(tab, index) in TABS"
+          :key="tab"
+          :class="{ active: index + 1 === Number($route.params.id) }"
+          @click="
+            $router.push({ name: 'step2List', params: { id: index + 1 } })
+          "
+        >
+          {{ tab }}
+        </li>
+      </ul>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, watch } from "vue";
+import { useRoute } from "vue-router";
+import { IRON_BULL_LIST, MONSTER_LIST, BUILDING_LIST } from "@canal/base";
+import { Swiper, SwiperSlide } from "swiper/vue";
+import { FreeMode, Mousewheel } from "swiper/modules";
+
+import "swiper/css";
+import "swiper/css/free-mode";
+
+const route = useRoute();
+const swiperInstance = ref(null);
+
+const modules = [FreeMode, Mousewheel];
+const TABS = ["铁 牛 类", "神 兽 类", "器物、建筑类"];
+const CLASS_MAP = {
+  1: {
+    class: "iron",
+    list: IRON_BULL_LIST,
+  },
+  2: {
+    class: "monster",
+    list: MONSTER_LIST,
+  },
+  3: {
+    class: "building",
+    list: BUILDING_LIST,
+  },
+};
+
+const onSwiper = (swiper) => {
+  swiperInstance.value = swiper;
+};
+
+watch(
+  () => route.params.id,
+  () => {
+    if (swiperInstance.value) {
+      swiperInstance.value.slideTo(0, 0);
+    }
+  }
+);
+</script>
+
+<style scoped lang="scss">
+@use "./list.scss";
+</style>

BIN
packages/pc/src/views/Cover/images/img_left-min.png


BIN
packages/pc/src/views/Cover/images/img_right-min.png


BIN
packages/pc/src/views/Cover/images/img_tip-min.png


+ 96 - 0
packages/pc/src/views/Cover/index.scss

@@ -0,0 +1,96 @@
+@use "../../assets/utils.scss";
+
+.cover {
+  position: absolute;
+  inset: 0;
+
+  &::before {
+    content: "";
+    position: absolute;
+    top: 50%;
+    left: 0;
+    width: 50%;
+    height: 100%;
+    transform: translateY(-50%);
+    background: url("./images/img_left-min.png") no-repeat center right / cover;
+  }
+  &::after {
+    content: "";
+    position: absolute;
+    top: 50%;
+    right: 0;
+    width: 50%;
+    height: 100%;
+    transform: translateY(-50%);
+    background: url("./images/img_right-min.png") no-repeat center left / cover;
+  }
+  &-cursor {
+    position: absolute;
+    right: 564px;
+    bottom: utils.vh-calc(290);
+    display: flex;
+    align-items: center;
+    gap: 13px;
+    cursor: pointer;
+    font-size: utils.vh-calc(20);
+    color: white;
+    z-index: 1;
+
+    &::before {
+      content: "";
+      width: utils.vh-calc(110);
+      height: utils.vh-calc(120);
+      background: url("./images/img_tip-min.png") no-repeat center / contain;
+      animation: knock 1.5s ease-in-out infinite;
+      transform-origin: center bottom;
+    }
+  }
+
+  // 过渡动画
+  .fade-enter-active,
+  .fade-leave-active {
+    transition: opacity 0.5s ease, transform 0.5s ease;
+  }
+
+  .fade-enter-from {
+    opacity: 0;
+  }
+
+  .fade-enter-to {
+    opacity: 1;
+  }
+
+  .fade-leave-from {
+    opacity: 1;
+  }
+
+  .fade-leave-to {
+    opacity: 0;
+  }
+
+  @keyframes knock {
+    0%,
+    100% {
+      transform: rotate(0deg);
+    }
+    25% {
+      transform: rotate(8deg);
+    }
+    50% {
+      transform: rotate(0deg);
+    }
+    75% {
+      transform: rotate(-8deg);
+    }
+  }
+  &__loading {
+    position: absolute;
+    left: 50%;
+    bottom: utils.vh-calc(119);
+    color: #fff9c9;
+    font-size: utils.vh-calc(24);
+    transform: translateX(-50%);
+    letter-spacing: 4px;
+    z-index: 1;
+  }
+}

+ 49 - 0
packages/pc/src/views/Cover/index.vue

@@ -0,0 +1,49 @@
+<template>
+  <div class="cover">
+    <transition name="fade" mode="out-in">
+      <div v-if="showCursor" key="cursor" class="cover-cursor">
+        <p>点击叩门</p>
+      </div>
+
+      <p v-else key="loading" class="cover__loading">
+        加载{{ loadingProgress }}%...
+      </p>
+    </transition>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, watch } from "vue";
+import { usePreloader } from "@shandong/base";
+import { useBaseStore, RESOURCE_LIST } from "@/stores/base";
+
+const baseStore = useBaseStore();
+const showCursor = ref(false);
+
+const {
+  percent: loadingProgress,
+  mediaUrlMap,
+  start,
+} = usePreloader({
+  list: RESOURCE_LIST,
+  success() {
+    baseStore.setMediaUrlMap(mediaUrlMap);
+  },
+});
+
+onMounted(() => {
+  start();
+});
+
+watch(loadingProgress, (newVal) => {
+  if (newVal === "100") {
+    setTimeout(() => {
+      showCursor.value = true;
+    }, 500);
+  }
+});
+</script>
+
+<style lang="scss" scoped>
+@use "./index.scss";
+</style>

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


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


BIN
packages/pc/src/views/Home/images/icon-1.png


BIN
packages/pc/src/views/Home/images/icon-2.png


BIN
packages/pc/src/views/Home/images/icon-3.png


+ 56 - 0
packages/pc/src/views/Home/index.scss

@@ -0,0 +1,56 @@
+@use "../../assets/utils.scss";
+
+.home {
+  position: absolute;
+  inset: 0;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  overflow: hidden;
+  background: url("./images/bg_01-min.jpg") no-repeat center / cover;
+
+  &__logo {
+    width: utils.vh-calc(230);
+    height: utils.vh-calc(230);
+  }
+  p {
+    margin: utils.vh-calc(85) 0 utils.vh-calc(105);
+    font-size: utils.vh-calc(20);
+    text-indent: 2em;
+    line-height: utils.vh-calc(40);
+    width: 1200px;
+    color: white;
+  }
+  &__btn {
+    width: utils.vh-calc(233);
+    height: utils.vh-calc(59);
+    cursor: pointer;
+    transition: opacity 0.2s ease-in-out;
+
+    &:hover {
+      opacity: 0.8;
+    }
+  }
+  &-footer {
+    position: absolute;
+    left: 58px;
+    bottom: utils.vh-calc(29);
+    display: flex;
+    align-items: center;
+    gap: 50px;
+
+    li {
+      position: relative;
+      top: 0;
+      width: utils.vh-calc(62);
+      height: utils.vh-calc(73);
+      cursor: pointer;
+      transition: top 0.2s ease-in-out;
+
+      &:hover {
+        top: -10px;
+      }
+    }
+  }
+}

+ 29 - 0
packages/pc/src/views/Home/index.vue

@@ -0,0 +1,29 @@
+<template>
+  <div class="home">
+    <img class="home__logo" src="@/assets/images/logo.png" />
+
+    <p>
+      六百多年前,兖(yǎn)州府曾生活过一位年轻的鲁王,他就是明朝开国皇帝朱元璋第十子朱檀。朱檀生于明洪武三年(1370),出生两月被封为鲁王,十五岁就藩兖州。他本应“尊德乐义”成为一方贤王,安享富贵,但终因迷恋仙道,服丹药毒发伤目,十九岁就结束了短暂一生。朱元璋为诫示子孙,赐其谥号“荒”,葬于今山东省邹城与曲阜市交界的九龙山南麓。
+    </p>
+
+    <img class="home__btn" src="./images/btn-min.png" />
+
+    <ul class="home-footer">
+      <li v-for="(item, index) in FOOTER_LIST" :key="index">
+        <img :src="item" />
+      </li>
+    </ul>
+  </div>
+</template>
+
+<script setup>
+import Icon1 from "./images/icon-1.png";
+import Icon2 from "./images/icon-2.png";
+import Icon3 from "./images/icon-3.png";
+
+const FOOTER_LIST = [Icon1, Icon2, Icon3];
+</script>
+
+<style lang="scss" scoped>
+@use "./index.scss";
+</style>

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


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


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


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


BIN
packages/pc/src/views/Tree/images/label4-min.png


+ 0 - 0
packages/pc/src/views/Tree/images/line.png


Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff