chenlei 1 месяц назад
Сommit
e6df554b23
55 измененных файлов с 3469 добавлено и 0 удалено
  1. 2 0
      .env
  2. 37 0
      .gitignore
  3. 2 0
      .npmrc
  4. 1 0
      README.md
  5. 33 0
      index.html
  6. 9 0
      jsconfig.json
  7. 35 0
      package.json
  8. 2459 0
      pnpm-lock.yaml
  9. 23 0
      postcss.config.js
  10. 37 0
      src/App.vue
  11. 1 0
      src/apis/index.js
  12. 7 0
      src/assets/elements.scss
  13. BIN
      src/assets/fonts/FZJinLing.TTF
  14. BIN
      src/assets/fonts/SOURCEHANSERIFCN-BOLD.OTF
  15. BIN
      src/assets/fonts/SourceHanSansCN-Bold.otf
  16. BIN
      src/assets/fonts/SourceHanSansCN-Regular.otf
  17. BIN
      src/assets/fonts/SourceHanSerifCN-Light.otf
  18. BIN
      src/assets/fonts/SourceHanSerifCN-Medium.otf
  19. BIN
      src/assets/fonts/SourceHanSerifCN-Regular.otf
  20. BIN
      src/assets/fonts/SourceHanSerifCN-SemiBold.otf
  21. BIN
      src/assets/images/1.png
  22. BIN
      src/assets/images/2.png
  23. BIN
      src/assets/images/bg.jpg
  24. BIN
      src/assets/images/head-bg.png
  25. BIN
      src/assets/images/icon-search.png
  26. BIN
      src/assets/images/icon-top.png
  27. BIN
      src/assets/images/logo.png
  28. 111 0
      src/assets/main.css
  29. 7 0
      src/assets/utils.scss
  30. 58 0
      src/components/TopBar/index.scss
  31. 24 0
      src/components/TopBar/index.vue
  32. 29 0
      src/configure.js
  33. 17 0
      src/main.js
  34. 19 0
      src/router/index.js
  35. 40 0
      src/stores/base.js
  36. 12 0
      src/stores/counter.js
  37. 113 0
      src/utils/constants.js
  38. 218 0
      src/utils/usePreloader.js
  39. BIN
      src/views/Collection/components/Item/images/lg-1.png
  40. BIN
      src/views/Collection/components/Item/images/lg-2.png
  41. BIN
      src/views/Collection/components/Item/images/lg-3.png
  42. BIN
      src/views/Collection/components/Item/images/sm-1.png
  43. BIN
      src/views/Collection/components/Item/images/sm-2.png
  44. BIN
      src/views/Collection/components/Item/images/sm-3.png
  45. 30 0
      src/views/Collection/components/Item/index.vue
  46. 19 0
      src/views/Collection/index.scss
  47. 20 0
      src/views/Collection/index.vue
  48. BIN
      src/views/Cover/images/1.png
  49. BIN
      src/views/Cover/images/2.png
  50. BIN
      src/views/Cover/images/bg.jpg
  51. BIN
      src/views/Cover/images/enter.png
  52. BIN
      src/views/Cover/images/title.png
  53. 43 0
      src/views/Cover/index.scss
  54. 18 0
      src/views/Cover/index.vue
  55. 45 0
      vite.config.js

+ 2 - 0
.env

@@ -0,0 +1,2 @@
+VITE_PUBLIC_URL=.
+VITE_BASE_URL=.

+ 37 - 0
.gitignore

@@ -0,0 +1,37 @@
+# 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
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+*.tsbuildinfo
+
+.eslintcache
+
+# Cypress
+/cypress/videos/
+/cypress/screenshots/
+
+# Vitest
+__screenshots__/
+node_modules

+ 2 - 0
.npmrc

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

+ 1 - 0
README.md

@@ -0,0 +1 @@
+WHS2504132-1 杭州市临安区博物馆-可移动文物数字化保护项目

+ 33 - 0
index.html

@@ -0,0 +1,33 @@
+<!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%;
+      }
+    </style>
+  </head>
+  <body>
+    <div id="app-container">
+      <div id="app"></div>
+    </div>
+    <script type="module" src="/src/main.js"></script>
+  </body>
+</html>

+ 9 - 0
jsconfig.json

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

+ 35 - 0
package.json

