chenlei 1 рік тому
батько
коміт
54c70b5ed7
100 змінених файлів з 3185 додано та 112 видалено
  1. 1 1
      README.md
  2. 25 0
      components.d.ts
  3. 5 2
      package.json
  4. 49 0
      pnpm-lock.yaml
  5. 22 0
      postcss.config.js
  6. 19 1
      src/App.vue
  7. 42 102
      src/assets/css/base.css
  8. BIN
      src/assets/fonts/SourceHanSans-Medium.otf
  9. BIN
      src/assets/images/bg_5.png
  10. BIN
      src/assets/images/bg_6.png
  11. BIN
      src/assets/images/bg_7.png
  12. BIN
      src/assets/images/chosen.png
  13. BIN
      src/assets/images/logo.png
  14. BIN
      src/assets/images/logo2.png
  15. BIN
      src/assets/images/see.png
  16. 219 0
      src/components/Calendar.vue
  17. 254 0
      src/components/DateTable/index.vue
  18. 347 0
      src/components/DateTable/utils.js
  19. 37 0
      src/components/ImgCard.vue
  20. 61 0
      src/components/ImgSwiper.vue
  21. 80 0
      src/components/Layout/components/Search/index.vue
  22. 72 0
      src/components/Layout/components/Sidebar/index.scss
  23. 89 0
      src/components/Layout/components/Sidebar/index.vue
  24. 223 0
      src/components/Layout/data.ts
  25. BIN
      src/components/Layout/images/erwei1.png
  26. BIN
      src/components/Layout/images/erwei2.png
  27. BIN
      src/components/Layout/images/mean.png
  28. BIN
      src/components/Layout/images/search.png
  29. BIN
      src/components/Layout/images/search2.png
  30. BIN
      src/components/Layout/images/zhong.png
  31. BIN
      src/components/Layout/images/zhong2.png
  32. 112 0
      src/components/Layout/index.scss
  33. 106 1
      src/components/Layout/index.vue
  34. 33 0
      src/components/PageBanner.vue
  35. BIN
      src/components/PageLabel/icon.png
  36. 24 0
      src/components/PageLabel/index.vue
  37. 78 0
      src/components/PageNav.vue
  38. 134 0
      src/components/SwipeCard.vue
  39. 6 0
      src/main.ts
  40. 7 0
      src/router-meta.d.ts
  41. 123 0
      src/router/index.ts
  42. 2 2
      src/shims-vue.d.ts
  43. 3 0
      src/utils/index.ts
  44. BIN
      src/views/Collections/Detail/images/bgCD-min.png
  45. 14 0
      src/views/Collections/Detail/index.scss
  46. 18 0
      src/views/Collections/Detail/index.vue
  47. BIN
      src/views/Collections/List/1.png
  48. BIN
      src/views/Collections/List/5.png
  49. BIN
      src/views/Collections/List/6.png
  50. 13 0
      src/views/Collections/List/index.scss
  51. 61 0
      src/views/Collections/List/index.vue
  52. BIN
      src/views/Collections/images/bannerC.png
  53. 25 0
      src/views/Collections/index.scss
  54. 23 0
      src/views/Collections/index.vue
  55. 3 0
      src/views/Employment/index.vue
  56. 3 0
      src/views/Events/index.vue
  57. 38 0
      src/views/Exhibitions/Current/index.vue
  58. BIN
      src/views/Exhibitions/Detail/images/bgExD-min.png
  59. 87 0
      src/views/Exhibitions/Detail/index.scss
  60. 109 0
      src/views/Exhibitions/Detail/index.vue
  61. 29 0
      src/views/Exhibitions/Galleries/index.scss
  62. 40 0
      src/views/Exhibitions/Galleries/index.vue
  63. BIN
      src/views/Exhibitions/Objects/images/banner-min.jpg
  64. 30 0
      src/views/Exhibitions/Objects/index.scss
  65. 40 0
      src/views/Exhibitions/Objects/index.vue
  66. 17 0
      src/views/Exhibitions/Overseas/index.vue
  67. 17 0
      src/views/Exhibitions/Past/index.vue
  68. 14 0
      src/views/Exhibitions/Permanent/index.scss
  69. 17 0
      src/views/Exhibitions/Permanent/index.vue
  70. BIN
      src/views/Exhibitions/images/bannerE.png
  71. 39 0
      src/views/Exhibitions/index.vue
  72. 74 0
      src/views/Home/components/Connections.vue
  73. 117 0
      src/views/Home/components/VisitInfo.vue
  74. BIN
      src/views/Home/images/bg-min.png
  75. BIN
      src/views/Home/images/frame.png
  76. BIN
      src/views/Home/images/link1.jpg
  77. BIN
      src/views/Home/images/link2.jpg
  78. BIN
      src/views/Home/images/link3.jpg
  79. BIN
      src/views/Home/images/link4.jpg
  80. BIN
      src/views/Home/images/link5.jpg
  81. BIN
      src/views/Home/images/link6.jpg
  82. BIN
      src/views/Home/images/v1.png
  83. BIN
      src/views/Home/images/v2.png
  84. BIN
      src/views/Home/images/v3.png
  85. BIN
      src/views/Home/images/v4.png
  86. BIN
      src/views/Home/images/v5.png
  87. BIN
      src/views/Home/images/v6.png
  88. 14 0
      src/views/Home/index.scss
  89. 27 3
      src/views/Home/index.vue
  90. BIN
      src/views/Learn/images/bannerL.png
  91. 35 0
      src/views/Learn/index.vue
  92. 3 0
      src/views/Use/index.vue
  93. BIN
      src/views/Visit/components/Accessibility/images/access1.jpg
  94. BIN
      src/views/Visit/components/Accessibility/images/access2.jpg
  95. BIN
      src/views/Visit/components/Accessibility/images/access3.jpg
  96. 47 0
      src/views/Visit/components/Accessibility/index.vue
  97. 58 0
      src/views/Visit/components/Admission/index.vue
  98. BIN
      src/views/Visit/components/Admission/map.png
  99. BIN
      src/views/Visit/components/Cafe/images/cafe1.jpg
  100. 0 0
      src/views/Visit/components/Cafe/images/cafe2.jpg

+ 1 - 1
README.md

@@ -1,4 +1,4 @@
-# en-shoubo-pc
+# en-shoubo-mobile
 
 This template should help get you started developing with Vue 3 in Vite.
 

+ 25 - 0
components.d.ts

@@ -7,9 +7,34 @@ export {}
 /* prettier-ignore */
 declare module 'vue' {
   export interface GlobalComponents {
+    Calendar: typeof import('./src/components/Calendar.vue')['default']
+    Card: typeof import('./src/components/Card.vue')['default']
+    DateTable: typeof import('./src/components/DateTable/index.vue')['default']
+    ImgCard: typeof import('./src/components/ImgCard.vue')['default']
+    ImgSwiper: typeof import('./src/components/ImgSwiper.vue')['default']
     Layout: typeof import('./src/components/Layout/index.vue')['default']
+    PageBanner: typeof import('./src/components/PageBanner.vue')['default']
+    PageLabel: typeof import('./src/components/PageLabel/index.vue')['default']
+    PageNav: typeof import('./src/components/PageNav.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
+    Search: typeof import('./src/components/Layout/components/Search/index.vue')['default']
+    Sidebar: typeof import('./src/components/Layout/components/Sidebar/index.vue')['default']
     SvgIcon: typeof import('./src/components/SvgIcon/index.vue')['default']
+    SwipeCard: typeof import('./src/components/SwipeCard.vue')['default']
+    VanCalendar: typeof import('vant/es')['Calendar']
+    VanCollapse: typeof import('vant/es')['Collapse']
+    VanCollapseItem: typeof import('vant/es')['CollapseItem']
+    VanIcon: typeof import('vant/es')['Icon']
+    VanImage: typeof import('vant/es')['Image']
+    VanImagePreview: typeof import('vant/es')['ImagePreview']
+    VanNoticeBar: typeof import('vant/es')['NoticeBar']
+    VanPagination: typeof import('vant/es')['Pagination']
+    VanPopup: typeof import('vant/es')['Popup']
+    VanSticky: typeof import('vant/es')['Sticky']
+    VanSwipe: typeof import('vant/es')['Swipe']
+    VanSwipeItem: typeof import('vant/es')['SwipeItem']
+    VanTab: typeof import('vant/es')['Tab']
+    VanTabs: typeof import('vant/es')['Tabs']
   }
 }

+ 5 - 2
package.json

@@ -2,7 +2,7 @@
   "name": "en-shoubo-mobile",
   "version": "0.0.0",
   "private": true,
-  "type": "module",
+  "type": "commonjs",
   "scripts": {
     "dev": "vite",
     "build": "run-p type-check \"build-only {@}\" --",
@@ -19,7 +19,8 @@
     "vant": "^4.9.0",
     "vue": "^3.4.21",
     "vue-router": "^4.3.0",
-    "vue-waterfall-plugin-next": "^2.4.3"
+    "vue-waterfall-plugin-next": "^2.4.3",
+    "vue3-touch-events": "^4.1.8"
   },
   "devDependencies": {
     "@tsconfig/node20": "^20.1.4",
@@ -27,8 +28,10 @@
     "@vitejs/plugin-vue": "^5.0.4",
     "@vitejs/plugin-vue-jsx": "^3.1.0",
     "@vue/tsconfig": "^0.5.1",
+    "autoprefixer": "^10.4.19",
     "install": "^0.13.0",
     "npm-run-all2": "^6.1.2",
+    "postcss-px-to-viewport": "^1.1.1",
     "sass": "^1.77.4",
     "typescript": "~5.4.0",
     "unplugin-vue-components": "^0.27.0",

+ 49 - 0
pnpm-lock.yaml

@@ -32,6 +32,9 @@ dependencies:
   vue-waterfall-plugin-next:
     specifier: ^2.4.3
     version: 2.4.3(@types/lodash-es@4.17.12)(vue@3.4.27)
+  vue3-touch-events:
+    specifier: ^4.1.8
+    version: 4.1.8
 
 devDependencies:
   '@tsconfig/node20':
@@ -49,12 +52,18 @@ devDependencies:
   '@vue/tsconfig':
     specifier: ^0.5.1
     version: 0.5.1
+  autoprefixer:
+    specifier: ^10.4.19
+    version: 10.4.19(postcss@8.4.38)
   install:
     specifier: ^0.13.0
     version: 0.13.0
   npm-run-all2:
     specifier: ^6.1.2
     version: 6.2.0
+  postcss-px-to-viewport:
+    specifier: ^1.1.1
+    version: 1.1.1
   sass:
     specifier: ^1.77.4
     version: 1.77.4
@@ -1208,6 +1217,22 @@ packages:
     hasBin: true
     dev: true
 
+  /autoprefixer@10.4.19(postcss@8.4.38):
+    resolution: {integrity: sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==}
+    engines: {node: ^10 || ^12 || >=14}
+    hasBin: true
+    peerDependencies:
+      postcss: ^8.1.0
+    dependencies:
+      browserslist: 4.23.0
+      caniuse-lite: 1.0.30001625
+      fraction.js: 4.3.7
+      normalize-range: 0.1.2
+      picocolors: 1.0.1
+      postcss: 8.4.38
+      postcss-value-parser: 4.2.0
+    dev: true
+
   /available-typed-arrays@1.0.7:
     resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
     engines: {node: '>= 0.4'}
@@ -1911,6 +1936,10 @@ packages:
     engines: {node: '>=0.10.0'}
     dev: true
 
+  /fraction.js@4.3.7:
+    resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
+    dev: true
+
   /fragment-cache@0.2.1:
     resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==}
     engines: {node: '>=0.10.0'}
@@ -2618,6 +2647,11 @@ packages:
     engines: {node: '>=0.10.0'}
     dev: true
 
+  /normalize-range@0.1.2:
+    resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
+    engines: {node: '>=0.10.0'}
+    dev: true
+
   /normalize-wheel-es@1.2.0:
     resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==}
     dev: false
@@ -2778,6 +2812,17 @@ packages:
       postcss: 5.2.18
     dev: true
 
+  /postcss-px-to-viewport@1.1.1:
+    resolution: {integrity: sha512-2x9oGnBms+e0cYtBJOZdlwrFg/mLR4P1g2IFu7jYKvnqnH/HLhoKyareW2Q/x4sg0BgklHlP1qeWo2oCyPm8FQ==}
+    dependencies:
+      object-assign: 4.1.1
+      postcss: 8.4.38
+    dev: true
+
+  /postcss-value-parser@4.2.0:
+    resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
+    dev: true
+
   /postcss@5.2.18:
     resolution: {integrity: sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==}
     engines: {node: '>=0.12'}
