Просмотр исходного кода

feat: calendar & direction page

chenlei 1 год назад
Родитель
Сommit
55ae3e86d5

+ 2 - 0
.npmrc

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

+ 6 - 0
components.d.ts

@@ -8,10 +8,16 @@ export {}
 declare module 'vue' {
   export interface GlobalComponents {
     Accessibility: typeof import('./src/components/Accessibility/index.vue')['default']
+    ElButton: typeof import('element-plus/es')['ElButton']
+    ElCollapse: typeof import('element-plus/es')['ElCollapse']
+    ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
+    ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
+    ElImage: typeof import('element-plus/es')['ElImage']
     Layout: typeof import('./src/components/Layout/index.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
     SvgIcon: typeof import('./src/components/SvgIcon/index.vue')['default']
+    VanIcon: typeof import('vant/es')['Icon']
     VanSwipe: typeof import('vant/es')['Swipe']
     VanSwipeItem: typeof import('vant/es')['SwipeItem']
     VanSwitch: typeof import('vant/es')['Switch']

+ 6 - 1
package.json

@@ -11,6 +11,10 @@
     "type-check": "vue-tsc --build --force"
   },
   "dependencies": {
+    "@dage/utils": "^1.0.2",
+    "@vue/shared": "^3.4.27",
+    "element-plus": "^2.7.3",
+    "lodash-unified": "^1.0.3",
     "pinia": "^2.1.7",
     "vant": "^4.9.0",
     "vue": "^3.4.21",
@@ -23,9 +27,10 @@
     "@vitejs/plugin-vue-jsx": "^3.1.0",
     "@vue/tsconfig": "^0.5.1",
     "install": "^0.13.0",
-    "less": "^4.2.0",
     "npm-run-all2": "^6.1.2",
+    "sass": "^1.77.4",
     "typescript": "~5.4.0",
+    "unplugin-auto-import": "^0.17.6",
     "unplugin-vue-components": "^0.27.0",
     "vite": "^5.2.8",
     "vite-plugin-svg-icons": "^2.0.1",

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


+ 7 - 1
src/App.vue

@@ -25,7 +25,7 @@ onBeforeMount(() => {
   <RouterView />
 </template>
 
-<style lang="less">
+<style lang="scss">
 body.aria-active {
   margin-top: 105px;
 }
@@ -33,4 +33,10 @@ body.aria-active {
 .van-swipe {
   cursor: default !important;
 }
+
+.container {
+  margin: 0 auto;
+  width: 100%;
+  max-width: 1300px;
+}
 </style>

+ 22 - 20
src/assets/css/ariaGlobalStyle.less

@@ -1,4 +1,4 @@
-@import './common.less';
+@import './commonlang="scss"';
 
 code {
   display: none;
@@ -18,8 +18,8 @@ code {
         outline: 2px solid red !important;
         outline-offset: -2px;
         &.aria-outline-out {
-        outline-offset: 0;
-      }
+          outline-offset: 0;
+        }
       }
     }
   }
@@ -41,8 +41,8 @@ code {
         outline: 2px solid black;
         outline-offset: -2px;
         &.aria-outline-out {
-        outline-offset: 0;
-      }
+          outline-offset: 0;
+        }
       }
       &::placeholder {
         color: black !important;
@@ -75,11 +75,11 @@ code {
       background: blue !important;
       color: yellow !important;
       &:focus {
-      outline: 2px solid yellow;
-      outline-offset: -2px;
-      &.aria-outline-out {
-        outline-offset: 0;
-      }
+        outline: 2px solid yellow;
+        outline-offset: -2px;
+        &.aria-outline-out {
+          outline-offset: 0;
+        }
       }
       &::placeholder {
         color: yellow !important;
@@ -112,11 +112,11 @@ code {
       background: yellow !important;
       color: black !important;
       &:focus {
-      outline: 2px solid black;
-      outline-offset: -2px;
-      &.aria-outline-out {
-        outline-offset: 0;
-      }
+        outline: 2px solid black;
+        outline-offset: -2px;
+        &.aria-outline-out {
+          outline-offset: 0;
+        }
       }
       &::placeholder {
         color: black !important;
@@ -152,8 +152,8 @@ code {
         outline: 2px solid yellow;
         outline-offset: -2px;
         &.aria-outline-out {
-        outline-offset: 0;
-      }
+          outline-offset: 0;
+        }
       }
       &::placeholder {
         color: yellow !important;
@@ -186,9 +186,11 @@ html.aria-active {
 }
 
 // 对于有data-aria-xxx-area attribute的元素,鼠标点击(或者点击其不可focus的子元素)时不应该导致focus到这个元素上,换言之,这种元素只应该能通过tab键focus。
-*[data-aria-navigation-area], *[data-aria-viewport-area], *[data-aria-interaction-area] {
+*[data-aria-navigation-area],
+*[data-aria-viewport-area],
+*[data-aria-interaction-area] {
   pointer-events: none;
-  >* {
+  > * {
     pointer-events: initial;
   }
-}
+}

+ 0 - 2
src/assets/css/common.less

@@ -1,2 +0,0 @@
-@accessibility-menu-height: 105px;
-@magnify-area-height: 219px;

+ 2 - 0
src/assets/css/common.scss

@@ -0,0 +1,2 @@
+$accessibility-menu-height: 105px;
+$magnify-area-height: 219px;

+ 7 - 0
src/assets/css/element.scss

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

+ 8 - 8
src/components/Accessibility/index.vue

@@ -1204,8 +1204,8 @@ export default {
 };
 </script>
 
-<style lang="less" scoped>
-@import "/src/assets/css/common.less";
+<style lang="scss" scoped>
+@import "@/assets/css/common.scss";
 
 li {
   list-style: none;
@@ -1233,7 +1233,7 @@ a {
   font-family: SourceHanSansCN-Bold-GBpc-EUC-H;
   line-height: 15px;
   background-color: #36584c;
-  height: @accessibility-menu-height;
+  height: $accessibility-menu-height;
   position: fixed;
   top: 0;
   width: 100%;
@@ -1258,15 +1258,15 @@ a {
   }
   .mignify-area {
     position: fixed;
-    height: @magnify-area-height;
+    height: $magnify-area-height;
     width: 100%;
     bottom: 0;
     background: #fff;
     z-index: 1;
     display: flex;
     .text-wrapper {
-      height: @magnify-area-height;
-      line-height: @magnify-area-height;
+      height: $magnify-area-height;
+      line-height: $magnify-area-height;
       width: 1px;
       flex: 1 0 auto;
       overflow: auto;
@@ -1300,7 +1300,7 @@ a {
         color: white;
         height: 100%;
         padding-top: 10px;
-        width: @accessibility-menu-height;
+        width: $accessibility-menu-height;
         display: flex;
         flex-direction: column;
         align-items: center;
@@ -1377,7 +1377,7 @@ a {
         color: white;
         height: 100%;
         padding-top: 10px;
-        width: @accessibility-menu-height;
+        width: $accessibility-menu-height;
         display: flex;
         flex-direction: column;
         align-items: center;

+ 1 - 1
src/components/Layout/data.ts

@@ -1,5 +1,5 @@
 export const topData = [
-  { id: 1, name: "Home", url: "Home" },
+  { id: 1, name: "Home", url: "home" },
   {
     id: 2,
     name: "Visit",

+ 34 - 6
src/components/Layout/index.vue

@@ -35,7 +35,13 @@
                 :key="item.id"
                 aria-label="Link"
                 :aria-description="item.name"
-                :class="{ active: $route.name === item.name }"
+                :class="{
+                  active:
+                    $route.path.toLowerCase().indexOf(item.name.toLowerCase()) >
+                    -1,
+                }"
+                @click="skipOne(item.url)"
+                @keydown.enter.passive="skipOne(item.url)"
               >
                 <span>{{ item.name }}</span>
                 <ul class="main_nav_sub">
@@ -112,8 +118,8 @@
                 <input
                   type="text"
                   placeholder="search"
-                  v-model="searchTxt2"
-                  :aria-description="searchTxt2 || 'search'"
+                  v-model="searchTxt"
+                  :aria-description="searchTxt || 'search'"
                 />
               </div>
             </div>
@@ -173,7 +179,7 @@
 
 <script setup lang="ts">
 import { ref, watch } from "vue";
-import { useRouter } from "vue-router";
+import { useRoute, useRouter } from "vue-router";
 import Accessibility from "@/components/Accessibility/index.vue";
 import { useTheme, THEME_COLOR } from "@/components/Accessibility/hooks";
 import { topData, footerData } from "./data";
@@ -189,12 +195,14 @@ const THEME_MAP: Record<THEME_COLOR, string> = {
 };
 
 const themeIdx = useTheme();
+const route = useRoute();
 const router = useRouter();
 /** 控制二级菜单的高亮 */
-const menaInd = ref(0);
+const menaInd = ref<string | null>(null);
 /** 是否开启爱心模式 */
 const loveSwitch = ref(false);
 const accessibility = ref();
+const searchTxt = ref("");
 const searchTxt2 = ref("");
 
 watch(loveSwitch, (v) => {
@@ -205,6 +213,15 @@ watch(loveSwitch, (v) => {
   }
 });
 
+watch(route, () => {
+  menaInd.value = route.path as string;
+  searchTxt.value = "";
+});
+
+// 第一级的跳转
+const skipOne = (url: string) => {
+  router.push("/Layout/" + url);
+};
 // 第二级的跳转
 const skipTow = (url: string | number, about?: boolean) => {
   // 如果是about页面
@@ -225,31 +242,41 @@ const handleSearch = () => {
 };
 </script>
 
-<style lang="less">
+<style lang="scss">
 .layout {
   --van-primary-color: #ca000a;
   --topnav-bg-color: rgba(0, 0, 0, 0.8);
   --primary-hover-bg: rgba(204, 0, 3, 0.8);
+  --black-text-color: black;
+  --white-bg: white;
 
   &.white {
     --topnav-bg-color: white;
     --van-white: black;
     --primary-hover-bg: white;
+    --black-text-color: black;
+    --white-bg: white;
   }
   &.blue {
     --topnav-bg-color: #00f;
     --van-white: #ff0;
     --primary-hover-bg: #00f;
+    --black-text-color: #ff0;
+    --white-bg: #00f;
   }
   &.yellow {
     --topnav-bg-color: #ff0;
     --van-white: black;
     --primary-hover-bg: #ff0;
+    --black-text-color: black;
+    --white-bg: #ff0;
   }
   &.black {
     --topnav-bg-color: black;
     --van-white: #ff0;
     --primary-hover-bg: black;
+    --black-text-color: #ff0;
+    --white-bg: black;
   }
 }
 
@@ -264,6 +291,7 @@ const handleSearch = () => {
   padding: 0 40px 0 5px;
   height: inherit;
   background: var(--topnav-bg-color);
+  z-index: 1;
 
   &-wrap {
     position: relative;

+ 30 - 2
src/router/index.ts

@@ -8,13 +8,41 @@ const router = createRouter({
       path: "/",
       name: "layout",
       component: Layout,
-      redirect: "/home",
+      redirect: "/Layout/home",
       children: [
         {
-          path: "/home",
+          path: "/Layout/home",
           name: "Home",
           component: () => import("../views/Home/index.vue"),
         },
+        {
+          path: "/Layout/visit",
+          name: "Visit",
+          redirect: "/Layout/visit/8",
+          component: () => import("../views/Visit/index.vue"),
+          children: [
+            {
+              path: "/Layout/visit/8",
+              name: "Calendar",
+              component: () => import("../views/Visit/Calendar/index.vue"),
+            },
+            {
+              path: "/Layout/visit/1",
+              name: "Direction",
+              component: () => import("../views/Visit/Direction/index.vue"),
+            },
+            {
+              path: "/Layout/visit/2",
+              name: "Reservation",
+              component: () => import("../views/Visit/Reservation/index.vue"),
+            },
+            {
+              path: "/Layout/visit/3",
+              name: "Plans",
+              component: () => import("../views/Visit/Plans/index.vue"),
+            },
+          ],
+        },
       ],
     },
   ],

+ 344 - 1
src/views/Home/index.vue

@@ -23,18 +23,173 @@
         </ul>
       </template>
     </van-swipe>
+
+    <div
+      class="bottomNav"
+      data-aria-viewport-area
+      tabindex="0"
+      aria-description="You've reached the pop-up window section; this section contains four URLs; please use the tab key to go through the content."
+    >
+      <div
+        class="t1"
+        @click="$router.push('/Layout/Visit/2')"
+        tabindex="0"
+        aria-label="Link"
+        aria-description="Reservation"
+      >
+        <div class="title">
+          <span class="aria-theme-independent"> Reservation </span>
+        </div>
+        <div class="info">
+          <router-link
+            class="p"
+            to="/Layout/Visit/2"
+            replace
+            tabindex="0"
+            aria-label="Image link"
+            aria-description="Reservation"
+          >
+            <img src="../../assets/images/bott1.jpg" alt="Reservation" />
+          </router-link>
+          <div class="d">
+            <p class="n1 aria-theme-independent" tabindex="0">
+              Online Reservation
+            </p>
+            <p class="n2 aria-theme-independent" tabindex="0">
+              Telephone Reservation
+            </p>
+            <p class="n3" tabindex="0">
+              Individual <span>+86 (10) 63393339</span>
+            </p>
+            <p class="n3" tabindex="0">Group <span>+86 (10) 63370458</span></p>
+          </div>
+        </div>
+      </div>
+      <div
+        class="t2"
+        @click="$router.push('/Layout/Visit/1')"
+        tabindex="0"
+        aria-label="Link"
+        aria-description="Visit Info"
+      >
+        <div class="title">
+          <span class="aria-theme-independent">Visit Info</span>
+        </div>
+        <div class="info">
+          <router-link
+            class="p"
+            to="/Layout/Visit/1"
+            replace
+            tabindex="0"
+            aria-label="Image link"
+            aria-description="Hours, Direction & Admission"
+          >
+            <img
+              src="../../assets/images/bott2.jpg"
+              alt="Hours, Direction & Admission"
+            />
+          </router-link>
+          <div class="d">
+            <p class="n4 aria-theme-independent" tabindex="0">
+              Opening Hours 09:00-17:00
+            </p>
+            <p class="n5" tabindex="0">No admission after 16:00</p>
+            <p class="n5" tabindex="0">Closed on Monday</p>
+            <p class="n6 aria-theme-independent" tabindex="0">Phone</p>
+            <p tabindex="0">+86 (10) 63370491</p>
+          </div>
+        </div>
+      </div>
+      <div
+        class="t3"
+        @click="botskip()"
+        tabindex="0"
+        aria-label="Link"
+        aria-description="Partners & Connections"
+      >
+        <div class="title">
+          <span class="aria-theme-independent">Partners & Connections</span>
+        </div>
+        <div class="info">
+          <router-link
+            class="p"
+            to="/Layout/About"
+            replace
+            tabindex="0"
+            aria-label="Image link"
+            aria-description="Partners & Connections"
+          >
+            <img
+              src="../../assets/images/bott3.jpg"
+              alt="Partners & Connections"
+            />
+          </router-link>
+          <div class="d">
+            <p class="n7 aria-theme-independent" tabindex="0">
+              Partners & Connections
+            </p>
+          </div>
+        </div>
+      </div>
+      <div
+        class="t4"
+        @click="$router.push('/Layout/Events')"
+        tabindex="0"
+        aria-label="Link"
+        aria-description="Events"
+      >
+        <div class="title">
+          <span class="aria-theme-independent">Events</span>
+        </div>
+        <div class="info">
+          <router-link
+            class="p"
+            to="/Layout/Events"
+            replace
+            tabindex="0"
+            aria-label="Image link"
+            aria-description="Events"
+          >
+            <img src="../../assets/images/bott4.jpg" alt="Events" />
+          </router-link>
+          <div class="d">
+            <p class="n8 aria-theme-independent" tabindex="0">Events</p>
+          </div>
+        </div>
+      </div>
+    </div>
   </div>
 </template>
 
 <script setup lang="ts">
 import type { SwipeInstance } from "vant";
 import { ref } from "vue";
+import { useRouter } from "vue-router";
 
+const router = useRouter();
 const swipeHeight = window.innerHeight - 105;
 const swipeRef = ref<SwipeInstance>();
+
+const botskip = () => {
+  router.push("/Layout/About").catch(() => {});
+  setTimeout(() => {
+    window.scrollTo({ top: 1307, behavior: "smooth" });
+  }, 100);
+};
 </script>
 
-<style lang="less" scoped>
+<style lang="scss" scoped>
+.home {
+  position: relative;
+
+  img {
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+    cursor: pointer;
+  }
+}
+
 .home-indicator {
   position: absolute;
   left: 50%;
@@ -58,4 +213,192 @@ const swipeRef = ref<SwipeInstance>();
     }
   }
 }
+
+.bottomNav {
+  padding: 0 100px;
+  background-color: var(--topnav-bg-color);
+  width: 100%;
+  height: 55px;
+  position: absolute;
+  left: 0;
+  bottom: 0;
+  z-index: 4;
+  display: flex;
+  > div {
+    display: block;
+    cursor: pointer;
+    width: 25%;
+    height: 58px;
+    position: relative;
+    transform: translateY(-3px);
+    .title {
+      font-size: 14px;
+      color: var(--van-white);
+      line-height: 55px;
+      text-align: center;
+      width: 100%;
+      span {
+        padding-left: 30px;
+      }
+    }
+    .info {
+      transition: all 0.3s;
+      height: 0;
+      overflow: hidden;
+      width: 100%;
+      position: absolute;
+      left: 0;
+      bottom: 0;
+      z-index: 99;
+      .d {
+        font-size: 14px;
+        color: #ccc;
+        background: #000;
+        padding: 20px;
+      }
+      .p {
+        display: block;
+        img {
+          vertical-align: bottom;
+        }
+      }
+    }
+  }
+  .t1:hover .info {
+    height: 320px;
+  }
+  .t2:hover .info {
+    height: 320px;
+  }
+  .t3:hover .info {
+    height: 270px;
+  }
+  .t4:hover .info {
+    height: 270px;
+  }
+  .t1:focus-within .info {
+    height: 320px;
+  }
+  .t2:focus-within .info {
+    height: 320px;
+  }
+  .t3:focus-within .info {
+    height: 270px;
+  }
+  .t4:focus-within .info {
+    height: 270px;
+  }
+  .t1 {
+    .title {
+      span {
+        background: url("../../assets/images/bottom_ico1.png") no-repeat left
+          center;
+      }
+    }
+    .p {
+      border-bottom: 3px solid #1116e5;
+    }
+    border-top: 3px solid #1116e5;
+  }
+  .t2 {
+    .title {
+      span {
+        background: url("../../assets/images/bottom_ico2.png") no-repeat left
+          center;
+      }
+    }
+    .p {
+      border-bottom: 3px solid #a211e5;
+    }
+    border-top: 3px solid #a211e5;
+  }
+  .t3 {
+    .title {
+      span {
+        background: url("../../assets/images/bottom_ico3.png") no-repeat left
+          center;
+      }
+    }
+    .p {
+      border-bottom: 3px solid #229382;
+    }
+    .d {
+      padding-top: 40px !important;
+      padding-bottom: 40px !important;
+    }
+    border-top: 3px solid #229382;
+  }
+  .t4 {
+    .title {
+      span {
+        background: url("../../assets/images/bottom_ico4.png") no-repeat left
+          center;
+      }
+    }
+    .p {
+      border-bottom: 3px solid #7a9322;
+    }
+    .d {
+      padding-top: 40px !important;
+      padding-bottom: 40px !important;
+    }
+
+    border-top: 3px solid #7a9322;
+  }
+  p {
+    margin-bottom: 5px;
+  }
+  .n1 {
+    line-height: 30px;
+    background: url(../../assets/images/bottom_sub_ico1.png) no-repeat left
+      center;
+    text-indent: 25px;
+    & > a {
+      color: #ff0000;
+      text-decoration: underline;
+      margin-left: 20px;
+    }
+  }
+  .n2 {
+    line-height: 30px;
+    background: url(../../assets/images/bottom_sub_ico2.png) no-repeat left
+      center;
+    text-indent: 25px;
+    margin-bottom: 10px;
+  }
+  .n3 {
+    width: 200px;
+    margin-bottom: 5px;
+    & > span {
+      float: right;
+    }
+  }
+  .n4 {
+    line-height: 30px;
+    background: url(../../assets/images/bottom_sub_ico3.png) no-repeat left
+      center;
+    text-indent: 25px;
+  }
+  .n5 {
+    font-size: 12px;
+    color: #828282;
+    margin-bottom: 0;
+  }
+  .n6 {
+    line-height: 30px;
+    background: url(../../assets/images/bottom_sub_ico2.png) no-repeat left
+      center;
+    text-indent: 25px;
+  }
+  .n7 {
+    font-size: 18px;
+    background: url(../../assets/images/bottom_ico3.png) no-repeat left center;
+    padding-left: 30px;
+  }
+  .n8 {
+    font-size: 18px;
+    background: url(../../assets/images/bottom_ico4.png) no-repeat left center;
+    padding-left: 30px;
+  }
+}
 </style>

+ 216 - 0
src/views/Visit/Calendar/components/Calendar.vue

@@ -0,0 +1,216 @@
+<template>
+  <div class="panel-rg">
+    <div class="panel-rg__header">
+      <van-icon name="arrow-left" @click="selectDate('prev-month')" />
+      <p>
+        <span>{{ months[month] }}</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 slot="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 { months } from "./DateTable/utils";
+
+export default {
+  components: {
+    DateTable,
+  },
+  props: {
+    modelValue: [Date, String, Number],
+  },
+  provide() {
+    return {
+      elCalendar: this,
+    };
+  },
+  data() {
+    return {
+      months,
+      selectedDay: null,
+      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();
+    },
+  },
+  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 {
+  flex: 0 0 527px;
+  padding: 27px 30px;
+  box-sizing: border-box;
+  color: var(--black-text-color);
+
+  &__header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 0 45px 20px;
+
+    i {
+      cursor: pointer;
+      font-size: 20px;
+      color: var(--van-primary-color);
+    }
+    p {
+      font-size: 28px;
+      font-weight: bold;
+
+      span {
+        color: var(--van-primary-color);
+      }
+    }
+  }
+  :deep(.panel-rg__date) {
+    margin: 0 auto 12px;
+    width: calc(100% - 30px);
+    min-height: 370px;
+
+    th {
+      color: var(--black-text-color);
+    }
+    td {
+      border: 0 !important;
+    }
+    .el-calendar-day {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      height: 55px;
+      color: var(--black-text-color);
+
+      &: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;
+    gap: 70px;
+    justify-content: center;
+    padding-top: 20px;
+    border-top: 1px dashed rgba(32, 32, 32, 0.5);
+
+    p {
+      position: relative;
+
+      &::before {
+        content: "";
+        position: absolute;
+        top: 50%;
+        left: -20px;
+        width: 10px;
+        height: 10px;
+        border-radius: 10px;
+        transform: translateY(-50%);
+        background-color: #95d2ff;
+      }
+      &:first-child::before {
+        background-color: #a9da8c;
+      }
+      &:last-child::before {
+        background-color: #ffce7e;
+      }
+    }
+  }
+}
+</style>

+ 254 - 0
src/views/Visit/Calendar/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 { ref, 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: 12px 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: 8px;
+    height: 85px;
+    cursor: pointer;
+  }
+}
+</style>

+ 333 - 0
src/views/Visit/Calendar/components/DateTable/utils.js

@@ -0,0 +1,333 @@
+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",
+];
+
+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()
+  );
+};

+ 228 - 0
src/views/Visit/Calendar/index.scss

@@ -0,0 +1,228 @@
+.calendar {
+  margin-bottom: 30px;
+}
+
+.dots {
+  display: flex;
+  gap: 8px;
+  margin-top: 10px;
+
+  .dot {
+    width: 5px;
+    height: 5px;
+    border-radius: 5px;
+    background-color: #a9da8c;
+
+    &.events {
+      background-color: #95d2ff;
+    }
+    &.learn {
+      background-color: #ffce7e;
+    }
+  }
+}
+
+.panel {
+  display: flex;
+  gap: 19px;
+  color: #202020;
+
+  > div {
+    background: var(--white-bg);
+    box-shadow: 0px 10px 32px 0px rgba(146, 146, 146, 0.25);
+    border-radius: 10px;
+    overflow: hidden;
+  }
+  &-lf {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+
+    &-swipe {
+      height: 440px;
+
+      &-indicator {
+        position: absolute;
+        right: 0;
+        top: 50%;
+        transform: translateY(-50%);
+        z-index: 1;
+
+        li {
+          position: relative;
+          padding: 8px 15px;
+          cursor: pointer;
+
+          &.is-active {
+            &::before {
+              background-color: var(--van-primary-color);
+            }
+            &::after {
+              content: "";
+              position: absolute;
+              top: 50%;
+              left: 50%;
+              width: 17px;
+              height: 17px;
+              border-radius: 17px;
+              border: 2px solid var(--van-primary-color);
+              transform: translate(-50%, -50%);
+            }
+          }
+          &::before {
+            content: "";
+            position: relative;
+            display: block;
+            width: 10px;
+            height: 10px;
+            opacity: 0.5;
+            border-radius: 10px;
+            background-color: #918784;
+            z-index: 1;
+          }
+        }
+      }
+    }
+    &__footer {
+      flex: 1;
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      padding: 0 22px;
+      font-size: 19px;
+      color: var(--black-text-color);
+
+      div:first-child {
+        display: flex;
+        align-items: center;
+        font-weight: bold;
+
+        p {
+          flex: 1;
+          width: 0;
+          line-height: 24px;
+        }
+        span {
+          margin-left: 16px;
+          padding: 5px;
+          font-size: 12px;
+          color: #d2b986;
+          background: rgba(210, 185, 134, 0.3);
+          border-radius: 4px;
+          white-space: nowrap;
+          border: 1px solid #d2b986;
+        }
+      }
+      p:last-child {
+        margin-top: 4px;
+      }
+    }
+  }
+}
+.main {
+  margin-top: 20px;
+  padding: 22px 21px;
+  color: var(--black-text-color);
+  background: var(--white-bg);
+  border-radius: 10px;
+  box-shadow: 0px 8px 25px 0px rgba(146, 146, 146, 0.25);
+
+  :deep(.el-date-editor) {
+    width: 233px;
+    height: 45px;
+
+    .el-input__inner {
+      height: 45px;
+      line-height: 45px;
+      border-radius: 4px;
+    }
+  }
+  .el-button {
+    padding: 0 35px;
+    height: 45px;
+  }
+  &__header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 0 22px 24px;
+    border-bottom: 1px dashed rgba(32, 32, 32, 0.5);
+  }
+  &__list {
+    .visit8-card {
+      display: flex;
+      padding: 28px 23px;
+      height: 320px;
+      box-sizing: border-box;
+      border-bottom: 1px dashed rgba(32, 32, 32, 0.5);
+      cursor: pointer;
+
+      &__left {
+        flex: 0 0 185px;
+        display: flex;
+        flex-direction: column;
+        justify-content: space-between;
+        font-size: 37px;
+        line-height: 48px;
+
+        img {
+          width: 50px;
+          height: 50px;
+        }
+      }
+      &__center {
+        flex: 1;
+        display: flex;
+        flex-direction: column;
+        justify-content: space-between;
+        padding: 0 26px 0 69px;
+
+        > p {
+          font-size: 37px;
+          font-weight: bold;
+          line-height: 48px;
+        }
+        &__inner {
+          line-height: 18px;
+        }
+      }
+      &__right {
+        flex: 0 0 420px;
+        height: 268px;
+      }
+      &__tag {
+        display: inline-block;
+        padding: 8px 15px;
+        color: #d2b986;
+        background: rgba(210, 185, 134, 0.3);
+        border-radius: 4px;
+        border: 1px solid #d2b986;
+      }
+    }
+  }
+}
+.page {
+  display: flex;
+  justify-content: center;
+  padding-top: 30px;
+  & > span {
+    width: 30px;
+    height: 30px;
+    text-align: center;
+    line-height: 30px;
+    margin-right: 15px;
+    border-radius: 50%;
+    cursor: pointer;
+  }
+  .active {
+    background-color: var(--van-primary-color);
+    color: var(--white-bg);
+    pointer-events: none;
+  }
+}
+.empty {
+  text-align: center;
+  font-size: 20px;
+  padding-top: 30px;
+  color: #666666;
+}

Разница между файлами не показана из-за своего большого размера
+ 238 - 0
src/views/Visit/Calendar/index.vue


+ 32 - 0
src/views/Visit/Direction/index.vue

@@ -0,0 +1,32 @@
+<template>
+  <div
+    style="margin-bottom: 20px; text-align: center"
+    data-aria-viewport-area
+    aria-description="You have reached the content area of the Hours, Directions, and Admission page. This area contains one image. To browse the content, please use the tab key."
+  >
+    <img
+      src="@/assets/images/Visit/Visit1.jpg"
+      style="max-width: 1200px; width: 100%; height: 530px"
+      aria-label="Image"
+      :aria-description="`
+Hours
+Tuesday - Sunday, 9:00 - 17:00 (No admission after 16:00)
+Closed on Mondays (Except for holidays)\n
+
+Direction
+16 Fuxingmenwai Street, Xicheng District, Beijing 100045
+Subway
+500 meters east to Muxidi Station, Line 1
+Bus
+Baiyun Road Station:
+By Bus 26, 45, 80,114, 308, 843, 844r 937 or 特 19
+Gonghui Dalou Station:
+By Bus 1 or 52
+
+Admission
+Capital Museum offers free admission to visitors.
+You can book a free ticket in advance.
+      `"
+    />
+  </div>
+</template>

+ 3 - 0
src/views/Visit/Plans/index.vue

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

+ 54 - 0
src/views/Visit/Reservation/components/Information.vue

@@ -0,0 +1,54 @@
+<template>
+  <div class="in-sidebar in-sidebar-three">
+    <p><br /></p>
+    <p tabindex="0">
+      <span style="font-family: arial, helvetica, sans-serif; font-size: 14px">
+        Ticket-reservation is subject to change when there is a large museum
+        event or a special opening ceremony for a new exhibition. The Capital
+        Museum will make a public announcement in advance. Please visit the
+        official website or inquire by telephone for detailed information.
+      </span>
+    </p>
+    <p><br /></p>
+    <p>
+      <span
+        tabindex="0"
+        style="font-family: arial, helvetica, sans-serif; font-size: 14px"
+      >
+        Official website:
+      </span>
+      <a
+        tabindex="0"
+        style="
+          font-size: 14px;
+          font-family: arial, helvetica, sans-serif;
+          color: rgb(255, 0, 0);
+        "
+      >
+        {{ origin }}
+      </a>
+      <span
+        tabindex="0"
+        style="font-family: arial, helvetica, sans-serif; font-size: 14px"
+      >
+        &nbsp; &nbsp; &nbsp; &nbsp;Phone:
+      </span>
+      <span
+        tabindex="0"
+        style="
+          font-size: 14px;
+          font-family: arial, helvetica, sans-serif;
+          color: rgb(255, 0, 0);
+        "
+      >
+        +86 (10) 63370491
+      </span>
+    </p>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from "vue";
+
+const origin = ref(location.origin);
+</script>

+ 277 - 0
src/views/Visit/Reservation/components/Reservations.vue

@@ -0,0 +1,277 @@
+<template>
+  <div class="in-sidebar in-sidebar-one">
+    <p style="text-align: center"><br /></p>
+    <p>
+      <img
+        src="@/assets/images/Visit/pp1.jpg"
+        alt=""
+        tabindex="0"
+        aria-description="Ways of Reservation"
+      />
+    </p>
+    <p><br /></p>
+    <p>
+      <span tabindex="0"><strong>Ways of Reservation</strong></span>
+    </p>
+    <p><br /></p>
+    <p
+      tabindex="0"
+      :aria-description="`There are 3,600 daily personal booking places, available through website (3000 places) and telephone (600 places). The website ${origin} offers 24-hour service, and the telephone +86 (10) 63393339 service is available from 09:00 to 17:00. One person is only allowed to book one ticket.`"
+    >
+      <span style="font-family: arial, helvetica, sans-serif; font-size: 14px">
+        There are 3,600 daily personal booking places, available through website
+        (3000 places) and telephone (600 places). The website
+      </span>
+      <a
+        style="
+          font-family: arial, helvetica, sans-serif;
+          font-size: 14px;
+          text-decoration: none;
+          color: rgb(255, 0, 0);
+        "
+        :href="origin"
+        tabindex="0"
+      >
+        {{ origin }}
+      </a>
+      <span style="font-family: arial, helvetica, sans-serif; font-size: 14px">
+        offers&nbsp;24-hour&nbsp;
+      </span>
+      <span style="font-family: arial, helvetica, sans-serif; font-size: 14px">
+        service, and the&nbsp;telephone
+      </span>
+      <span
+        style="
+          font-family: arial, helvetica, sans-serif;
+          font-size: 14px;
+          color: rgb(255, 0, 0);
+        "
+      >
+        +86 (10) 63393339
+      </span>
+      <span style="font-family: arial, helvetica, sans-serif; font-size: 14px">
+        service is available from 09:00 to 17:00. One person is only allowed to
+        book one ticket.
+      </span>
+    </p>
+    <p><br /></p>
+    <p tabindex="0">
+      <span style="font-family: arial, helvetica, sans-serif; font-size: 14px"
+        >For group reservation, we offer 400 tickets a day. Please call<span
+          style="
+            font-size: 14px;
+            font-family: arial, helvetica, sans-serif;
+            color: rgb(255, 0, 0);
+          "
+          >+86 (10) 63370458&nbsp;&nbsp;</span
+        >between 09:00 to 17:00. Identity information of the group leader is
+        required.</span
+      >
+    </p>
+    <p><br /></p>
+    <p tabindex="0">
+      <span style="font-family: arial, helvetica, sans-serif; font-size: 14px"
+        >Reservation should be made at least one day in advance, and at most
+        seven days in advance.</span
+      >
+    </p>
+    <p><br /></p>
+    <table data-sort="sortDisabled">
+      <tbody>
+        <tr class="firstRow">
+          <td valign="top" colspan="2" rowspan="1">
+            <p style="text-align: center; height: 1px">
+              <img
+                src="@/assets/images/Visit/pp4.jpg"
+                style="max-width: 100%"
+              />
+            </p>
+          </td>
+        </tr>
+        <tr>
+          <td width="420" valign="top">
+            <p style="text-align: center">
+              <img
+                src="@/assets/images/Visit/pp2.jpg"
+                style="max-width: 100%"
+                tabindex="0"
+                aria-description="Way to Get Ticket"
+              />
+            </p>
+          </td>
+          <td width="420" valign="top">
+            <p>
+              <span style="font-family: arial, helvetica, sans-serif"
+                ><strong>&nbsp; &nbsp;&nbsp;</strong></span
+              >
+            </p>
+            <p tabindex="0">
+              <strong style="font-family: arial, helvetica, sans-serif"
+                ><span style="font-size: 18px">Way to Get Ticket</span></strong
+              >
+            </p>
+            <p><br /></p>
+            <p tabindex="0">
+              <span
+                style="
+                  font-size: 14px;
+                  font-family: arial, helvetica, sans-serif;
+                "
+              >
+                For personal visitors, please obtain the ticket at the service
+                center at the north door by showing your booking number and the
+                ID card used when the booking was made.
+              </span>
+            </p>
+            <p><br /></p>
+            <p tabindex="0">
+              <span
+                style="
+                  font-size: 14px;
+                  font-family: arial, helvetica, sans-serif;
+                "
+              >
+                For group visitors, the leader can obtain the ticket at the east
+                door of the ground floor with valid documents and introductory
+                letters.
+              </span>
+            </p>
+            <p><br /></p>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+    <br />
+    <table data-sort="sortDisabled">
+      <tbody>
+        <tr class="firstRow">
+          <td valign="top" colspan="2" rowspan="1">
+            <p style="text-align: center; height: 1px">
+              <img
+                src="@/assets/images/Visit/pp4.jpg"
+                style="max-width: 100%"
+              />
+            </p>
+          </td>
+        </tr>
+        <tr>
+          <td width="420" valign="top">
+            <p>
+              <strong
+                style="
+                  font-family: arial, helvetica, sans-serif;
+                  font-size: 18px;
+                "
+                ><br
+              /></strong>
+            </p>
+            <p tabindex="0">
+              <strong
+                style="
+                  font-family: arial, helvetica, sans-serif;
+                  font-size: 18px;
+                "
+                >&nbsp; Entrance Time</strong
+              ><br />
+            </p>
+            <p><br /></p>
+            <p tabindex="0">
+              <span
+                style="
+                  font-family: arial, helvetica, sans-serif;
+                  font-size: 14px;
+                "
+                >From 09:00-16:00, Tuesday to Sunday.&nbsp;</span
+              >
+            </p>
+            <p tabindex="0">
+              <span
+                style="
+                  font-family: arial, helvetica, sans-serif;
+                  font-size: 14px;
+                "
+                >The museum is closed every Monday, except for
+                holidays.&nbsp;</span
+              >
+            </p>
+            <p tabindex="0">
+              <span
+                style="
+                  font-family: arial, helvetica, sans-serif;
+                  font-size: 14px;
+                "
+                >Please enter the museum before 16:00.</span
+              >
+            </p>
+          </td>
+          <td width="420" valign="top">
+            <p style="text-align: center">
+              <img
+                src="@/assets/images/Visit/pp3.jpg"
+                style="max-width: 100%"
+                tabindex="0"
+                aria-description="Entrance Time"
+              />
+            </p>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+    <p tabindex="0">
+      <span style="font-family: arial, helvetica, sans-serif; font-size: 18px"
+        ><strong>Special Notices</strong></span
+      >
+    </p>
+    <p><br /></p>
+    <p tabindex="0">
+      <span style="font-family: arial, helvetica, sans-serif; font-size: 14px"
+        >1. One ticket is only for one person and the ticket is only valid on
+        the date printed.&nbsp;Please have the ticket checked at the
+        entrance.</span
+      >
+    </p>
+    <p tabindex="0">
+      <span style="font-family: arial, helvetica, sans-serif; font-size: 14px"
+        >2. Senior citizens (above 60) and handicapped persons can enter the
+        exhibition with valid documents even without reservation. Please ask
+        museum personnel for help.</span
+      ><br />
+    </p>
+    <div
+      tabindex="0"
+      aria-description="3. Space in the exhibition is limited, so museum may control the visitor numbers at any time to ensure orderly and pleasant viewing. Thanks for your understanding and cooperation."
+    >
+      <p>
+        <span
+          style="font-family: arial, helvetica, sans-serif; font-size: 14px"
+        >
+          3. Space in the exhibition is limited, so museum may control the
+          visitor numbers at any time to ensure orderly and pleasant viewing.
+          Thanks for your understanding and&nbsp;
+        </span>
+        <br />
+      </p>
+      <p>
+        <span
+          style="font-family: arial, helvetica, sans-serif; font-size: 14px"
+        >
+          &nbsp; &nbsp; cooperation.
+        </span>
+        <br />
+      </p>
+    </div>
+    <p tabindex="0">
+      <span style="font-family: arial, helvetica, sans-serif; font-size: 14px"
+        >4. The exhibition lasts for three months. Please keep this in mind when
+        planning a visit.</span
+      ><br />
+    </p>
+    <p><br /></p>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from "vue";
+
+const origin = ref(location.origin);
+</script>

+ 98 - 0
src/views/Visit/Reservation/components/Visit.vue

@@ -0,0 +1,98 @@
+<template>
+  <div class="in-sidebar in-sidebar-two">
+    <table data-sort="sortDisabled">
+      <tbody>
+        <tr class="firstRow">
+          <td valign="top" colspan="2" rowspan="1">
+            <p style="text-align: center">
+              <img
+                src="@/assets/images/Visit/pp4.jpg"
+                style="max-width: 100%"
+              />
+            </p>
+          </td>
+        </tr>
+        <tr>
+          <td width="430" valign="top">
+            <p><br /></p>
+            <p tabindex="0">
+              <span
+                style="
+                  font-family: arial, helvetica, sans-serif;
+                  font-size: 14px;
+                "
+                >We encourage groups and travel agencies to make reservations by
+                telephone.</span
+              >
+            </p>
+            <p><br /></p>
+            <p tabindex="0">
+              <span
+                style="
+                  font-family: arial, helvetica, sans-serif;
+                  font-size: 14px;
+                "
+              >
+                Requirements: Name of the tour group, full name of the person
+                making the reservation, contact information and number of group
+                members.
+              </span>
+            </p>
+            <p><br /></p>
+            <p>
+              <span
+                tabindex="0"
+                style="
+                  font-family: arial, helvetica, sans-serif;
+                  font-size: 14px;
+                "
+              >
+                Telephone reservation (group visitors):
+                <span
+                  style="
+                    font-size: 14px;
+                    font-family: arial, helvetica, sans-serif;
+                    color: rgb(255, 0, 0);
+                  "
+                >
+                  +86 (10) 63370458&nbsp;
+                </span>
+              </span>
+            </p>
+            <p><br /><br /></p>
+            <p>
+              <span
+                style="
+                  color: rgb(255, 0, 0);
+                  font-family: arial, helvetica, sans-serif;
+                  font-size: 14px;
+                "
+                ><br
+              /></span>
+            </p>
+            <p style="text-align: center">
+              <img
+                src="@/assets/images/Visit/pp6.jpg"
+                style="max-width: 100%"
+                tabindex="0"
+                aria-description="Group Visit"
+              />
+            </p>
+          </td>
+          <td width="430" valign="top">
+            <p style="text-align: center"><br /></p>
+            <p>
+              <img
+                src="@/assets/images/Visit/pp5.jpg"
+                style="max-width: 100%"
+                tabindex="0"
+                aria-description="Group Visit"
+              /><br />
+            </p>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+    <p><br /></p>
+  </div>
+</template>

+ 141 - 0
src/views/Visit/Reservation/index.scss

@@ -0,0 +1,141 @@
+.reservation {
+  margin-bottom: 20px;
+
+  &-container {
+    width: 1180px;
+    margin: auto;
+  }
+
+  strong {
+    font-weight: 700;
+    font-size: 16px;
+  }
+
+  .public {
+    overflow: hidden;
+    transition: all 0.3s;
+    height: 39px;
+    background-color: rgba(206, 36, 69, 0.97);
+    width: 100%;
+    cursor: pointer;
+    position: absolute;
+    left: 0;
+    top: 60px;
+    & > div {
+      width: 1180px;
+      margin: 0 auto;
+      padding: 5px 0;
+      font-size: 18px;
+      line-height: 1.6;
+      color: #fff;
+      & > span {
+        float: right;
+      }
+    }
+  }
+  .hintShow {
+    height: 0px;
+  }
+  .mm1 {
+    height: 1%;
+    overflow: hidden;
+    padding-bottom: 30px;
+    .mm1l {
+      width: 550px;
+      float: left;
+      font-size: 18px;
+      line-height: 28px;
+      & > p {
+        font-size: 14px;
+      }
+    }
+    .mm1r {
+      width: 590px;
+      float: right;
+      padding-top: 43px;
+      & > p {
+        padding: 15px;
+        font-size: 14px;
+        line-height: 28px;
+      }
+      & > h2 {
+        cursor: pointer;
+        clear: both;
+        background-color: #c90006;
+        text-align: center;
+        height: 30px;
+        line-height: 30px;
+        color: #fff;
+        font-weight: normal;
+      }
+    }
+  }
+
+  .collapse {
+    --el-collapse-content-bg-color: var(--white-bg);
+    --el-collapse-content-text-color: var(--black-text-color);
+
+    :deep(.el-collapse-item) {
+      margin-bottom: 1px;
+
+      &:nth-child(1) .el-collapse-item__header {
+        background: url("@/assets/images/Visit/m-28.jpg");
+      }
+      &:nth-child(2) .el-collapse-item__header {
+        background: url("@/assets/images/Visit/m-29.jpg");
+      }
+      &:nth-child(3) .el-collapse-item__header {
+        background: url("@/assets/images/Visit/m-30.jpg");
+      }
+      &.is-active {
+        .el-collapse-item__header::after {
+          background-image: url("@/assets/images/Visit/m-32.png");
+        }
+      }
+      .el-collapse-item__header {
+        position: relative;
+        padding: 0 10px;
+        height: 50px;
+        border: none;
+        color: white;
+        font-size: 24px;
+
+        &::after {
+          content: "";
+          position: absolute;
+          top: 50%;
+          right: 30px;
+          width: 14px;
+          height: 14px;
+          background: url("@/assets/images/Visit/m-31.png") no-repeat center /
+            contain;
+          transform: translateY(-50%);
+        }
+      }
+      .el-collapse-item__arrow {
+        display: none;
+      }
+    }
+
+    .in-sidebar {
+      padding: 0 20px;
+      font-size: 18px;
+      line-height: 28px;
+
+      .firstRow {
+        p {
+          height: 1px;
+        }
+      }
+      :deep(strong) {
+        font-weight: 700;
+        font-size: 16px;
+      }
+      :deep(img) {
+        border: none;
+        vertical-align: top;
+        max-width: 100%;
+      }
+    }
+  }
+}

+ 70 - 0
src/views/Visit/Reservation/index.vue

@@ -0,0 +1,70 @@
+<template>
+  <div
+    class="reservation"
+    data-aria-viewport-area
+    aria-description="You've reached the content area for the Reservation page. This area contains three parts of content. Please press the tab key to browse the information."
+  >
+    <div class="reservation-container">
+      <div class="mm1">
+        <div class="mm1l">
+          <p>
+            <img src="@/assets/images/Visit/pLeft.jpg" />
+            <span>&nbsp;<strong>How to Make a Reservation?</strong></span>
+          </p>
+          <p><br /></p>
+          <p>Telephone Reservation:</p>
+          <p>
+            •&nbsp; Individual visitors:&nbsp;<span
+              style="
+                color: rgb(255, 0, 0);
+                font-family: arial, helvetica, sans-serif;
+                font-size: 14px;
+              "
+              >+86 (10) 63393339</span
+            >
+          </p>
+          <p>
+            •&nbsp; Group visitors:&nbsp;<span
+              style="font-size: 14px; color: rgb(255, 0, 0)"
+              >+86 (10) 63370458</span
+            >
+          </p>
+          <p>From 9:00 to 17:00 every day.</p>
+          <p><br /></p>
+        </div>
+        <div class="mm1r">
+          <p>
+            Either system will issue a confirmation number. Visitors will be
+            required to show the number and valid ID in order to receive free
+            entrance tickets on the day of their visit.
+          </p>
+        </div>
+      </div>
+
+      <ElCollapse v-model="activeName" accordion class="collapse">
+        <ElCollapseItem title="Guidelines for Reservations" name="reservations">
+          <Reservations />
+        </ElCollapseItem>
+        <ElCollapseItem title="Group Visit" name="visit">
+          <Visit />
+        </ElCollapseItem>
+        <ElCollapseItem title="Relevant Information" name="information">
+          <Information />
+        </ElCollapseItem>
+      </ElCollapse>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from "vue";
+import Reservations from "./components/Reservations.vue";
+import Visit from "./components/Visit.vue";
+import Information from "./components/Information.vue";
+
+const activeName = ref("reservations");
+</script>
+
+<style lang="scss" scoped>
+@import "./index.scss";
+</style>

+ 170 - 0
src/views/Visit/index.vue

@@ -0,0 +1,170 @@
+<template>
+  <div class="visit">
+    <img
+      :aria-description="`You've reached the banner area of the ${curRoute?.name} page; this area has one image; please use the tab key to navigate through the content.`"
+      class="visit-banner"
+      src="@/assets/images/Visit/m-4.jpg"
+    />
+
+    <div class="container">
+      <ul class="visit-nav">
+        <li
+          v-for="(item, index) in NAV_LIST"
+          :key="index"
+          :class="['visit-nav-item', { active: $route.name === item.pathName }]"
+          aria-label="Link"
+          :aria-description="item.name"
+          @click="$router.push({ name: item.pathName })"
+          @keydown.enter.passive="$router.push({ name: item.pathName })"
+        >
+          <div class="visit-nav-item__img">
+            <img :src="item.img" />
+          </div>
+          <p>
+            {{ item.name }}
+          </p>
+        </li>
+      </ul>
+
+      <div
+        class="breadcrumb"
+        data-aria-viewport-area
+        aria-description="You've reached the path area; this area contains three URLs; please use the tab key to go through the content."
+      >
+        <span style="color: var(--van-primary-color)"
+          >Your Position:&nbsp;</span
+        >
+
+        <RouterLink replace :to="{ name: 'Home' }" aria-description="Home">
+          Home>
+        </RouterLink>
+        <RouterLink replace :to="{ name: 'Visit' }" aria-description="Visit">
+          Visit>
+        </RouterLink>
+        <span>
+          {{ curRoute?.name }}
+        </span>
+      </div>
+
+      <RouterView />
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import CalendarIcon from "@/assets/images/Visit/m-16.png";
+import DirectionIcon from "@/assets/images/Visit/m-9.png";
+import ReservationIcon from "@/assets/images/Visit/m-10.png";
+import PlansIcon from "@/assets/images/Visit/m-11.png";
+import GuideIcon from "@/assets/images/Visit/m-12.png";
+import AccessibilityIcon from "@/assets/images/Visit/m-13.png";
+import ShopIcon from "@/assets/images/Visit/m-14.png";
+import GuidelinesIcon from "@/assets/images/Visit/m-15.png";
+import { useRoute } from "vue-router";
+import { computed } from "vue";
+
+const NAV_LIST = [
+  {
+    name: "Calendar",
+    img: CalendarIcon,
+    pathName: "Calendar",
+  },
+  {
+    name: "Hours, Direction & Admission",
+    img: DirectionIcon,
+    pathName: "Direction",
+  },
+  { name: "Reservation", img: ReservationIcon, pathName: "Reservation" },
+  { name: "Floor Plans", img: PlansIcon, pathName: "Plans" },
+  {
+    name: "Audio Guide & Tour",
+    img: GuideIcon,
+    pathName: "/Layout/Visit/4",
+  },
+  {
+    name: "Accessibility",
+    img: AccessibilityIcon,
+    pathName: "/Layout/Visit/5",
+  },
+  { name: "Café & Shop", img: ShopIcon, pathName: "/Layout/Visit/6" },
+  {
+    name: "Visitor Guidelines",
+    img: GuidelinesIcon,
+    pathName: "/Layout/Visit/7",
+  },
+];
+
+const route = useRoute();
+const curRoute = computed(() =>
+  NAV_LIST.find((i) => i.pathName === (route.name as string))
+);
+</script>
+
+<style lang="scss" scoped>
+.visit {
+  &-banner {
+    margin-top: -60px;
+    display: block;
+    width: 100%;
+    height: 300px;
+    object-fit: cover;
+  }
+
+  &-nav {
+    position: relative;
+    margin-top: -15px;
+    padding: 5px 60px;
+    display: flex;
+    justify-content: center;
+    background: var(--white-bg);
+    border-radius: 5px;
+    z-index: 1;
+
+    li {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      width: 168px;
+      height: 108px;
+      cursor: pointer;
+    }
+
+    &-item {
+      color: var(--black-text-color);
+
+      &.active {
+        color: var(--van-primary-color);
+
+        .visit-nav-item__img {
+          background: var(--van-primary-color);
+        }
+      }
+      &__img {
+        margin: 10px 0;
+        width: 50px;
+        height: 50px;
+        border-radius: 50%;
+        background: black;
+
+        img {
+          width: inherit;
+          height: inherit;
+        }
+      }
+      p {
+        font-size: 14px;
+        line-height: 18px;
+        text-align: center;
+      }
+    }
+  }
+
+  .breadcrumb {
+    max-width: 1180px;
+    height: 28px;
+    line-height: 28px;
+    font-size: 12px;
+    margin: 10px auto;
+  }
+}
+</style>

+ 14 - 1
vite.config.ts

@@ -8,14 +8,27 @@ import { VantResolver } from "unplugin-vue-components/resolvers";
 import { createSvgIconsPlugin } from "vite-plugin-svg-icons";
 import path from "path";
 
+import AutoImport from "unplugin-auto-import/vite";
+import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
+
 // https://vitejs.dev/config/
 export default defineConfig({
   base: "./",
+  css: {
+    preprocessorOptions: {
+      scss: {
+        additionalData: `@use "@/assets/css/element.scss" as *;`,
+      },
+    },
+  },
   plugins: [
     vue(),
     vueJsx(),
     Components({
-      resolvers: [VantResolver()],
+      resolvers: [VantResolver(), ElementPlusResolver({ importStyle: "sass" })],
+    }),
+    AutoImport({
+      resolvers: [ElementPlusResolver()],
     }),
     createSvgIconsPlugin({
       // 指定需要缓存的图标文件夹