@@ -0,0 +1,35 @@
+{
+  "name": "lin_an-digital-collections",
+  "version": "0.0.0",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "dev:pano": "pnpm --dir pano-viewer dev",
+    "build:pano": "pnpm --dir pano-viewer build",
+    "build": "pnpm build:pano && vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "@dage/service": "^1.0.6",
+    "@element-plus/icons-vue": "^2.3.1",
+    "@vueuse/core": "^12.0.0",
+    "autoprefixer": "^10.4.20",
+    "element-plus": "^2.9.0",
+    "lodash": "^4.18.1",
+    "pinia": "^2.2.6",
+    "postcss-px-to-viewport": "^1.1.1",
+    "swiper": "^11.1.15",
+    "vue": "^3.5.13",
+    "vue-lazyload-next": "^0.0.2",
+    "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"
+  }
+}

Разница между файлами не показана из-за своего большого размера
+ 2459 - 0
pnpm-lock.yaml


+ 23 - 0
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/],
+    },
+  },
+};

+ 37 - 0
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: 1920;
+  --design-height: 1080;
+}
+
+img:not(.el-image-viewer__img) {
+  width: 100%;
+  height: 100%;
+}
+
+.el-image {
+  --el-fill-color-light: transparent;
+}
+
+#app {
+  font-size: 16px;
+}
+
+img[lazy] {
+  opacity: 0;
+  transition: opacity 0.2s ease-in-out;
+}
+img[lazy="loaded"] {
+  opacity: 1;
+}
+</style>

+ 1 - 0
src/apis/index.js

@@ -0,0 +1 @@
+import { requestByGet, requestByPost } from "@dage/service";

+ 7 - 0
src/assets/elements.scss

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

BIN
src/assets/fonts/FZJinLing.TTF


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


BIN
src/assets/fonts/SourceHanSansCN-Bold.otf


BIN
src/assets/fonts/SourceHanSansCN-Regular.otf


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


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


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


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


BIN
src/assets/images/1.png


BIN
src/assets/images/2.png


BIN
src/assets/images/bg.jpg


BIN
src/assets/images/head-bg.png


BIN
src/assets/images/icon-search.png


BIN
src/assets/images/icon-top.png


BIN
src/assets/images/logo.png


+ 111 - 0
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-Medium.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
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)));
+}

+ 58 - 0
src/components/TopBar/index.scss

@@ -0,0 +1,58 @@
+@use "../../assets/utils.scss";
+
+.top-bar {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 utils.vw-calc(32) utils.vh-calc(9) utils.vw-calc(47);
+  height: utils.vh-calc(108);
+  background: url("@/assets/images/head-bg.png") no-repeat center / cover;
+  z-index: 998;
+
+  .logo {
+    width: utils.vh-calc(502);
+    height: utils.vh-calc(63);
+  }
+  .search {
+    display: flex;
+    align-items: center;
+    gap: utils.vh-calc(10);
+    padding: 0 utils.vh-calc(5) 0 utils.vw-calc(13);
+    width: utils.vh-calc(500);
+    height: utils.vh-calc(50);
+    border: 1px solid #bba061;
+    background: white;
+    font-size: utils.vh-calc(18);
+    color: #2a2a2a;
+    line-height: 1;
+
+    input {
+      flex: 1;
+      color: #958474;
+      border: none;
+      outline: none;
+      font-size: utils.vh-calc(16);
+      font-family: inherit;
+      padding-left: utils.vh-calc(10);
+      border-left: 2px solid #9c917e;
+
+      &::placeholder {
+        color: #958474;
+        font-family: inherit;
+      }
+    }
+    .search-btn {
+      width: utils.vh-calc(97);
+      height: utils.vh-calc(39);
+      text-align: center;
+      line-height: utils.vh-calc(39);
+      background: #993418;
+      color: white;
+      cursor: pointer;
+    }
+  }
+}

+ 24 - 0
src/components/TopBar/index.vue

@@ -0,0 +1,24 @@
+<template>
+  <div class="top-bar">
+    <img
+      class="logo"
+      src="@/assets/images/logo.png"
+      alt="logo"
+      draggable="false"
+    />
+
+    <div class="right">
+      <div class="search">
+        <p>文物名称</p>
+        <input type="text" placeholder="输入您想查看的文物名称" />
+        <div class="search-btn">搜索</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup></script>
+
+<style lang="scss" scoped>
+@use "./index.scss";
+</style>

+ 29 - 0
src/configure.js