@@ -3598,6 +3643,10 @@ packages:
       - vue
     dev: false
 
+  /vue3-touch-events@4.1.8:
+    resolution: {integrity: sha512-8Zs0mj5k/7R579JHsc5w1V2IqAkNlz2gJs18bRV4T5WyCMfd5sBf2ESJ2xR8z+n7ypgK8fQO5MmDOPal0Evf3Q==}
+    dev: false
+
   /vue@3.4.27(typescript@5.4.5):
     resolution: {integrity: sha512-8s/56uK6r01r1icG/aEOHqyMVxd1bkYcSe9j8HcKtr/xTOFWvnzIVTehNW+5Yt89f+DLBe4A569pnZLS5HzAMA==}
     peerDependencies:

+ 22 - 0
postcss.config.js

@@ -0,0 +1,22 @@
+module.exports = {
+  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/], // 忽略某些文件夹下的文件或特定文件,例如 'node_modules' 下的文件
+      include: undefined, // 如果设置了include,那将只有匹配到的文件才会被转换
+      landscape: false, // 是否添加根据 landscapeWidth 生成的媒体查询条件 @media (orientation: landscape)
+      landscapeUnit: "vw", // 横屏时使用的单位
+      landscapeWidth: 750, // 横屏时使用的视口宽度
+    },
+  },
+};

+ 19 - 1
src/App.vue

@@ -6,4 +6,22 @@ import { RouterView } from "vue-router";
   <RouterView />
 </template>
 
-<style lang="scss"></style>
+<style lang="scss">
+@font-face {
+  font-family: "SourceHanSans-Medium";
+  src: url("@/assets/fonts/SourceHanSans-Medium.otf");
+  font-weight: normal;
+  font-style: normal;
+}
+
+.more {
+  margin: 40px auto 0;
+  width: 234px;
+  height: 74px;
+  line-height: 80px;
+  text-align: center;
+  color: #c1aa7b;
+  font-size: 32px;
+  background: url("@/assets/images/see.png") no-repeat center / contain;
+}
+</style>

+ 42 - 102
src/assets/css/base.css

@@ -79,13 +79,13 @@ time,
 mark,
 audio,
 video {
-    margin: 0;
-    padding: 0;
-    border: 0;
-    font-size: 100%;
-    font: inherit;
-    vertical-align: baseline;
-    box-sizing: border-box;
+  margin: 0;
+  padding: 0;
+  border: 0;
+  font-size: 100%;
+  font: inherit;
+  vertical-align: baseline;
+  box-sizing: border-box;
 }
 
 /* HTML5 display-role reset for older browsers */
@@ -100,130 +100,70 @@ hgroup,
 menu,
 nav,
 section {
-    display: block;
-}
-
-body {
-    line-height: 1;
-    background-color: #f1f1f1;
-    font-family: Arial;
+  display: block;
 }
 
 ol,
 ul {
-    list-style: none;
+  list-style: none;
 }
 
 blockquote,
 q {
-    quotes: none;
+  quotes: none;
+}
+
+img {
+  object-fit: cover;
+  object-position: center;
 }
 
 blockquote:before,
 blockquote:after,
 q:before,
 q:after {
-    content: '';
-    content: none;
+  content: "";
+  content: none;
 }
 
 table {
-    border-collapse: collapse;
-    border-spacing: 0;
+  border-collapse: collapse;
+  border-spacing: 0;
 }
 
 a {
-    color: #000;
-    text-decoration: none;
+  color: #000;
+  text-decoration: none;
 }
 
 a:hover {
-    color: #000;
-    text-decoration: none;
-}
-
-/* element 输入框样式 */
-.el-input {
-    height: 30px;
-}
-
-.el-input__inner {
-    height: 30px;
-    border-radius: 15px;
-    line-height: 30px;
-}
-
-.el-input.is-active .el-input__inner,
-.el-input__inner:focus {
-    border-color: #ca000a;
-}
-
-/* 轮播图样式 */
-.el-carousel--horizontal {
-    height: 100%;
-}
-
-.el-carousel__container {
-    height: 100%;
-}
-
-.el-carousel__arrow {
-    display: none !important;
-}
-
-.el-carousel__button {
-    height: 4px;
-    background-color: #918784;
-    opacity: 1;
+  color: #000;
+  text-decoration: none;
 }
 