@@ -0,0 +1,29 @@
+import { compose, initial } from "@dage/service";
+import { ElMessage } from "element-plus";
+import "element-plus/theme-chalk/el-message.css";
+
+const showMessage = (msg, type = "error") => {
+  ElMessage({
+    type,
+    message: msg,
+    duration: 4000,
+  });
+};
+
+initial({
+  fetch: window.fetch.bind(window),
+  baseURL: import.meta.env.VITE_BASE_URL,
+  interceptor: compose(async (request, next) => {
+    const response = await next();
+    const { showError = true } = request.meta;
+
+    if (response.code !== 0 && showError) {
+      const message = response.__raw__.data.msg ?? "网络繁忙,请稍后重试!";
+      showMessage({
+        message,
+      });
+    }
+
+    return response;
+  }),
+});

+ 17 - 0
src/main.js

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

+ 19 - 0
src/router/index.js

@@ -0,0 +1,19 @@
+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: "/collection",
+      name: "collection",
+      component: () => import("../views/Collection/index.vue"),
+    },
+  ],
+});
+
+export default router;

+ 40 - 0
src/stores/base.js

@@ -0,0 +1,40 @@
+import { ref } from "vue";
+import { defineStore } from "pinia";
+import { parseUrlName } from "@/utils/usePreloader";
+import Img1 from "../views/Collection/images/map.jpg";
+import Img2 from "../components/NavBar/images/menu.png";
+import Img3 from "../views/Search/images/bg.jpg";
+import Img4 from "../views/Collection/images/Component-16_01.png";
+import Img5 from "../views/Collection/images/Component-16_02.png";
+import Img6 from "../views/Collection/images/Component-16_03.png";
+import Img7 from "../views/Collection/images/Component-19_01.png";
+import Img8 from "../views/Collection/images/Component-19_02.png";
+import Img9 from "../views/Collection/images/Component-19_03.png";
+import Img10 from "../views/Collection/components/Popup/images/pop.png";
+
+export const RESOURCE_LIST = [
+  Img1,
+  Img2,
+  Img3,
+  Img4,
+  Img5,
+  Img6,
+  Img7,
+  Img8,
+  Img9,
+  Img10,
+];
+
+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
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 }
+})

+ 113 - 0
src/utils/constants.js