-.el-carousel__indicator.is-active button {
-    background-color: #c7000b;
-}
-
-.el-carousel__indicators--horizontal {
-    bottom: 80px;
-}
-
-.el-select .el-input.is-focus .el-input__inner {
-    border-color: #c7000b;
-}
-
-.el-select .el-input__inner:focus {
-    border-color: #c7000b;
+h1,
+h2,
+h3,
+h4,
+h5 {
+  font-family: Bold;
+  font-weight: bold;
 }
 
-.el-select-dropdown__item.selected {
-    color: #c7000b;
+.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;
 }
 
-.el-select-dropdown__item.hover,
-.el-select-dropdown__item:hover {
-    background-color: #c7000b;
-    color: #fff;
+.line-2 {
+  -webkit-line-clamp: 2;
 }
 
-
-/* 置灰 */
-/* * {
-    -webkit-filter: grayscale(100%);
-    -moz-filter: grayscale(100%);
-    -ms-filter: grayscale(100%);
-    -o-filter: grayscale(100%);
-    filter: grayscale(100%);
-    filter: gray;
-} */
-
-.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-4 {
+  -webkit-line-clamp: 4;
 }
-
-.line-2 {
-    -webkit-line-clamp: 2;
-}

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


BIN
src/assets/images/bg_5.png


BIN
src/assets/images/bg_6.png


BIN
src/assets/images/bg_7.png


BIN
src/assets/images/chosen.png


BIN
src/assets/images/logo.png


BIN
src/assets/images/logo2.png


BIN
src/assets/images/see.png


+ 219 - 0
src/components/Calendar.vue

@@ -0,0 +1,219 @@
+<template>
+  <div class="panel-rg">
+    <div class="panel-rg__header">
+      <van-icon name="arrow-left" @click="selectDate('prev-month')" />
+      <p>
+        <span>{{ fullMonths[month - 1] }}</span>
+        {{ date.getFullYear() }}
+      </p>
+      <van-icon name="arrow" @click="selectDate('next-month')" />
+    </div>
+    <DateTable
+      class="panel-rg__date"
+      :date="date"
+      :selected-day="realSelectedDay"
+      :first-day-of-week="1"
+      @pick="pickDay"
+    >
+      <template name="dateCell" slot-scope="{ date, data }">
+        {{ data.day.split("-")[1] }}1
+      </template>
+    </DateTable>
+
+    <div class="panel-rg__footer">
+      <p>Exhibitions</p>
+      <p>Events</p>
+      <p>Learn & Engage</p>
+    </div>
+  </div>
+</template>
+
+<script>
+import { formatDate } from "@dage/utils";
+import DateTable from "./DateTable/index.vue";
+import { fullMonths } from "./DateTable/utils";
+
+export default {
+  components: {
+    DateTable,
+  },
+  props: {
+    modelValue: [Date, String, Number],
+  },
+  provide() {
+    return {
+      elCalendar: this,
+    };
+  },
+  data() {
+    return {
+      fullMonths,
+      selectedDay: "",
+      now: new Date(),
+    };
+  },
+  computed: {
+    date() {
+      if (!this.modelValue) {
+        if (this.realSelectedDay) {
+          const d = this.selectedDay.split("-");
+          return new Date(d[0], d[1] - 1, d[2]);
+        }
+        return this.now;
+      } else {
+        return this.toDate(this.modelValue);
+      }
+    },
+    realSelectedDay: {
+      get() {
+        if (!this.modelValue) return this.selectedDay;
+        return this.formatedDate;
+      },
+      set(val) {
+        this.selectedDay = val;
+        const date = new Date(val);
+        this.$emit("update:modelValue", date);
+      },
+    },
+    prevMonthDatePrefix() {
+      const temp = new Date(this.date.getTime());
+      temp.setDate(0);
+      return formatDate(temp, "YYYY-MM");
+    },
+    nextMonthDatePrefix() {
+      const temp = new Date(
+        this.date.getFullYear(),
+        this.date.getMonth() + 1,
+        1
+      );
+      return formatDate(temp, "YYYY-MM");
+    },
+    formatedDate() {
+      return formatDate(this.date, "YYYY-MM-DD");
+    },
+    month() {
+      return this.date.getMonth() + 1;
+    },
+  },
+  methods: {
+    pickDay(day) {
+      this.realSelectedDay = day;
+    },
+    toDate(val) {
+      if (!val) {
+        throw new Error("invalid val");
+      }
+      return val instanceof Date ? val : new Date(val);
+    },
+    selectDate(type) {
+      let day = "";
+      if (type === "prev-month") {
+        day = `${this.prevMonthDatePrefix}-01`;
+      } else if (type === "next-month") {
+        day = `${this.nextMonthDatePrefix}-01`;
+      }
+
+      if (day === this.formatedDate) return;
+      this.pickDay(day);
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.panel-rg {
+  margin-top: 36px;
+  padding: 0 28px;
+  box-sizing: border-box;
+
+  &__header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 0 60px 50px;
+
+    i {
+      cursor: pointer;
+      font-size: 44px;
+      color: #000000;
+    }
+    p {
+      font-size: 56px;
+      font-weight: bold;
+      color: #202020;
+
+      span {
+        color: #c1aa7b;
+      }
+    }
+  }
+  :deep(.panel-rg__date) {
+    margin: 0 auto 10px;
+    width: calc(100% - 10px);
+    font-size: 24px;
+    table-layout: fixed;
+
+    th {
+      color: #000;
+      font-weight: 400;
+    }
+    td {
+      border: 0 !important;
+    }
+    .el-calendar-day {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      height: 80px;
+      color: #000000;
+
+      &:hover {
+        background-color: transparent;
+      }
+    }
+    .is-selected {
+      background-color: transparent;
+
+      .el-calendar-day {
+        color: var(--van-primary-color);
+        font-weight: bold;
+      }
+    }
+    .prev div,
+    .next div {
+      display: none;
+    }
+  }
+  &__footer {
+    display: flex;
+    justify-content: space-between;
+    padding: 30px 56px;
+    font-size: 24px;
+    border-top: 1px dashed rgba(32, 32, 32, 0.5);
+
+    p {
+      position: relative;
+      padding-left: 24px;
+
+      &::before {
+        content: "";
+        position: absolute;
+        top: 50%;
+        left: 0;
+        width: 12px;
+        height: 12px;
+        border-radius: 12px;
+        transform: translateY(-50%);
+        background-color: #d2b986;
+      }
+      &:first-child::before {
+        background-color: #bc1c24;
+      }
+      &:last-child::before {
+        background-color: #95d2ff;
+      }
+    }
+  }
+}
+</style>

+ 254 - 0
src/components/DateTable/index.vue

@@ -0,0 +1,254 @@
+<template>
+  <table
+    :class="['el-calendar-table', { 'is-range': isInRange }]"
+    cellspacing="0"
+    cellpadding="0"
+  >
+    <thead v-if="!hideHeader">
+      <tr>
+        <th v-for="day in weekDays" :key="day">{{ day }}</th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr
+        v-for="(row, rowIndex) in rows"
+        :key="rowIndex"
+        :class="[
+          'el-calendar-table__row',
+          {
+            'el-calendar-table__row--hide-border': rowIndex === 0 && hideHeader,
+          },
+        ]"
+      >
+        <td
+          v-for="(cell, cellIndex) in row"
+          :key="cellIndex"
+          :class="getCellClass(cell)"
+          @click="pickDay(cell)"
+        >
+          <div class="el-calendar-day">
+            {{ cellRenderProxy(cell) }}
+          </div>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+</template>
+
+<script>
+import { formatDate } from "@dage/utils";
+import {
+  range as rangeArr,
+  getFirstDayOfMonth,
+  getPrevMonthLastDays,
+  getMonthDays,
+  getI18nSettings,
+  validateRangeInOneMonth,
+} from "./utils";
+import { computed, inject, toRefs, defineComponent } from "vue";
+
+export default defineComponent({
+  name: "CalendarComponent",
+  props: {
+    selectedDay: String,
+    range: {
+      type: Array,
+      validator(val) {
+        if (!(val && val.length)) return true;
+        const [start, end] = val;
+        return validateRangeInOneMonth(start, end);
+      },
+    },
+    date: Date,
+    hideHeader: Boolean,
+    firstDayOfWeek: Number,
+  },
+  setup(props, { emit }) {
+    const { selectedDay, range, date, hideHeader, firstDayOfWeek } =
+      toRefs(props);
+    const elCalendar = inject("elCalendar");
+
+    const WEEK_DAYS = computed(() => getI18nSettings().dayNames);
+
+    const prevMonthDatePrefix = computed(() => {
+      const temp = new Date(date.value.getTime());
+      temp.setDate(0);
+      return formatDate(temp, "YYYY-MM");
+    });
+
+    const curMonthDatePrefix = computed(() =>
+      formatDate(date.value, "YYYY-MM")
+    );
+
+    const nextMonthDatePrefix = computed(() => {
+      const temp = new Date(
+        date.value.getFullYear(),
+        date.value.getMonth() + 1,
+        1
+      );
+      return formatDate(temp, "YYYY-MM");
+    });
+
+    const formatedToday = computed(() => elCalendar.formatedToday);
+
+    const isInRange = computed(() => range.value && range.value.length);
+
+    const weekDays = computed(() => {
+      const start = firstDayOfWeek.value;
+      const { WEEK_DAYS } = { WEEK_DAYS: getI18nSettings().dayNames };
+
+      if (typeof start !== "number" || start === 0) {
+        return WEEK_DAYS.slice();
+      } else {
+        return WEEK_DAYS.slice(start).concat(WEEK_DAYS.slice(0, start));
+      }
+    });
+
+    const rows = computed(() => {
+      let days = [];
+      if (isInRange.value) {
+        const [start, end] = range.value;
+        const currentMonthRange = rangeArr(
+          end.getDate() - start.getDate() + 1
+        ).map((_, index) => ({
+          text: start.getDate() + index,
+          type: "current",
+        }));
+        let remaining = currentMonthRange.length % 7;
+        remaining = remaining === 0 ? 0 : 7 - remaining;
+        const nextMonthRange = rangeArr(remaining).map((_, index) => ({
+          text: index + 1,
+          type: "next",
+        }));
+        days = currentMonthRange.concat(nextMonthRange);
+      } else {
+        let firstDay = getFirstDayOfMonth(date.value);
+        firstDay = firstDay === 0 ? 7 : firstDay;
+        const firstDayOfWeek =
+          typeof props.firstDayOfWeek === "number" ? props.firstDayOfWeek : 1;
+        const offset = (7 + firstDay - firstDayOfWeek) % 7;
+        const prevMonthDays = getPrevMonthLastDays(date.value, offset).map(
+          (day) => ({
+            text: day,
+            type: "prev",
+          })
+        );
+        const currentMonthDays = getMonthDays(date.value).map((day) => ({
+          text: day,
+          type: "current",
+        }));
+        days = [...prevMonthDays, ...currentMonthDays];
+        const nextMonthDays = rangeArr(42 - days.length).map((_, index) => ({
+          text: index + 1,
+          type: "next",
+        }));
+        days = days.concat(nextMonthDays);
+      }
+      return toNestedArr(days);
+    });
+
+    function toNestedArr(days) {
+      return rangeArr(days.length / 7).map((_, index) => {
+        const start = index * 7;
+        return days.slice(start, start + 7);
+      });
+    }
+
+    function getFormateDate(day, type) {
+      if (!day || ["prev", "current", "next"].indexOf(type) === -1) {
+        throw new Error("invalid day or type");
+      }
+      let prefix = curMonthDatePrefix.value;
+      if (type === "prev") {
+        prefix = prevMonthDatePrefix.value;
+      } else if (type === "next") {
+        prefix = nextMonthDatePrefix.value;
+      }
+      day = `00${day}`.slice(-2);
+      return `${prefix}-${day}`;
+    }
+
+    function getCellClass({ text, type }) {
+      const classes = [type];
+      if (type === "current") {
+        const date = getFormateDate(text, type);
+        if (date === selectedDay.value) {
+          classes.push("is-selected");
+        }
+        if (date === formatedToday.value) {
+          classes.push("is-today");
+        }
+      }
+      return classes;
+    }
+
+    function pickDay({ text, type }) {
+      const date = getFormateDate(text, type);
+      emit("pick", date);
+    }
+
+    function cellRenderProxy({ text, type }) {
+      let render = elCalendar.$slots.dateCell;
+      if (!render) return text;
+
+      const day = getFormateDate(text, type);
+      const date = new Date(day);
+      const data = {
+        isSelected: selectedDay.value === day,
+        type: `${type}-month`,
+        day,
+      };
+      return render({ date, data });
+    }
+
+    return {
+      WEEK_DAYS,
+      prevMonthDatePrefix,
+      curMonthDatePrefix,
+      nextMonthDatePrefix,
+      formatedToday,
+      isInRange,
+      rows,
+      weekDays,
+      toNestedArr,
+      getFormateDate,
+      getCellClass,
+      pickDay,
+      cellRenderProxy,
+    };
+  },
+});
+</script>
+
+<style lang="scss">
+.el-calendar-table {
+  table-layout: fixed;
+  width: 100%;
+
+  thead th {
+    padding: 24px 0;
+    color: #606266;
+    font-weight: 400;
+  }
+
+  td {
+    border-bottom: 1px solid #ebeef5;
+    border-right: 1px solid #ebeef5;
+    vertical-align: top;
+    transition: background-color 0.2s ease;
+
+    &.prev {
+      color: #c0c4cc;
+    }
+  }
+  tr:first-child td {
+    border-top: 1px solid #ebeef5;
+  }
+  .el-calendar-day {
+    box-sizing: border-box;
+    padding: 16px;
+    height: 170px;
+    cursor: pointer;
+  }
+}
+</style>

+ 347 - 0
src/components/DateTable/utils.js

@@ -0,0 +1,347 @@
+import {
+  formatDate as _formatDate,
+  parseDate as _parseDate,
+} from "@dage/utils";
+
+const weeks = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+export const months = [
+  "Jan",
+  "Feb",
+  "Mar",
+  "Apr",
+  "May",
+  "Jun",
+  "Jul",
+  "Aug",
+  "Sep",
+  "Oct",
+  "Nov",
+  "Dec",
+];
+export const fullMonths = [
+  "January",
+  "February",
+  "March",
+  "April",
+  "May",
+  "June",
+  "July",
+  "August",
+  "September",
+  "October",
+  "November",
+  "December",
+];
+
+const newArray = function (start, end) {
+  let result = [];
+  for (let i = start; i <= end; i++) {
+    result.push(i);
+  }
+  return result;
+};
+
+export const getI18nSettings = () => {
+  return {
+    dayNamesShort: weeks,
+    dayNames: weeks,
+    monthNamesShort: months,
+    monthNames: months,
+    amPm: ["am", "pm"],
+  };
+};
+
+export const toDate = function (date) {
+  return isDate(date) ? new Date(date) : null;
+};
+
+export const isDate = function (date) {
+  if (date === null || date === undefined) return false;
+  if (isNaN(new Date(date).getTime())) return false;
+  if (Array.isArray(date)) return false; // deal with `new Date([ new Date() ]) -> new Date()`
+  return true;
+};
+
+export const isDateObject = function (val) {
+  return val instanceof Date;
+};
+
+export const formatDate = function (date, format) {
+  date = toDate(date);
+  if (!date) return "";
+  return _formatDate(date, format || "YYYY-MM-DD");
+};
+
+export const parseDate = function (string, format) {
+  return _parseDate(string, format || "YYYY-MM-DD");
+};
+
+export const getDayCountOfMonth = function (year, month) {
+  if (isNaN(+month)) return 31;
+
+  return new Date(year, +month + 1, 0).getDate();
+};
+
+export const getDayCountOfYear = function (year) {
+  const isLeapYear = year % 400 === 0 || (year % 100 !== 0 && year % 4 === 0);
+  return isLeapYear ? 366 : 365;
+};
+
+export const getFirstDayOfMonth = function (date) {
+  const temp = new Date(date.getTime());
+  temp.setDate(1);
+  return temp.getDay();
+};
+
+// see: https://stackoverflow.com/questions/3674539/incrementing-a-date-in-javascript
+// {prev, next} Date should work for Daylight Saving Time
+// Adding 24 * 60 * 60 * 1000 does not work in the above scenario
+export const prevDate = function (date, amount = 1) {
+  return new Date(date.getFullYear(), date.getMonth(), date.getDate() - amount);
+};
+
+export const nextDate = function (date, amount = 1) {
+  return new Date(date.getFullYear(), date.getMonth(), date.getDate() + amount);
+};
+
+export const getStartDateOfMonth = function (year, month) {
+  const result = new Date(year, month, 1);
+  const day = result.getDay();
+
+  if (day === 0) {
+    return prevDate(result, 7);
+  } else {
+    return prevDate(result, day);
+  }
+};
+
+export const getWeekNumber = function (src) {
+  if (!isDate(src)) return null;
+  const date = new Date(src.getTime());
+  date.setHours(0, 0, 0, 0);
+  // Thursday in current week decides the year.
+  date.setDate(date.getDate() + 3 - ((date.getDay() + 6) % 7));
+  // January 4 is always in week 1.
+  const week1 = new Date(date.getFullYear(), 0, 4);
+  // Adjust to Thursday in week 1 and count number of weeks from date to week 1.
+  // Rounding should be fine for Daylight Saving Time. Its shift should never be more than 12 hours.
+  return (
+    1 +
+    Math.round(
+      ((date.getTime() - week1.getTime()) / 86400000 -
+        3 +
+        ((week1.getDay() + 6) % 7)) /
+        7
+    )
+  );
+};
+
+export const getRangeHours = function (ranges) {
+  const hours = [];
+  let disabledHours = [];
+
+  (ranges || []).forEach((range) => {
+    const value = range.map((date) => date.getHours());
+
+    disabledHours = disabledHours.concat(newArray(value[0], value[1]));
+  });
+
+  if (disabledHours.length) {
+    for (let i = 0; i < 24; i++) {
+      hours[i] = disabledHours.indexOf(i) === -1;
+    }
+  } else {
+    for (let i = 0; i < 24; i++) {
+      hours[i] = false;
+    }
+  }
+
+  return hours;
+};
+
+export const getPrevMonthLastDays = (date, amount) => {
+  if (amount <= 0) return [];
+  const temp = new Date(date.getTime());
+  temp.setDate(0);
+  const lastDay = temp.getDate();
+  return range(amount).map((_, index) => lastDay - (amount - index - 1));
+};
+
+export const getMonthDays = (date) => {
+  const temp = new Date(date.getFullYear(), date.getMonth() + 1, 0);
+  const days = temp.getDate();
+  return range(days).map((_, index) => index + 1);
+};
+
+function setRangeData(arr, start, end, value) {
+  for (let i = start; i < end; i++) {
+    arr[i] = value;
+  }
+}
+
+export const getRangeMinutes = function (ranges, hour) {
+  const minutes = new Array(60);
+
+  if (ranges.length > 0) {
+    ranges.forEach((range) => {
+      const start = range[0];
+      const end = range[1];
+      const startHour = start.getHours();
+      const startMinute = start.getMinutes();
+      const endHour = end.getHours();
+      const endMinute = end.getMinutes();
+      if (startHour === hour && endHour !== hour) {
+        setRangeData(minutes, startMinute, 60, true);
+      } else if (startHour === hour && endHour === hour) {
+        setRangeData(minutes, startMinute, endMinute + 1, true);
+      } else if (startHour !== hour && endHour === hour) {
+        setRangeData(minutes, 0, endMinute + 1, true);
+      } else if (startHour < hour && endHour > hour) {
+        setRangeData(minutes, 0, 60, true);
+      }
+    });
+  } else {
+    setRangeData(minutes, 0, 60, true);
+  }
+  return minutes;
+};
+
+export const range = function (n) {
+  // see https://stackoverflow.com/questions/3746725/create-a-javascript-array-containing-1-n
+  return Array.apply(null, { length: n }).map((_, n) => n);
+};
+
+export const modifyDate = function (date, y, m, d) {
+  return new Date(
+    y,
+    m,
+    d,
+    date.getHours(),
+    date.getMinutes(),
+    date.getSeconds(),
+    date.getMilliseconds()
+  );
+};
+
+export const modifyTime = function (date, h, m, s) {
+  return new Date(
+    date.getFullYear(),
+    date.getMonth(),
+    date.getDate(),
+    h,
+    m,
+    s,
+    date.getMilliseconds()
+  );
+};
+
+export const modifyWithTimeString = (date, time) => {
+  if (date == null || !time) {
+    return date;
+  }
+  time = parseDate(time, "HH:mm:ss");
+  return modifyTime(
+    date,
+    time.getHours(),
+    time.getMinutes(),
+    time.getSeconds()
+  );
+};
+
+export const clearTime = function (date) {
+  return new Date(date.getFullYear(), date.getMonth(), date.getDate());
+};
+
+export const clearMilliseconds = function (date) {
+  return new Date(
+    date.getFullYear(),
+    date.getMonth(),
+    date.getDate(),
+    date.getHours(),
+    date.getMinutes(),
+    date.getSeconds(),
+    0
+  );
+};
+
+export const limitTimeRange = function (date, ranges, format = "HH:mm:ss") {
+  // TODO: refactory a more elegant solution
+  if (ranges.length === 0) return date;
+  const normalizeDate = (date) => parseDate(formatDate(date, format), format);
+  const ndate = normalizeDate(date);
+  const nranges = ranges.map((range) => range.map(normalizeDate));
+  if (nranges.some((nrange) => ndate >= nrange[0] && ndate <= nrange[1]))
+    return date;
+
+  let minDate = nranges[0][0];
+  let maxDate = nranges[0][0];
+
+  nranges.forEach((nrange) => {
+    minDate = new Date(Math.min(nrange[0], minDate));
+    maxDate = new Date(Math.max(nrange[1], minDate));
+  });
+
+  const ret = ndate < minDate ? minDate : maxDate;
+  // preserve Year/Month/Date
+  return modifyDate(ret, date.getFullYear(), date.getMonth(), date.getDate());
+};
+
+export const timeWithinRange = function (date, selectableRange, format) {
+  const limitedDate = limitTimeRange(date, selectableRange, format);
+  return limitedDate.getTime() === date.getTime();
+};
+
+export const changeYearMonthAndClampDate = function (date, year, month) {
+  // clamp date to the number of days in `year`, `month`
+  // eg: (2010-1-31, 2010, 2) => 2010-2-28
+  const monthDate = Math.min(date.getDate(), getDayCountOfMonth(year, month));
+  return modifyDate(date, year, month, monthDate);
+};
+
+export const prevMonth = function (date) {
+  const year = date.getFullYear();
+  const month = date.getMonth();
+  return month === 0
+    ? changeYearMonthAndClampDate(date, year - 1, 11)
+    : changeYearMonthAndClampDate(date, year, month - 1);
+};
+
+export const nextMonth = function (date) {
+  const year = date.getFullYear();
+  const month = date.getMonth();
+  return month === 11
+    ? changeYearMonthAndClampDate(date, year + 1, 0)
+    : changeYearMonthAndClampDate(date, year, month + 1);
+};
+
+export const prevYear = function (date, amount = 1) {
+  const year = date.getFullYear();
+  const month = date.getMonth();
+  return changeYearMonthAndClampDate(date, year - amount, month);
+};
+
+export const nextYear = function (date, amount = 1) {
+  const year = date.getFullYear();
+  const month = date.getMonth();
+  return changeYearMonthAndClampDate(date, year + amount, month);
+};
+
+export const extractDateFormat = function (format) {
+  return format
+    .replace(/\W?m{1,2}|\W?ZZ/g, "")
+    .replace(/\W?h{1,2}|\W?s{1,3}|\W?a/gi, "")
+    .trim();
+};
+
+export const extractTimeFormat = function (format) {
+  return format
+    .replace(/\W?D{1,2}|\W?Do|\W?d{1,4}|\W?M{1,4}|\W?y{2,4}/g, "")
+    .trim();
+};
+
+export const validateRangeInOneMonth = function (start, end) {
+  return (
+    start.getMonth() === end.getMonth() &&
+    start.getFullYear() === end.getFullYear()
+  );
+};

+ 37 - 0
src/components/ImgCard.vue

@@ -0,0 +1,37 @@
+<template>
+  <div class="img-card">
+    <VanImage
+      class="img-card__img"
+      :height="160"
+      src="https://en.capitalmuseum.org.cn/data/Exhibitions/Past/40.jpg"
+    />
+
+    <div class="img-card__inner">
+      <p class="limit-line line-4">
+        Three Thousand Boundless Universes in Paintings - Portraits of Taoist
+        and Buddhist Figures Collected in the Capital Museum
+      </p>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup></script>
+
+<style lang="scss" scoped>
+.img-card {
+  border-radius: 10px;
+  overflow: hidden;
+  background: white;
+  box-shadow: 0 2px 8px 6px #ccc;
+
+  &__img {
+    display: block;
+  }
+  &__inner {
+    color: #1f1f1f;
+    padding: 30px 20px;
+    line-height: 36px;
+    font-family: "SourceHanSans-Medium";
+  }
+}
+</style>

+ 61 - 0
src/components/ImgSwiper.vue

@@ -0,0 +1,61 @@
+<template>
+  <div
+    class="img-swiper"
+    v-touch:swipe.left="moveSwiper"
+    v-touch:swipe.right="moveSwiper"
+  >
+    <VanImage
+      v-for="(item, idx) in list"
+      :key="item"
+      :class="{ active: idx === current }"
+      :src="item"
+      @click="idx === current && (showPreview = true)"
+    />
+  </div>
+
+  <VanImagePreview
+    v-model:show="showPreview"
+    :images="list"
+    :start-position="current"
+  />
+</template>
+
+<script lang="ts" setup>
+import { ref } from "vue";
+
+const props = defineProps<{
+  list: string[];
+}>();
+
+const current = ref(0);
+const showPreview = ref(false);
+
+const moveSwiper = (type: "left" | "right") => {
+  current.value =
+    type === "left"
+      ? (current.value + 1) % props.list.length
+      : (current.value - 1 + props.list.length) % props.list.length;
+};
+</script>
+
+<style lang="scss" scoped>
+.img-swiper {
+  display: flex;
+  align-items: center;
+  justify-content: space-around;
+  height: 600px;
+
+  .van-image {
+    width: 5%;
+    height: 580px;
+    overflow: hidden;
+    border-radius: 16px;
+    transition: width linear 0.3s;
+
+    &.active {
+      width: 45%;
+      height: 100%;
+    }
+  }
+}
+</style>

+ 80 - 0
src/components/Layout/components/Search/index.vue

@@ -0,0 +1,80 @@
+<template>
+  <VanPopup v-model:show="show" class="layout-search" :overlay="false">
+    <div class="layout-search-header">
+      <div class="layout-search-header__input">
+        <input placeholder="search......" />
+      </div>
+
+      <div class="layout-search-header__close" @click="show = false">
+        Cancel
+      </div>
+    </div>
+  </VanPopup>
+</template>
+
+<script setup lang="ts">
+import { computed, ref } from "vue";
+
+const props = defineProps<{
+  visible: boolean;
+}>();
+const emits = defineEmits(["update:visible"]);
+
+const show = computed({
+  get() {
+    return props.visible;
+  },
+  set(v: boolean) {
+    emits("update:visible", v);
+  },
+});
+</script>
+
+<style lang="scss" scoped>
+.layout-search {
+  --van-popup-background: #f9f8f5;
+
+  padding: 20px;
+  width: 100%;
+  height: 100%;
+  max-width: 100%;
+
+  &-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    height: 80px;
+
+    &__input {
+      position: relative;
+      padding: 0 30px 0 80px;
+      width: 80%;
+      height: inherit;
+      border-radius: 40px;
+      border: 1px solid #d2b986;
+
+      input {
+        border: 0;
+        width: 100%;
+        height: calc(100% - 2px);
+        background: transparent;
+      }
+      &::before {
+        content: "";
+        position: absolute;
+        top: 50%;
+        left: 20px;
+        width: 40px;
+        height: 40px;
+        background: url("../../images/search2.png") no-repeat center / contain;
+        transform: translateY(-50%);
+      }
+    }
+    &__close {
+      flex-shrink: 0;
+      white-space: nowrap;
+      font-weight: bold;
+    }
+  }
+}
+</style>

+ 72 - 0
src/components/Layout/components/Sidebar/index.scss

@@ -0,0 +1,72 @@
+.sidebar {
+  width: 90%;
+  height: 100%;
+  background: #f6f4f1;
+
+  &-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-around;
+    height: 100px;
+
+    &__close {
+      font-size: 56px;
+    }
+    &__logo {
+      margin: 0 30px;
+      width: 320px;
+      height: 64px;
+    }
+    &__lang,
+    &__search {
+      width: 40px;
+      height: 40px;
+    }
+  }
+
+  &-main {
+    padding: 40px 60px;
+  }
+
+  &-nav {
+    --van-cell-right-icon-color: #c1aa7b;
+    --van-collapse-item-content-background: #f6f4f1;
+    --van-cell-background: #f6f4f1;
+    --van-cell-font-size: 40px;
+    --van-cell-horizontal-padding: 0;
+    --van-collapse-item-content-font-size: 32px;
+    --van-collapse-item-content-text-color: #c1aa7b;
+    --van-collapse-item-content-padding: 0 40px;
+
+    :deep(.van-collapse-item__title) {
+      color: #c1aa7b;
+      font-weight: bold;
+    }
+    :deep(.van-collapse-item) {
+      &:nth-child(6) {
+        margin-top: 10px;
+        padding-top: 20px;
+        border-top: 1px solid #c1aa7b;
+      }
+      &:nth-child(7) {
+        padding-bottom: 20px;
+        border-bottom: 1px solid #c1aa7b;
+      }
+    }
+    &__link {
+      padding: 10px 0;
+    }
+  }
+
+  &-qrcode {
+    display: flex;
+    justify-content: space-between;
+    margin-top: 60px;
+
+    img {
+      width: 40%;
+      max-width: 300px;
+      max-height: 300px;
+    }
+  }
+}

+ 89 - 0
src/components/Layout/components/Sidebar/index.vue

@@ -0,0 +1,89 @@
+<template>
+  <VanPopup v-model:show="show" position="left" class="sidebar">
+    <div class="sidebar-header">
+      <VanIcon
+        class="sidebar-header__close"
+        name="arrow-left"
+        @click="show = false"
+      />
+
+      <img class="sidebar-header__logo" src="@/assets/images/logo2.png" />
+
+      <img
+        class="sidebar-header__lang"
+        src="../../images/zhong2.png"
+        @click="emits('goToCNWeb')"
+      />
+      <img class="sidebar-header__search" src="../../images/search2.png" />
+    </div>
+
+    <div class="sidebar-main">
+      <VanCollapse v-model="activeCollapse" accordion class="sidebar-nav">
+        <VanCollapseItem
+          v-for="item in topData"
+          :key="item.id"
+          :title="item.name"
+          :name="item.id"
+        >
+          <template #title>
+            <div
+              class="sidebar-nav__title"
+              @click.stop="() => {
+                $router.push(item.routeParams!)
+                show = false
+              }"
+            >
+              {{ item.name }}
+            </div>
+          </template>
+
+          <div
+            v-for="sub in item.children"
+            :key="sub.id"
+            class="sidebar-nav__link"
+            @click="handleClick(sub)"
+          >
+            {{ sub.name }}
+          </div>
+        </VanCollapseItem>
+      </VanCollapse>
+
+      <div class="sidebar-qrcode">
+        <img src="../../images/erwei1.png" alt="" />
+        <img src="../../images/erwei2.png" alt="" />
+      </div>
+    </div>
+  </VanPopup>
+</template>
+
+<script setup lang="ts">
+import { computed, ref } from "vue";
+import { topData, type TOP_DATA } from "../../data";
+import { useRouter } from "vue-router";
+
+const props = defineProps<{
+  visible: boolean;
+}>();
+const emits = defineEmits(["update:visible", "goToCNWeb"]);
+
+const router = useRouter();
+const activeCollapse = ref();
+
+const show = computed({
+  get() {
+    return props.visible;
+  },
+  set(v: boolean) {
+    emits("update:visible", v);
+  },
+});
+
+const handleClick = (item: TOP_DATA) => {
+  router.push(item.routeParams!);
+  show.value = false;
+};
+</script>
+
+<style lang="scss" scoped>
+@import "./index.scss";
+</style>