@@ -0,0 +1,113 @@
+export const POINTS = [
+  {
+    num: "SG-yNl9f7UXWhg",
+    name: "苏俄学校旧址",
+    label: "(绥芬河市博物馆)",
+    desc: `
+      <p>中东铁路建筑群—绥芬河中东铁路建筑群-苏俄学校旧址位于黑龙江省牡丹江市绥芬河市绥芬河
+镇光华社区光华路94号,东侧为光华路,南侧为教育幼儿园,西侧为东正教堂旧址,北侧为迎新
+街,该建筑建于1913年,是一幢建筑平面呈“山”字形的二层楼房,楼正面向东,正立面为三段
+式,每段突出部分有一小三角形山花,正门开在一楼南部,一楼长方形窗,有窗楣,二楼为尖券
+拱形窗,二楼窗下部方形,上部为尖券拱形,整体建筑为石基厚墙,砖木结构,表面黄色与白色
+相间,上为黑色铁瓦屋顶。建筑长60.65米,宽17.06米,占地面积为991.16平方米,建筑面积
+1932平方米。</p>
+      <p>此建筑初为俄国侨民学校;日伪时期为侵华日军第764部队驻地;1945年8月苏联红军驻此,冬
+季大火将楼烧毁后又修复;1950年后为中国人民解放军驻绥部队营房;1982年为市政府办公大楼,
+后为教师进修学校办公大楼和机关幼儿园办公楼。2018年苏俄学校旧址经过抢险加固工程修缮
+后,在绥芬河市委市政府要求下绥芬河市博物馆搬迁至此,并重新布展开放参观,并在2021年进
+行了安全防护工程和消防工程建设。该建筑是中东铁路时期建筑,该建筑主体结构完整,承重体
+系无可见变形或损伤,整体稳定性良好,是百年中东铁路历史和绥芬河开发史的历史见证。</p>
+    `,
+  },
+  {
+    num: "SG-6C2hfsv0XgF",
+    name: "绥芬河市铁路大白楼",
+    label: "(绥芬河红色过激通道纪念馆、国家安全教育纪念展馆)",
+    desc: `
+      <p>中东铁路建筑群—绥芬河中东铁路建筑群-绥芬河市铁路大白楼位于黑龙江省牡丹江市绥芬河市
+绥芬河镇三合林社区梨树街2号,该建筑东临花园路铁路住宅楼,北临梨树街,南临站前路和文
+化街,西临滨绥铁路线。</p>
+      <p>中东铁路建筑群—绥芬河中东铁路建筑群-绥芬河市铁路大白楼约建于1913年,建筑长约39.45
+米,宽约28.97米,占地面积为1165.16平方米,建筑面积2050平方米。为长方环形二层楼房,
+中间有天井,东南部有一天井通道。建筑正门设在西南角处,门台阶两侧粗重的绞拧状圆柱独具
+特色,一楼南侧有拱形门洞,西南角有伸出墙体的亭式平台。80厘米厚的墙体,上为黑色铁瓦房
+顶,呈菱形拼接,这是一座具有俄式折衷主义风格的建筑,屋面坡陡,强调两翼,具有童话的情
+趣。特别是拱券式的门廊、丰富的门窗形式及檐口的装饰手法,都体现出浪漫主义建筑风格。整
+体建筑庄重典雅、落落大方犹如一幅清丽透彻的水彩画。大白楼原为总理绥芬河铁路交涉分局总
+理委员官邸,俗称“大白楼”。1933年为满铁日藉员工宿舍;1945年后为苏联铁路专家宿舍及办
+公地;1955年为铁路职工的独身宿舍;1968年后为绥芬河铁路地区党委办公室及职工宿舍,绥芬
+河铁路地区办事处;2005年改为绥芬河市政府宾馆。该建筑于2019年布展并面向公众开放,一
+楼为绥芬河红色国际通道纪念馆,二楼为国家安全教育纪念展馆。该建筑主体结构完整,承重体
+系无可见变形或损伤,整体稳定性良好。</p>
+    `,
+  },
+  {
+    num: "SG-AvlgkYjZjVb",
+    name: "火车站候车室",
+    label: "(中东铁路记忆馆)",
+    desc: `
+      <p>中东铁路建筑群—绥芬河中东铁路建筑群-火车站候车室位于黑龙江省牡丹江市绥芬河市绥芬河
+镇光华社区站前路38号,东侧为站前路,南侧为铁路用房,西侧为铁路站场,北侧为铁路车站办
+公楼,该建筑始建于1902年。绥芬河站是中东铁路东部线的终点车站,过境与俄罗斯铁路相接,
+时为二等站。1929年中东路事件时曾受战毁,后修复为现状。该建筑为长方形二层建筑,建筑长
+约62.74米,宽约21.73米,占地面积为1252.51平方米,建筑面积1342平方米,一层主体为大
+跨度钢梁屋架站舍,北侧局部有二层建筑。建筑墙面强调垂直划分,壁柱装饰,女儿墙高低错落,
+为城垛式造型,山墙及窗采用曲线构图,具有新艺术运动建筑的特点。窗楣采用俄式传统图形装
+饰,装饰色彩采用白色墙面,灰色点缀,体现出庄重典雅的气氛。该建筑为现存中东铁路遗留建
+筑中唯一珍贵的室内大跨度钢梁屋架站舍,是中东铁路的重要历史见证。2018年对其布展辟建为
+中东铁路记忆馆。2025年实施了安防工程。该建筑因在百余年的使用中地坪不断抬高,原散水设
+施已消失,建筑水害严重,保存状况较差,已于2024年申请文物保护资金进行修缮,国家文物局
+〔2024〕1923号文件《国家文物局关于黑龙江省2025年度全国重点文物保护单位文物保护项目
+(文物古迹类)计划的批复》中对该建筑的修缮工程予以立项,截至2026年1月已进行两轮修缮
+设计方案的审核工作。</p>
+    `,
+  },
+  {
+    num: "SG-Uhfwlhc8ff2",
+    name: "东正教堂",
+    label: "(绥芬河市基督教会)",
+    desc: `
+      <p>中东铁路建筑群—绥芬河中东铁路建筑群-东正教堂旧址位于黑龙江省牡丹江市绥芬河市绥芬河
+镇光华社区大直路135号,东侧为绥芬河市博物馆,南侧为绥芬河市总工会,西侧为大直路,北
+侧为迎新街,该建筑建于1913年。这是一幢典型俄罗斯宗教建筑(俗称“喇嘛台”),又称“协达
+亚•尼古拉教堂”。平面呈十字形。建筑长约35.54米,宽约17.19米,建筑面积为427.73平方米。
+该建筑正门西开,南北两侧各一门,厚门高窗,上部均为曲拱形。教堂正门为高大钟楼,顶部有
+洋葱头塔顶。钟楼为砖混结构,主体建筑为砖木结构。教堂活动至1945年终止,同年由于战事塔
+楼被炸毁一部分。1950年当地驻军将其作为基地转运站,1960年前后将塔楼拆掉改成鱼脊状屋顶。
+1982年绥芬河市人民政府接管后作为会议室。现被信教群众捐资购得并在此开展活动,为绥芬河
+市基督教会所有。该建筑是西方宗教传入绥芬河的历史见证。</p>
+    `,
+  },
+  {
+    num: "SG-L0hjrhuL68K",
+    name: "苏俄领事旧馆",
+    label: "(绥芬河中俄油画艺术展馆)",
+    desc: `<p>中东铁路建筑群—绥芬河中东铁路建筑群-苏俄领事馆旧址位于黑龙江省牡丹江市绥芬河市绥芬
+河镇光华社区光华路111号,建于1910年,该建筑东邻日本领事馆旧址,西邻光华路,南邻公安
+局家属楼,北至迎新街,是一幢建筑平面呈凹字形的俄式二层楼房,砖混结构,建筑长34.8米,
+宽26.53米,占地面积696.33平方米,建筑面积1358平方米。大屋檐,四坡面屋顶,墙面用凸
+出的隅石及条形图案装饰,一楼二楼结合处有腰线,屋檐处用线脚装饰,窗户顶部为拱形并用砖
+砌窗楣装饰,该建筑初为沙俄领事馆,后为苏联领事馆,日伪时期闲置。解放初为远东公司办事
+处,继为军政委员会、中共绥芬河市委、中共绥芬河市纪委、绥芬河市邮政局办公楼,后由市政
+府出资置换并予以综合保护开发,产权收归国有,2023年该建筑进行了修缮工程,该建筑主体结
+构完整,承重体系无可见变形或损伤,整体稳定性良好,2025年在此处开办绥芬河中俄油画艺术
+展馆。</p>`,
+  },
+  {
+    num: "SG-rBuP7Q8VGem",
+    name: "日本领事馆旧址",
+    label: "(个人)",
+    desc: `
+      <p>中东铁路建筑群—绥芬河中东铁路建筑群-日本领事馆旧址位于黑龙江省牡丹江市绥芬河市绥芬
+河镇光华社区迎新街88号,东侧为兴隆路,南侧为金城楼,西侧为苏俄领事馆旧址,北侧为迎新
+街,该建筑建于1914年。这是一幢具有俄式风格的四层楼房,建筑长22.2米,宽10.9米,占地
+面积为307.26平方米,建筑面积1240平方米。因其三、四层间有凸出的外檐,檐下有人面浮雕,
+故称其为“人头楼”。该建筑四层之上有一方亭,顶置球形塔尖。楼体土红色,突起的棱角部分涂
+白色,间有黑色,顶为黑色铁瓦盖,正门临街北开。此楼原为俄罗斯人契斯恰科夫的茶庄,当地
+人称其为“赤查果夫茶庄”。1922年该楼被从俄境内撤回的日本陆军购买,1935年为日本驻绥芬
+河领事馆。1929年中东路事件时被苏军将木质顶楼击毁,后用砖木恢复。解放后曾做为东宁县人
+民政府办公楼。1952年后为驻军营房,1992年后改为商用。现产权为个人所有,内部为空置状态。
+该建筑主体结构完整,承重体系无可见变形或损伤,整体稳定性良好。</p>
+    `,
+  },
+];