+ 223 - 0
src/components/Layout/data.ts

@@ -0,0 +1,223 @@
+import type { RouteLocationRaw } from "vue-router";
+
+export interface TOP_DATA {
+  id: number;
+  name: string;
+  routeParams?: RouteLocationRaw;
+  children?: TOP_DATA[];
+}
+
+export const topData: TOP_DATA[] = [
+  {
+    id: 2,
+    name: "Visit",
+    routeParams: { name: "Visit", params: { id: 8 } },
+    children: [
+      { routeParams: "/Layout/Visit/8", id: 2.8, name: "Calendar" },
+      {
+        routeParams: "/Layout/Visit/1",
+        id: 2.1,
+        name: "Hours, Direction & Admission",
+      },
+      { routeParams: "/Layout/Visit/2", id: 2.2, name: "Reservation" },
+      { routeParams: "/Layout/Visit/3", id: 2.3, name: "Floor Plans" },
+      { routeParams: "/Layout/Visit/4", id: 2.4, name: "Audio Guide & Tour" },
+      { routeParams: "/Layout/Visit/5", id: 2.5, name: "Accessibility" },
+      { routeParams: "/Layout/Visit/6", id: 2.6, name: "Café & Shop" },
+      { routeParams: "/Layout/Visit/7", id: 2.7, name: "Visitor Guidelines" },
+    ],
+  },
+  {
+    id: 3,
+    name: "Exhibitions",
+    routeParams: { name: "Exhibitions", params: { type: 1 } },
+    children: [
+      {
+        routeParams: "/Layout/Exhibitions/Current",
+        id: 3.1,
+        name: "Current Exhibitions",
+      },
+      {
+        routeParams: "/Layout/Exhibitions/Permanent",
+        id: 3.2,
+        name: "Permanent Exhibitions",
+      },
+      {
+        routeParams: "/Layout/Exhibitions/Past",
+        id: 3.3,
+        name: "Past Exhibitions",
+      },
+      {
+        routeParams: "/Layout/Exhibitions/Overseas",
+        id: 3.4,
+        name: "Overseas Exhibitions",
+      },
+    ],
+  },
+  {
+    id: 4,
+    name: "Collections",
+    routeParams: { name: "Collections", params: { type: "Bronzes" } },
+    children: [
+      { routeParams: "/Layout/Collections/Bronzes", id: 4.1, name: "Bronzes" },
+      {
+        routeParams: "/Layout/Collections/Ceramics",
+        id: 4.2,
+        name: "Ceramics",
+      },
+      {
+        routeParams: "/Layout/Collections/Buddhist",
+        id: 4.3,
+        name: "Buddhist Statues",
+      },
+      {
+        routeParams: "/Layout/Collections/Jadewares",
+        id: 4.4,
+        name: "Jadewares",
+      },
+      {
+        routeParams: "/Layout/Collections/Calligraphies",
+        id: 4.5,
+        name: "Calligraphies",
+      },
+      {
+        routeParams: "/Layout/Collections/Paintings",
+        id: 4.6,
+        name: "Paintings",
+      },
+      {
+        routeParams: "/Layout/Collections/Gold",
+        id: 4.7,
+        name: "Gold & Silverwares",
+      },
+      {
+        routeParams: "/Layout/Collections/Coins",
+        id: 4.8,
+        name: "Coins & Banknotes",
+      },
+      {
+        routeParams: "/Layout/Collections/Brocades",
+        id: 4.9,
+        name: "Brocades & Embroideries",
+      },
+      {
+        routeParams: "/Layout/Collections/Cultural",
+        id: 4.1,
+        name: "Cultural Supplies",
+      },
+      {
+        routeParams: "/Layout/Collections/Miscellaneous",
+        id: 4.11,
+        name: "Miscellaneous",
+      },
+    ],
+  },
+  {
+    id: 5,
+    name: "Learn & Engage",
+    routeParams: { name: "LearnEngage", params: { type: "Students" } },
+    children: [
+      {
+        routeParams: "/Layout/LearnEngage/Students",
+        id: 5.1,
+        name: "For Students",
+      },
+      {
+        routeParams: "/Layout/LearnEngage/Adults",
+        id: 5.2,
+        name: "For Adults",
+      },
+      {
+        routeParams: "/Layout/LearnEngage/Families",
+        id: 5.3,
+        name: "For Families & Children",
+      },
+    ],
+  },
+  {
+    id: 6,
+    name: "Publications",
+    routeParams: { name: "Publications" },
+    children: [
+      { routeParams: "/Layout/Publications/1", id: 6.1, name: "Magazines" },
+      {
+        routeParams: "/Layout/Publications/2",
+        id: 6.2,
+        name: "Exhibition Catalogues",
+      },
+    ],
+  },
+  {
+    id: 7,
+    name: "Join & Support",
+    routeParams: { name: "JoinSupport" },
+    children: [
+      {
+        routeParams: "/Layout/JoinSupport/Volunteer",
+        id: 7.1,
+        name: "Ways to Volunteer",
+        children: [
+          {
+            routeParams: "/Layout/JoinSupport/VolunteerInfo?id=1",
+            id: 7.11,
+            name: "Volunteer Team Introduction",
+          },
+          {
+            routeParams: "/Layout/JoinSupport/VolunteerInfo?id=2",
+            id: 7.12,
+            name: "Volunteer Apply",
+          },
+          {
+            routeParams: "/Layout/JoinSupport/VolunteerInfo?id=3",
+            id: 7.13,
+            name: "Volunteer Program",
+          },
+        ],
+      },
+      {
+        routeParams: "/Layout/JoinSupport/Give",
+        id: 7.2,
+        name: "Ways to Give",
+        children: [
+          {
+            routeParams: "/Layout/JoinSupport/GiveInfo?id=4",
+            id: 7.21,
+            name: "Individuals",
+          },
+          {
+            routeParams: "/Layout/JoinSupport/GiveInfo?id=5",
+            id: 7.22,
+            name: "Corporations Institutions",
+          },
+        ],
+      },
+    ],
+  },
+  {
+    id: 8,
+    name: "About",
+    routeParams: { name: "About" },
+    children: [
+      {
+        routeParams: { name: "About", query: { scroll: 352 } },
+        id: 8.1,
+        name: "From the Director",
+      },
+      {
+        routeParams: { name: "About", query: { scroll: 816 } },
+        id: 8.2,
+        name: "History",
+      },
+      {
+        routeParams: { name: "About", query: { scroll: 1319 } },
+        id: 8.3,
+        name: "Partners & Connections",
+      },
+      {
+        routeParams: { name: "About", query: { scroll: 1525 } },
+        id: 8.4,
+        name: "Contact",
+      },
+    ],
+  },
+];