+ 218 - 0
src/utils/usePreloader.js

@@ -0,0 +1,218 @@
+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(() => {
+        setThrottlePercent("100");
+        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,
+  };
+}

BIN
src/views/Collection/components/Item/images/lg-1.png


BIN
src/views/Collection/components/Item/images/lg-2.png


BIN
src/views/Collection/components/Item/images/lg-3.png


BIN
src/views/Collection/components/Item/images/sm-1.png


BIN
src/views/Collection/components/Item/images/sm-2.png


BIN
src/views/Collection/components/Item/images/sm-3.png


+ 30 - 0
src/views/Collection/components/Item/index.vue

@@ -0,0 +1,30 @@
+<template>
+  <div class="collection-item">
+    <img src="@/assets/images/1.png" draggable="false" />
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.collection-item {
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  padding: 30px;
+  width: 250px;
+  height: 373px;
+  background: url("./images/lg-1.png") repeat center / cover;
+
+  &.sm {
+    width: 250px;
+    height: 266px;
+    background-image: url("./images/sm-1.png");
+  }
+  img {
+    width: auto;
+    max-width: 100%;
+    object-fit: contain;
+  }
+}
+</style>

+ 19 - 0
src/views/Collection/index.scss

@@ -0,0 +1,19 @@
+@use "../../assets/utils.scss";
+
+.collection-page {
+  position: absolute;
+  inset: 0;
+  padding-top: utils.vh-calc(108 + 13);
+  background: url("@/assets/images/bg.jpg") no-repeat center top / cover;
+
+  .scroll-container {
+    width: 100%;
+    height: 100%;
+  }
+  .scroll-content {
+    display: grid;
+    grid-template-columns: repeat(7, 1fr);
+    gap: utils.vw-calc(11);
+    padding: 0 utils.vw-calc(48);
+  }
+}