BIN
src/components/Layout/images/erwei1.png


BIN
src/components/Layout/images/erwei2.png


BIN
src/components/Layout/images/mean.png


BIN
src/components/Layout/images/search.png


BIN
src/components/Layout/images/search2.png


BIN
src/components/Layout/images/zhong.png


BIN
src/components/Layout/images/zhong2.png


+ 112 - 0
src/components/Layout/index.scss

@@ -0,0 +1,112 @@
+:root {
+  --van-primary-color: #bc1c24;
+  --van-gray-7: #6a6a6a;
+}
+
+.layout {
+  padding-top: 100px;
+  font-size: 28px;
+  height: 100vh;
+  overflow-y: auto;
+
+  &-header {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: 100px;
+    z-index: 997;
+
+    &__menu,
+    &__right {
+      position: absolute;
+      top: 50%;
+      transform: translateY(-50%);
+    }
+    &__menu {
+      left: 20px;
+      width: 40px;
+      height: 40px;
+    }
+    &__right {
+      display: flex;
+      gap: 30px;
+      right: 20px;
+
+      img {
+        width: 40px;
+        height: 40px;
+      }
+    }
+    &__logo {
+      width: 320px;
+      height: 66px;
+    }
+  }
+
+  .notice {
+    position: fixed;
+    top: 100px;
+    left: 0;
+    right: 0;
+    z-index: 9;
+  }
+
+  &-bottom {
+    padding: 40px 40px 0;
+    color: white;
+    background: #c1aa7b;
+
+    &__logo {
+      display: block;
+      width: 540px;
+    }
+    &__qrcode {
+      display: flex;
+      justify-content: space-between;
+      margin: 60px 0 40px;
+      padding: 0 20px;
+
+      img {
+        width: 300px;
+      }
+    }
+    p {
+      padding: 0 20px 40px;
+      border-bottom: 1px solid #d1d1d1;
+    }
+    &__link {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      height: 100px;
+
+      a {
+        color: inherit;
+      }
+      li:not(:last-child) {
+        margin-right: 30px;
+        padding-right: 30px;
+        border-right: 1px solid #fff;
+      }
+    }
+  }
+
+  .toTop {
+    position: fixed;
+    width: 96px;
+    height: 96px;
+    text-align: center;
+    line-height: 96px;
+    border-radius: 50%;
+    background-color: rgba(193, 170, 123, 0.5);
+    color: #fff;
+    font-size: 60px;
+    right: 20px;
+    bottom: 200px;
+    z-index: 99;
+  }
+}

+ 106 - 1
src/components/Layout/index.vue

@@ -1,3 +1,108 @@
 <template>
-  <div>12</div>
+  <div ref="layoutRef" class="layout" @scroll="scrollEvent">
+    <!-- 顶部 -->
+    <div class="layout-header" :style="{ background: $route.meta.navBgColor }">
+      <img
+        class="layout-header__menu"
+        src="./images/mean.png"
+        @click="sidebarVisible = true"
+      />
+
+      <img
+        class="layout-header__logo"
+        src="@/assets/images/logo.png"
+        @click="$router.push({ name: 'Home' })"
+      />
+
+      <div class="layout-header__right">
+        <img src="./images/zhong.png" @click="goToCNWeb" />
+        <img src="./images/search.png" @click="searchVisible = true" />
+      </div>
+    </div>
+
+    <div v-if="noticeVisible" class="notice">
+      <VanNoticeBar
+        mode="closeable"
+        left-icon="volume-o"
+        color="#ffffff"
+        :delay="3"
+        background="rgb(116, 18, 11)"
+        text="Due to permanent exhibition refurbishment, our museum has been closed since October 7, 2023. We have also temporarily closed the reservation channel. The specific reopening time will be announced separately. We apologize for any inconvenience this may cause."
+        @close="noticeVisible = false"
+      />
+    </div>
+
+    <RouterView />
+
+    <!-- 回到顶部 -->
+    <div class="toTop" v-show="srocllShow" @click="toTop">
+      <VanIcon name="back-top" />
+    </div>
+
+    <!-- 底部 -->
+    <div class="layout-bottom">
+      <img
+        class="layout-bottom__logo"
+        src="@/assets/images/logo.png"
+        @click="$router.push({ name: 'Home' })"
+      />
+
+      <div class="layout-bottom__qrcode">
+        <img src="./images/erwei1.png" alt="" />
+        <img src="./images/erwei2.png" alt="" />
+      </div>
+
+      <p>
+        Capital Museum. China 16 Fuxingmenwai Street, Xicheng District, Beijing
+        100045, P.R.China.
+      </p>
+
+      <ul class="layout-bottom__link">
+        <li>
+          <RouterLink :to="{ name: 'Use' }">Terms of Use</RouterLink>
+        </li>
+        <li>
+          <RouterLink :to="{ name: 'Events' }">Events</RouterLink>
+        </li>
+        <li>
+          <RouterLink :to="{ name: 'Employment' }">Employment</RouterLink>
+        </li>
+      </ul>
+    </div>
+  </div>
+
+  <Sidebar v-model:visible="sidebarVisible" @goToCNWeb="goToCNWeb" />
+  <Search v-model:visible="searchVisible" />
 </template>
+
+<script lang="ts" setup>
+import { ref } from "vue";
+import { debounce } from "lodash-unified";
+import Sidebar from "./components/Sidebar/index.vue";
+import Search from "./components/Search/index.vue";
+
+const layoutRef = ref();
+const noticeVisible = ref(false);
+const sidebarVisible = ref(false);
+const searchVisible = ref(false);
+const srocllShow = ref(false);
+
+const toTop = () => {
+  const dom = layoutRef.value;
+  dom.scrollTo({ top: 0, behavior: "smooth" });
+};
+
+const scrollEvent = debounce(() => {
+  const dom = layoutRef.value;
+  if (dom.scrollTop > 400) srocllShow.value = true;
+  else srocllShow.value = false;
+}, 200);
+
+const goToCNWeb = () => {
+  window.open("https://www.capitalmuseum.org.cn/", "_blank");
+};
+</script>
+
+<style lang="scss">
+@import "./index.scss";
+</style>

+ 33 - 0
src/components/PageBanner.vue

@@ -0,0 +1,33 @@
+<template>
+  <div class="page-banner">
+    <img :src="img" />
+    <h3>{{ title }}</h3>
+  </div>
+</template>
+
+<script lang="ts" setup>
+defineProps<{
+  title: string;
+  img: string;
+}>();
+</script>
+
+<style lang="scss" scoped>
+.page-banner {
+  position: relative;
+
+  img {
+    display: block;
+    width: 100%;
+    height: 300px;
+  }
+  h3 {
+    position: absolute;
+    left: 60px;
+    bottom: 60px;
+    color: white;
+    font-size: 48px;
+    border-bottom: 1px solid #fff;
+  }
+}
+</style>