+ 20 - 0
src/views/Collection/index.vue

@@ -0,0 +1,20 @@
+<template>
+  <div class="collection-page">
+    <TopBar />
+
+    <el-scrollbar class="scroll-container" scroll-y>
+      <div class="scroll-content">
+        <Item />
+      </div>
+    </el-scrollbar>
+  </div>
+</template>
+
+<script setup>
+import TopBar from "@/components/TopBar/index.vue";
+import Item from "./components/Item/index.vue";
+</script>
+
+<style lang="scss" scoped>
+@use "./index.scss";
+</style>

BIN
src/views/Cover/images/1.png


BIN
src/views/Cover/images/2.png


BIN
src/views/Cover/images/bg.jpg


BIN
src/views/Cover/images/enter.png


BIN
src/views/Cover/images/title.png


+ 43 - 0
src/views/Cover/index.scss

@@ -0,0 +1,43 @@
+@use "../../assets/utils.scss";
+
+.cover-page {
+  position: absolute;
+  inset: 0;
+  background: url("./images/bg.jpg") no-repeat center / cover;
+
+  .title {
+    position: absolute;
+    top: utils.vh-calc(236);
+    left: utils.vw-calc(432);
+    width: utils.vh-calc(730);
+    height: utils.vh-calc(563);
+  }
+
+  .enter {
+    position: absolute;
+    bottom: utils.vh-calc(121);
+    left: utils.vw-calc(870);
+    width: utils.vh-calc(319);
+    height: utils.vh-calc(65);
+    cursor: pointer;
+  }
+
+  &::before {
+    content: "";
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: utils.vh-calc(454);
+    height: utils.vh-calc(471);
+    background: url("./images/1.png") no-repeat center / cover;
+  }
+  &::after {
+    content: "";
+    position: absolute;
+    right: 0;
+    bottom: 0;
+    width: utils.vh-calc(632);
+    height: utils.vh-calc(692);
+    background: url("./images/2.png") no-repeat center / cover;
+  }
+}

+ 18 - 0
src/views/Cover/index.vue

@@ -0,0 +1,18 @@
+<template>
+  <div class="cover-page">
+    <img class="title" src="./images/title.png" alt="title" draggable="false" />
+    <img
+      class="enter"
+      src="./images/enter.png"
+      alt="button"
+      draggable="false"
+      @click="$router.push({ name: 'collection' })"
+    />
+  </div>
+</template>
+
+<script setup></script>
+
+<style lang="scss" scoped>
+@use "./index.scss";
+</style>

+ 45 - 0
vite.config.js

@@ -0,0 +1,45 @@
+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 { ElementPlusResolver } from "unplugin-vue-components/resolvers";
+
+// https://vite.dev/config/
+export default defineConfig({
+  base: "./",
+  server: {
+    host: "0.0.0.0",
+    port: 5175,
+  },
+  plugins: [
+    vue(),
+    AutoImport({
+      resolvers: [
+        ElementPlusResolver({
+          importStyle: "sass",
+        }),
+      ],
+    }),
+    Components({
+      resolvers: [ElementPlusResolver({ importStyle: "sass" })],
+    }),
+  ],
+  resolve: {
+    alias: {
+      "@": fileURLToPath(new URL("./src", import.meta.url)),
+    },
+  },
+  css: {
+    preprocessorOptions: {
+      scss: {
+        api: "modern-compiler",
+        additionalData: `
+          @use "@/assets/elements.scss" as *;
+        `,
+      },
+    },
+  },
+});