BIN
src/components/PageLabel/icon.png


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

@@ -0,0 +1,24 @@
+<template>
+  <div class="page-label">
+    <img src="./icon.png" />
+
+    <slot />
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.page-label {
+  margin-bottom: 48px;
+  display: flex;
+  align-items: center;
+  color: #c1aa7b;
+  font-size: 44px;
+  font-weight: bold;
+
+  img {
+    margin-right: 40px;
+    width: 82px;
+    height: 58px;
+  }
+}
+</style>

+ 78 - 0
src/components/PageNav.vue

@@ -0,0 +1,78 @@
+<template>
+  <van-tabs
+    ref="tabsRef"
+    v-model:active="activeTab"
+    class="page-nav"
+    sticky
+    :scrollspy="scrollspy"
+    :animated="animated"
+    :offset-top="getActualPixelValue(100)"
+  >
+    <slot />
+  </van-tabs>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref } from "vue";
+import type { TabsInstance } from "vant";
+import { getActualPixelValue } from "@/utils";
+
+const props = defineProps<{
+  modelValue: string | number;
+  animated?: boolean;
+  scrollspy?: boolean;
+}>();
+const emits = defineEmits(["update:modelValue"]);
+
+const tabsRef = ref<TabsInstance>();
+
+const activeTab = computed({
+  get() {
+    return props.modelValue;
+  },
+  set(v: string | number) {
+    emits("update:modelValue", v);
+  },
+});
+
+defineExpose({
+  tabsRef,
+});
+</script>
+
+<style lang="scss">
+.page-nav {
+  --van-tabs-line-height: 100px;
+  --van-tabs-nav-background: #f7f6f3;
+  --van-tab-text-color: black;
+  --van-tab-active-text-color: white;
+
+  .van-tabs {
+    &__nav {
+      padding: 0 40px !important;
+    }
+    &__line {
+      display: none;
+    }
+  }
+  .van-tab {
+    span {
+      display: block;
+      padding: 0 24px;
+      height: 60px;
+      line-height: 60px;
+      border-radius: 30px;
+      white-space: nowrap;
+      font-weight: normal;
+    }
+    &--grow {
+      padding: 0;
+    }
+    &--active {
+      span {
+        background: #c1aa7b;
+      }
+    }
+  }
+}
+</style>

+ 134 - 0
src/components/SwipeCard.vue

@@ -0,0 +1,134 @@
+<!--  -->
+<template>
+  <div
+    :class="`cardCom ${keyval}`"
+    v-touch:swipe.left="moveSwiper"
+    v-touch:swipe.right="moveSwiper"
+  >
+    <div
+      v-for="(_, index) in cardData"
+      :key="index"
+      class="card"
+      :style="`top:${index * 15}px;width:${100 - index * 5}%;opacity:${
+        1 - index * 0.2 <= 0 ? 0.1 : 1 - index * 0.2
+      }; z-index: ${cardData.length - index};`"
+    >
+      <template v-if="index === 0">
+        <slot :item="cardData[cardInd]" />
+        <span class="page">{{ cardInd + 1 }} / {{ cardData.length }}</span>
+      </template>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "cardCom",
+  props: {
+    cardData: {
+      type: Array,
+      default: () => [],
+    },
+    keyval: {
+      type: String,
+      default: "",
+    },
+  },
+  components: {},
+  data() {
+    //这里存放数据
+    return {
+      cardInd: 0,
+    };
+  },
+  //监听属性 类似于data概念
+  computed: {},
+  //监控data中的数据变化
+  watch: {},
+  //方法集合
+  methods: {
+    // 封装一个滑动的方法
+    moveSwiper(val) {
+      let dom = document.querySelector(`.${this.keyval}`);
+      if (!dom) return;
+      dom.style.opacity = 0;
+      setTimeout(() => {
+        if (val === "right") {
+          // 右滑减小
+          if (this.cardInd === 0) this.cardInd = this.cardData.length - 1;
+          else this.cardInd--;
+        } else {
+          //左滑增加
+          if (this.cardInd < this.cardData.length - 1) this.cardInd++;
+          else this.cardInd = 0;
+        }
+        dom.style.opacity = 1;
+      }, 300);
+    },
+  },
+  //生命周期 - 创建完成(可以访问当前this实例)
+  created() {},
+  //生命周期 - 挂载完成(可以访问DOM元素)
+  mounted() {
+    // 动态控制cardCom元素的高度
+    this.$nextTick(() => {
+      setTimeout(() => {
+        let dom = document.querySelector(`.${this.keyval}`);
+        dom.style.height = 535 + (this.cardData.length - 1) * 15 + "px";
+      }, 300);
+    });
+  },
+  beforeCreate() {}, //生命周期 - 创建之前
+  beforeMount() {}, //生命周期 - 挂载之前
+  beforeUpdate() {}, //生命周期 - 更新之前
+  updated() {}, //生命周期 - 更新之后
+  beforeDestroy() {}, //生命周期 - 销毁之前
+  destroyed() {}, //生命周期 - 销毁完成
+  activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
+};
+</script>
+<style lang="scss" scoped>
+.txt::-webkit-scrollbar {
+  /*滚动条整体样式*/
+  width: 8px; /*高宽分别对应横竖滚动条的尺寸*/
+  height: 2px;
+}
+.txt::-webkit-scrollbar-thumb {
+  /*滚动条里面小方块*/
+  -webkit-box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.2);
+  background: #d9cdb6;
+}
+.txt::-webkit-scrollbar-track {
+  /*滚动条里面轨道*/
+  width: 0px;
+  background: transparent;
+}
+.cardCom {
+  margin-top: 80px;
+  width: 100%;
+  position: relative;
+  transition: all 0.3s;
+}
+.card {
+  background-color: #fff;
+  box-shadow: 0px 2px 8px 6px #ccc;
+  position: absolute;
+  top: 0;
+  left: 50%;
+  transform: translateX(-50%);
+  width: 100%;
+  margin: 0 auto;
+  display: flex;
+  flex-direction: column;
+
+  .page {
+    opacity: 0.3;
+    color: #b79550;
+    position: absolute;
+    font-weight: 700;
+    right: 10px;
+    bottom: 10px;
+    font-size: 32px;
+  }
+}
+</style>

+ 6 - 0
src/main.ts

@@ -9,10 +9,16 @@ import router from "./router";
 import svgIcon from "@/components/SvgIcon/index.vue";
 import "virtual:svg-icons-register";
 
+import Vue3TouchEvents from "vue3-touch-events";
+import PageLabel from "./components/PageLabel/index.vue";
+
 const app = createApp(App);
 
 app.use(createPinia());
 app.use(router);
+// @ts-ignore
+app.use(Vue3TouchEvents);
 app.component("svg-icon", svgIcon);
+app.component("page-label", PageLabel);
 
 app.mount("#app");

+ 7 - 0
src/router-meta.d.ts

@@ -0,0 +1,7 @@
+import "vue-router";
+
+declare module "vue-router" {
+  interface RouteMeta {
+    navBgColor?: string;
+  }
+}

+ 123 - 0
src/router/index.ts

@@ -13,8 +13,131 @@ const router = createRouter({
         {
           path: "/Layout/home",
           name: "Home",
+          meta: {
+            navBgColor: "#74120b",
+          },
           component: () => import("../views/Home/index.vue"),
         },
+        // Visit页面
+        {
+          path: "/Layout/Visit/:id",
+          name: "Visit",
+          component: () => import("../views/Visit/index.vue"),
+          meta: { navBgColor: "#c3ac8d" },
+        },
+        {
+          path: "/Layout/VisitInfo",
+          name: "VisitInfo",
+          component: () => import("../views/VisitInfo/index.vue"),
+          meta: { navBgColor: "#c3ac8d" },
+        },
+        // Exhibitions页面
+        {
+          path: "/Layout/Exhibitions",
+          name: "Exhibitions",
+          redirect: "/Layout/Exhibitions/Current",
+          component: () => import("../views/Exhibitions/index.vue"),
+          meta: { navBgColor: "#bf8a6d" },
+          children: [
+            {
+              path: "/Layout/Exhibitions/Current",
+              name: "ExhibitionsCurrent",
+              component: () => import("../views/Exhibitions/Current/index.vue"),
+              meta: { navBgColor: "#bf8a6d" },
+            },
+            {
+              path: "/Layout/Exhibitions/Permanent",
+              name: "ExhibitionsPermanent",
+              component: () =>
+                import("../views/Exhibitions/Permanent/index.vue"),
+              meta: { navBgColor: "#bf8a6d" },
+            },
+            {
+              path: "/Layout/Exhibitions/Past",
+              name: "ExhibitionsPast",
+              component: () => import("../views/Exhibitions/Past/index.vue"),
+              meta: { navBgColor: "#bf8a6d" },
+            },
+            {
+              path: "/Layout/Exhibitions/Overseas",
+              name: "ExhibitionsOverseas",
+              component: () =>
+                import("../views/Exhibitions/Overseas/index.vue"),
+              meta: { navBgColor: "#bf8a6d" },
+            },
+          ],
+        },
+        // ---------- 详情
+        {
+          path: "/Layout/Detail",
+          name: "ExDetail",
+          component: () => import("../views/Exhibitions/Detail/index.vue"),
+          meta: { navBgColor: "#801c20" },
+        },
+        // ----------Objects
+        {
+          path: "/Layout/Objects",
+          name: "ExObjects",
+          component: () => import("../views/Exhibitions/Objects/index.vue"),
+          meta: { navBgColor: "#801c20" },
+        },
+        // ----------Galleries
+        {
+          path: "/Layout/Galleries",
+          name: "ExGalleries",
+          component: () => import("../views/Exhibitions/Galleries/index.vue"),
+          meta: { navBgColor: "#801c20" },
+        },
+        {
+          path: "/Layout/Use",
+          name: "Use",
+          meta: {
+            navBgColor: "#806245",
+          },
+          component: () => import("../views/Use/index.vue"),
+        },
+        // Collections页面
+        {
+          path: "/Layout/Collections",
+          name: "Collections",
+          component: () => import("../views/Collections/index.vue"),
+          meta: { navBgColor: "#b09c86" },
+        },
+        {
+          path: "/Layout/Collections/:name",
+          name: "CollectionsList",
+          component: () => import("../views/Collections/List/index.vue"),
+          meta: { navBgColor: "#36382f" },
+        },
+        {
+          path: "/Layout/CollectionsDetail",
+          name: "CollectionsDetail",
+          component: () => import("../views/Collections/Detail/index.vue"),
+          meta: { navBgColor: "#656567" },
+        },
+        // Learn页面
+        {
+          path: "/Layout/Learn/:type",
+          name: "LearnEngage",
+          component: () => import("../views/Learn/index.vue"),
+          meta: { navBgColor: "#997369" },
+        },
+        {
+          path: "/Layout/Employment",
+          name: "Employment",
+          meta: {
+            navBgColor: "#806245",
+          },
+          component: () => import("../views/Employment/index.vue"),
+        },
+        {
+          path: "/Layout/Events",
+          name: "Events",
+          meta: {
+            navBgColor: "#a39382",
+          },
+          component: () => import("../views/Events/index.vue"),
+        },
       ],
     },
   ],

+ 2 - 2
src/shims-vue.d.ts

@@ -1,6 +1,6 @@
 /* eslint-disable */
-declare module '*.vue' {
-  import type { DefineComponent } from 'vue';
+declare module "*.vue" {
+  import type { DefineComponent } from "vue";
   const component: DefineComponent<{}, {}, any>;
   export default component;
 }

+ 3 - 0
src/utils/index.ts

@@ -0,0 +1,3 @@
+export const getActualPixelValue = (val: number) => {
+  return ((val / 750) * 100 * window.innerWidth) / 100;
+};

BIN
src/views/Collections/Detail/images/bgCD-min.png


+ 14 - 0
src/views/Collections/Detail/index.scss

@@ -0,0 +1,14 @@
+.collections-detail {
+  &-main {
+    padding: 40px 40px 80px;
+    background: url("./images/bgCD-min.png") no-repeat center bottom / cover;
+
+    h3 {
+      margin-bottom: 40px;
+      padding-left: 60px;
+      font-size: 44px;
+      background: url("@/assets/images/chosen.png") no-repeat left 8px / 44px
+        36px;
+    }
+  }
+}

+ 18 - 0
src/views/Collections/Detail/index.vue

@@ -0,0 +1,18 @@
+<template>
+  <div class="collections-detail">
+    <VanImage
+      src="https://en.capitalmuseum.org.cn/data/Collections/Ceramics/big1.png"
+      style="display: block"
+    />
+
+    <div class="collections-detail-main">
+      <h3>Blue-and-white Snuff Bottle with Mythic Fungus Design</h3>
+
+      <div></div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+@import "./index.scss";
+</style>

BIN
src/views/Collections/List/1.png


BIN
src/views/Collections/List/5.png


BIN
src/views/Collections/List/6.png


+ 13 - 0
src/views/Collections/List/index.scss

@@ -0,0 +1,13 @@
+.collections-list {
+  &-card {
+    padding-bottom: 30px;
+    background-color: #fff;
+    box-shadow: 0 10px 10px 2px #ccc;
+
+    p {
+      padding: 30px 20px 0;
+      line-height: 36px;
+      font-family: "SourceHanSans-Medium";
+    }
+  }
+}

+ 61 - 0
src/views/Collections/List/index.vue

@@ -0,0 +1,61 @@
+<template>
+  <div class="collections-list">
+    <PageBanner
+      title="Ceramics"
+      img="https://en.capitalmuseum.org.cn/data/Collections/topImg/Ceramics.png"
+    />
+
+    <Waterfall
+      background-color="#f7f6f3"
+      :list="list"
+      :breakpoints="{
+        500: { rowPerView: 2 },
+      }"
+      :gutter="20"
+    >
+      <template #item="{ item, url }">
+        <div
+          class="collections-list-card"
+          @click="$router.push({ name: 'CollectionsDetail', query: { id: 1 } })"
+        >
+          <LazyImg :url="url" />
+          <p class="limit-line line-2">{{ item.p }}</p>
+        </div>
+      </template>
+    </Waterfall>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from "vue";
+import { LazyImg, Waterfall } from "vue-waterfall-plugin-next";
+import type { ViewCard } from "vue-waterfall-plugin-next/dist/types/types/waterfall";
+import PageBanner from "@/components/PageBanner.vue";
+import "vue-waterfall-plugin-next/dist/style.css";
+
+import Img1 from "./1.png";
+import Img2 from "./5.png";
+import Img3 from "./6.png";
+
+const list = ref<ViewCard[]>([
+  {
+    id: "1",
+    src: Img1,
+    p: "Blue-and-white Snuff Bottle with Mythic Fungus Design",
+  },
+  {
+    id: "2",
+    src: Img2,
+    p: "Blue-and-white Snuff Bottle with Mythic Fungus Design",
+  },
+  {
+    id: "3",
+    src: Img3,
+    p: "Blue-and-white Snuff Bottle with Mythic Fungus Design",
+  },
+]);
+</script>
+
+<style lang="scss" scoped>
+@import "./index.scss";
+</style>

BIN
src/views/Collections/images/bannerC.png


+ 25 - 0
src/views/Collections/index.scss

@@ -0,0 +1,25 @@
+.collections {
+  background-color: #f7f6f3;
+
+  &-main {
+    display: flex;
+    flex-direction: column;
+    gap: 20px;
+    padding: 40px;
+  }
+  &-card {
+    position: relative;
+    border-radius: 10px;
+    overflow: hidden;
+
+    p {
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      font-size: 48px;
+      font-weight: bold;
+      color: white;
+      transform: translate(-50%, -50%);
+    }
+  }
+}

+ 23 - 0
src/views/Collections/index.vue

@@ -0,0 +1,23 @@
+<template>
+  <div class="collections">
+    <PageBanner title="Collections" :img="BannerImg" />
+
+    <ul class="collections-main">
+      <li v-for="item in 5" :key="item" class="collections-card">
+        <VanImage
+          src="https://en.capitalmuseum.org.cn/mobile/img/Bronzes.f986eec7.png"
+        />
+        <p>Bronzes</p>
+      </li>
+    </ul>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import PageBanner from "@/components/PageBanner.vue";
+import BannerImg from "./images/bannerC.png";
+</script>
+
+<style lang="scss" scoped>
+@import "./index.scss";
+</style>

+ 3 - 0
src/views/Employment/index.vue

@@ -0,0 +1,3 @@
+<template>
+  <div></div>
+</template>

+ 3 - 0
src/views/Events/index.vue

@@ -0,0 +1,3 @@
+<template>
+  <div></div>
+</template>

+ 38 - 0
src/views/Exhibitions/Current/index.vue

@@ -0,0 +1,38 @@
+<template>
+  <div class="current">
+    <div
+      class="current-item"
+      @click="$router.push({ name: 'ExDetail', query: { id: 10 } })"
+    >
+      <VanImage
+        src="https://en.capitalmuseum.org.cn/data/Exhibitions/Current/2.jpg"
+      />
+      <h3>Splendid Central Axis of Beijing</h3>
+      <p>
+        Starting from the planning and construction of the Central Axis of the
+        Capital Dadu of the Yuan Dynasty and with the ongoing inheritance and
+        carrying forward of past achievements over the later dynasties, the
+        Central Axis of Beijing has finally been made such a magnificent
+        presence as it stands now, with originality and creativeness to be found
+        everywhere along the Axis.
+      </p>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.current {
+  &-item {
+    h3 {
+      padding: 40px;
+      font-size: 40px;
+    }
+    p {
+      padding: 0 40px 80px;
+      color: #6a6a6a;
+      font-size: 32px;
+      line-height: 40px;
+    }
+  }
+}
+</style>

BIN
src/views/Exhibitions/Detail/images/bgExD-min.png


+ 87 - 0
src/views/Exhibitions/Detail/index.scss

@@ -0,0 +1,87 @@
+.detail {
+  &-main {
+    padding: 40px 0;
+    background: url("./images/bgExD-min.png") no-repeat center / 100% 100%;
+  }
+  &-info {
+    margin: 0 40px;
+    padding-bottom: 40px;
+    border-bottom: 1px solid #ccc;
+
+    h3 {
+      margin-bottom: 30px;
+      font-size: 36px;
+    }
+    p {
+      color: #666;
+      font-size: 28px;
+      line-height: 72px;
+    }
+    &__date {
+      padding-left: 60px;
+      background: url("@/assets/images/bg_5.png") 2px 16px no-repeat;
+    }
+    &__time {
+      padding-left: 60px;
+      background: url("@/assets/images/bg_6.png") left 16px no-repeat;
+    }
+    &__address {
+      padding-left: 60px;
+      background: url("@/assets/images/bg_7.png") 6px 16px no-repeat;
+    }
+  }
+  &-overview {
+    margin: 0 40px;
+    padding: 80px 0;
+    border-bottom: 1px solid #ccc;
+
+    &-card {
+      :deep(.card) {
+        height: 1000px;
+      }
+      &__container {
+        padding: 40px;
+        height: 932px;
+        overflow-y: auto;
+      }
+      h3 {
+        color: var(--van-primary-color);
+        font-size: 32px;
+        margin-bottom: 30px;
+      }
+      div {
+        line-height: 48px;
+        font-size: 32px;
+        text-align: justify;
+      }
+    }
+  }
+  &-objects {
+    margin: 0 40px;
+    padding: 80px 0;
+    border-bottom: 1px solid #ccc;
+
+    .page-label {
+      padding: 0 40px;
+    }
+    :deep(.img-swiper) {
+      margin: 0 -40px;
+    }
+    .more {
+      margin-top: 80px;
+    }
+  }
+  &-galleries {
+    padding: 80px 0;
+
+    .page-label {
+      padding: 0 40px;
+    }
+    img {
+      width: 100%;
+    }
+    .more {
+      margin-top: 80px;
+    }
+  }
+}

Різницю між файлами не показано, бо вона завелика
+ 109 - 0
src/views/Exhibitions/Detail/index.vue


+ 29 - 0
src/views/Exhibitions/Galleries/index.scss

@@ -0,0 +1,29 @@
+.ex-objects {
+  .page-banner {
+    &::before {
+      content: "";
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      background-color: rgba(0, 0, 0, 0.3);
+      z-index: 1;
+    }
+    :deep(h3) {
+      z-index: 2;
+    }
+  }
+
+  &-main {
+    display: flex;
+    flex-direction: column;
+    gap: 40px;
+    padding: 60px 40px;
+
+    img {
+      width: 100%;
+      border-radius: 10px;
+    }
+  }
+}

+ 40 - 0
src/views/Exhibitions/Galleries/index.vue

@@ -0,0 +1,40 @@
+<template>
+  <div class="ex-objects">
+    <PageBanner title="Exhibition Galleries" :img="BannerImg" />
+
+    <div class="ex-objects-main">
+      <img
+        src="https://en.capitalmuseum.org.cn/data/Exhibitions/Current/galleries2/galleries1.jpg"
+        alt=""
+      />
+      <img
+        src="https://en.capitalmuseum.org.cn/data/Exhibitions/Current/galleries2/galleries1.jpg"
+        alt=""
+      />
+      <img
+        src="https://en.capitalmuseum.org.cn/data/Exhibitions/Current/galleries2/galleries1.jpg"
+        alt=""
+      />
+    </div>
+  </div>
+
+  <VanImagePreview
+    v-model:show="showPreview"
+    :images="previewList"
+    :start-position="curPreviewIdx"
+  />
+</template>
+
+<script lang="ts" setup>
+import { ref } from "vue";
+import PageBanner from "@/components/PageBanner.vue";
+import BannerImg from "../Objects/images/banner-min.jpg";
+
+const showPreview = ref(false);
+const curPreviewIdx = ref(0);
+const previewList = ref<string[]>([]);
+</script>
+
+<style lang="scss" scoped>
+@import "./index.scss";
+</style>

BIN
src/views/Exhibitions/Objects/images/banner-min.jpg


+ 30 - 0
src/views/Exhibitions/Objects/index.scss

@@ -0,0 +1,30 @@
+.ex-objects {
+  .page-banner {
+    &::before {
+      content: "";
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      background-color: rgba(0, 0, 0, 0.3);
+      z-index: 1;
+    }
+    :deep(h3) {
+      z-index: 2;
+    }
+  }
+
+  &-main {
+    display: flex;
+    justify-content: space-between;
+    flex-wrap: wrap;
+    padding: 40px 20px;
+
+    img {
+      margin: 20px;
+      width: calc(50% - 40px);
+      border-radius: 10px;
+    }
+  }
+}

+ 40 - 0
src/views/Exhibitions/Objects/index.vue

@@ -0,0 +1,40 @@
+<template>
+  <div class="ex-objects">
+    <PageBanner title="Exhibition Objects" :img="BannerImg" />
+
+    <div class="ex-objects-main">
+      <img
+        src="https://en.capitalmuseum.org.cn/data/Exhibitions/Current/objects2/objects2.jpg"
+        alt=""
+      />
+      <img
+        src="https://en.capitalmuseum.org.cn/data/Exhibitions/Current/objects2/objects2.jpg"
+        alt=""
+      />
+      <img
+        src="https://en.capitalmuseum.org.cn/data/Exhibitions/Current/objects2/objects2.jpg"
+        alt=""
+      />
+    </div>
+  </div>
+
+  <VanImagePreview
+    v-model:show="showPreview"
+    :images="previewList"
+    :start-position="curPreviewIdx"
+  />
+</template>
+
+<script lang="ts" setup>
+import { ref } from "vue";
+import PageBanner from "@/components/PageBanner.vue";
+import BannerImg from "./images/banner-min.jpg";
+
+const showPreview = ref(false);
+const curPreviewIdx = ref(0);
+const previewList = ref<string[]>([]);
+</script>
+
+<style lang="scss" scoped>
+@import "./index.scss";
+</style>

+ 17 - 0
src/views/Exhibitions/Overseas/index.vue

@@ -0,0 +1,17 @@
+<template>
+  <div class="permanent">
+    <div class="permanent-main">
+      <div v-for="key in 10" :key="key" class="permanent-card">
+        <ImgCard />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import ImgCard from "@/components/ImgCard.vue";
+</script>
+
+<style lang="scss" scoped>
+@import "../Permanent/index.scss";
+</style>

+ 17 - 0
src/views/Exhibitions/Past/index.vue

@@ -0,0 +1,17 @@
+<template>
+  <div class="permanent">
+    <div class="permanent-main">
+      <div v-for="key in 10" :key="key" class="permanent-card">
+        <ImgCard />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import ImgCard from "@/components/ImgCard.vue";
+</script>
+
+<style lang="scss" scoped>
+@import "../Permanent/index.scss";
+</style>

+ 14 - 0
src/views/Exhibitions/Permanent/index.scss

@@ -0,0 +1,14 @@
+.permanent {
+  background-color: #f7f6f3;
+
+  &-main {
+    margin: -15px;
+    padding: 20px 40px 40px;
+    display: flex;
+    flex-wrap: wrap;
+  }
+  &-card {
+    padding: 15px;
+    width: 50%;
+  }
+}

+ 17 - 0
src/views/Exhibitions/Permanent/index.vue

@@ -0,0 +1,17 @@
+<template>
+  <div class="permanent">
+    <div class="permanent-main">
+      <div v-for="key in 10" :key="key" class="permanent-card">
+        <ImgCard />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import ImgCard from "@/components/ImgCard.vue";
+</script>
+
+<style lang="scss" scoped>
+@import "./index.scss";
+</style>

BIN
src/views/Exhibitions/images/bannerE.png


+ 39 - 0
src/views/Exhibitions/index.vue

@@ -0,0 +1,39 @@
+<template>
+  <div class="exhibitions">
+    <PageBanner title="Exhibitions" :img="BannerImg" />
+
+    <PageNav v-model="activeTab" @change="handleNav">
+      <van-tab title="Current"></van-tab>
+      <van-tab title="Permanent"></van-tab>
+      <van-tab title="Past"></van-tab>
+      <van-tab title="Overseas"></van-tab>
+    </PageNav>
+
+    <RouterView />
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from "vue";
+import PageBanner from "@/components/PageBanner.vue";
+import PageNav from "@/components/PageNav.vue";
+import BannerImg from "./images/bannerE.png";
+import { useRoute, useRouter } from "vue-router";
+
+const ROUTE_MAP: Record<number, string> = {
+  0: "ExhibitionsCurrent",
+  1: "ExhibitionsPermanent",
+  2: "ExhibitionsPast",
+  3: "ExhibitionsOverseas",
+};
+
+const route = useRoute();
+const router = useRouter();
+const activeTab = ref(
+  Object.values(ROUTE_MAP).findIndex((key) => key === route.name) || 0
+);
+
+const handleNav = (v: number) => {
+  router.push({ name: ROUTE_MAP[v] });
+};
+</script>

+ 74 - 0
src/views/Home/components/Connections.vue

@@ -0,0 +1,74 @@
+<template>
+  <div class="connections">
+    <page-label>Partners & Connections</page-label>
+
+    <ul>
+      <li v-for="(item, idx) in LIST" :key="idx">
+        <a :href="item.link" target="_blank">
+          <img :src="item.img" />
+        </a>
+      </li>
+    </ul>
+
+    <div class="more">See More</div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import Img1 from "../images/link1.jpg";
+import Img2 from "../images/link2.jpg";
+import Img3 from "../images/link3.jpg";
+import Img4 from "../images/link4.jpg";
+import Img5 from "../images/link5.jpg";
+import Img6 from "../images/link6.jpg";
+
+const LIST = [
+  {
+    img: Img1,
+    link: "http://www.edo-tokyo-museum.or.jp/en/",
+  },
+  {
+    img: Img2,
+    link: "https://en.shm.ru/",
+  },
+  {
+    img: Img3,
+    link: "https://www.rom.on.ca/en",
+  },
+  {
+    img: Img4,
+    link: "https://museum.seoul.go.kr/eng/index.do",
+  },
+  {
+    img: Img5,
+    link: "https://museumsvictoria.com.au/",
+  },
+  {
+    img: Img6,
+    link: "https://www.vmfa.museum/",
+  },
+];
+</script>
+
+<style lang="scss" scoped>
+.connections {
+  padding: 80px 0;
+  border-bottom: 1px solid #ccc;
+
+  ul {
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: space-between;
+
+    li {
+      width: 336px;
+      height: 124px;
+      margin-bottom: 20px;
+
+      img {
+        width: 100%;
+      }
+    }
+  }
+}
+</style>

+ 117 - 0
src/views/Home/components/VisitInfo.vue

@@ -0,0 +1,117 @@
+<template>
+  <div class="visit-info">
+    <page-label>Visit Info.</page-label>
+
+    <div v-for="(item, index) in LIST" :key="index" class="visit-info__item">
+      <h3>{{ item.label }}</h3>
+      <p v-for="(p, idx) in item.content" :key="idx">{{ p }}</p>
+    </div>
+
+    <ul>
+      <li v-for="(item, idx) in LIST2" :key="idx">
+        <img :src="item.icon" />
+        <p>{{ item.label }}</p>
+      </li>
+    </ul>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import ICON1 from "../images/v1.png";
+import ICON2 from "../images/v2.png";
+import ICON3 from "../images/v3.png";
+import ICON4 from "../images/v4.png";
+import ICON5 from "../images/v5.png";
+import ICON6 from "../images/v6.png";
+
+const LIST = [
+  {
+    label: "Opening Hours 09:00-17:00",
+    content: [
+      "No admission after 16:00",
+      "Closed on Mondays(Except for holidays)",
+    ],
+  },
+  {
+    label: "Individual visitors:",
+    content: ["+86 (10) 63393339"],
+  },
+  {
+    label: "Group visitors:",
+    content: ["+86 (10) 63370458"],
+  },
+];
+
+const LIST2 = [
+  {
+    icon: ICON1,
+    label: "Hours, Direction & Admission",
+  },
+  {
+    icon: ICON2,
+    label: "Floor Plans",
+  },
+  {
+    icon: ICON3,
+    label: "Audio Guide & Tour",
+  },
+  {
+    icon: ICON4,
+    label: "Accessibility",
+  },
+  {
+    icon: ICON5,
+    label: "Café & Shop",
+  },
+  {
+    icon: ICON6,
+    label: "Visitor Guidelines",
+  },
+];
+</script>
+
+<style lang="scss" scoped>
+.visit-info {
+  padding: 80px 0;
+  border-bottom: 1px solid #ccc;
+
+  &__item {
+    padding: 0 0 48px 120px;
+
+    h3 {
+      margin-bottom: 10px;
+      color: var(--van-primary-color);
+      font-size: 36px;
+    }
+    p {
+      color: var(--van-gray-7);
+      line-height: 32px;
+    }
+  }
+  ul {
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: space-between;
+
+    li {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      margin-bottom: 20px;
+      width: 322px;
+      height: 140px;
+      background: url("../images/frame.png") center / 100% 100%;
+
+      &:first-child p {
+        color: var(--van-primary-color);
+      }
+      p {
+        font-size: 24px;
+        line-height: 24px;
+        color: #c1aa7b;
+      }
+    }
+  }
+}
+</style>

BIN
src/views/Home/images/bg-min.png


BIN
src/views/Home/images/frame.png


BIN
src/views/Home/images/link1.jpg


BIN
src/views/Home/images/link2.jpg


BIN
src/views/Home/images/link3.jpg


BIN
src/views/Home/images/link4.jpg


BIN
src/views/Home/images/link5.jpg


BIN
src/views/Home/images/link6.jpg


BIN
src/views/Home/images/v1.png


BIN
src/views/Home/images/v2.png


BIN
src/views/Home/images/v3.png


BIN
src/views/Home/images/v4.png


BIN
src/views/Home/images/v5.png


BIN
src/views/Home/images/v6.png


+ 14 - 0
src/views/Home/index.scss

@@ -0,0 +1,14 @@
+.home {
+  &-banner {
+    width: 100%;
+
+    img {
+      display: block;
+      width: 100%;
+    }
+  }
+  &-main {
+    padding: 0 30px 80px;
+    background: url("./images/bg-min.png") no-repeat top / 100% 100%;
+  }
+}

+ 27 - 3
src/views/Home/index.vue

@@ -1,7 +1,31 @@
 <template>
-  <div class="home">123</div>
+  <div class="home">
+    <VanSwipe class="home-banner" :autoplay="3000" indicator-color="white">
+      <VanSwipeItem>
+        <img
+          src="https://en.capitalmuseum.org.cn/mobile/img/banner7.42692dcb.png"
+        />
+      </VanSwipeItem>
+      <VanSwipeItem>
+        <img
+          src="https://en.capitalmuseum.org.cn/mobile/img/banner7.42692dcb.png"
+        />
+      </VanSwipeItem>
+    </VanSwipe>
+
+    <div class="home-main">
+      <VisitInfo />
+
+      <Connections />
+    </div>
+  </div>
 </template>
 
-<script setup lang="ts"></script>
+<script setup lang="ts">
+import VisitInfo from "./components/VisitInfo.vue";
+import Connections from "./components/Connections.vue";
+</script>
 
-<style lang="scss" scoped></style>
+<style lang="scss" scoped>
+@import "./index.scss";
+</style>

BIN
src/views/Learn/images/bannerL.png


+ 35 - 0
src/views/Learn/index.vue

@@ -0,0 +1,35 @@
+<template>
+  <div class="learn">
+    <PageBanner title="" :img="BannerImg" />
+
+    <PageNav v-model="activeTab" @change="handleNav">
+      <van-tab title="Students"></van-tab>
+      <van-tab title="Adults"></van-tab>
+      <van-tab title="Families & Children"></van-tab>
+    </PageNav>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from "vue";
+import { useRoute, useRouter } from "vue-router";
+import PageBanner from "@/components/PageBanner.vue";
+import PageNav from "@/components/PageNav.vue";
+import BannerImg from "./images/bannerL.png";
+
+const ROUTE_MAP: Record<number, string> = {
+  0: "Students",
+  1: "Adults",
+  2: "Families",
+};
+
+const route = useRoute();
+const router = useRouter();
+const activeTab = ref(
+  Object.values(ROUTE_MAP).findIndex((key) => key === route.name) || 0
+);
+
+const handleNav = (v: number) => {
+  router.push({ name: "LearnEngage", params: { type: ROUTE_MAP[v] } });
+};
+</script>

+ 3 - 0
src/views/Use/index.vue

@@ -0,0 +1,3 @@
+<template>
+  <div></div>
+</template>

BIN
src/views/Visit/components/Accessibility/images/access1.jpg


BIN
src/views/Visit/components/Accessibility/images/access2.jpg


BIN
src/views/Visit/components/Accessibility/images/access3.jpg


Різницю між файлами не показано, бо вона завелика
+ 47 - 0
src/views/Visit/components/Accessibility/index.vue


+ 58 - 0
src/views/Visit/components/Admission/index.vue

@@ -0,0 +1,58 @@
+<template>
+  <div class="admission">
+    <h3>Hours</h3>
+    <p>Tuesday - Sunday,9:00 - 17:00</p>
+    <p>(No admission after 16:00)</p>
+    <p>Closed on Mondays(Except for holidays)</p>
+    <h3>Direction</h3>
+    <p>16 Fuxingmenwai Street, Xicheng District, Beijing 100045</p>
+    <p class="ttb">Subway</p>
+    <p>500 meters east to Muxidi Station, Line 1</p>
+    <p class="ttb">Bus</p>
+    <p>Baiyun Road Station:</p>
+    <p>By Bus 26, 45, 80, 114, 308, 843, 844, 937 or 特 19</p>
+    <p>Gonghui Dalou Station:</p>
+    <p>By Bus 1 or 52</p>
+    <h3>Admission</h3>
+    <p>Capital Museum offers free admission to visitors.</p>
+    <p>you can book a free ticket in advance.</p>
+    <img src="./map.png" alt="" />
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.admission {
+  padding: 0 0 40px;
+
+  & > h3 {
+    margin: 40px 0 16px;
+    padding: 0 60px;
+    font-size: 44px;
+    font-weight: 700;
+    padding-left: 120px;
+    background: url("@/assets/images/chosen.png") 60px center no-repeat;
+    background-size: 44px 36px;
+  }
+  & > p {
+    padding: 0 60px;
+    color: #6a6a6a;
+    font-size: 28px;
+    line-height: 36px;
+  }
+  .ttb {
+    margin-top: 20px;
+    background: url("@/assets/images/chosen.png") 60px center no-repeat;
+    background-size: 44px 36px;
+    padding-left: 120px;
+    font-size: 44px;
+    font-weight: 700;
+    color: black;
+    line-height: 72px;
+  }
+  & > img {
+    width: 100%;
+    height: 600px;
+    margin-top: 40px;
+  }
+}
+</style>

BIN
src/views/Visit/components/Admission/map.png


BIN
src/views/Visit/components/Cafe/images/cafe1.jpg


+ 0 - 0
src/views/Visit/components/Cafe/images/cafe2.jpg


Деякі файли не було показано, через те що забагато файлів було змінено