chenlei 1 年之前
父節點
當前提交
966b3bd64b
共有 41 個文件被更改,包括 3410 次插入2108 次删除
  1. 42 0
      src/api/collections.ts
  2. 29 0
      src/api/exhibition.ts
  3. 3 0
      src/api/index.ts
  4. 42 0
      src/api/publications.ts
  5. 7 8
      src/components/Layout/index.vue
  6. 55 0
      src/data.ts
  7. 5 0
      src/router/index.ts
  8. 57 0
      src/utils/usePagination.ts
  9. 139 0
      src/views/About/Link.vue
  10. 84 56
      src/views/About/components/Connections.vue
  11. 176 106
      src/views/About/components/History.vue
  12. 282 0
      src/views/About/data.ts
  13. 二進制
      src/views/About/images/acBac.jpg
  14. 二進制
      src/views/About/images/botBac.jpg
  15. 二進制
      src/views/About/images/botTxt.gif
  16. 二進制
      src/views/About/images/linktop.jpg
  17. 61 52
      src/views/About/index.vue
  18. 81 73
      src/views/Collections/components/DetailDialog/index.scss
  19. 141 87
      src/views/Collections/components/DetailDialog/index.vue
  20. 70 43
      src/views/Collections/components/Menu.vue
  21. 80 79
      src/views/Collections/index.scss
  22. 131 100
      src/views/Collections/index.vue
  23. 92 92
      src/views/Exhibitions/Detail/index.vue
  24. 65 40
      src/views/Exhibitions/components/ImgItem/index.vue
  25. 70 55
      src/views/Exhibitions/components/ListItem/index.vue
  26. 88 86
      src/views/Exhibitions/index.scss
  27. 201 129
      src/views/Exhibitions/index.vue
  28. 85 67
      src/views/JoinSupport/Give/detail.vue
  29. 57 52
      src/views/JoinSupport/Give/index.vue
  30. 96 20
      src/views/JoinSupport/Volunteer/detail.vue
  31. 82 72
      src/views/JoinSupport/index.vue
  32. 20 21
      src/views/LearnEngage/List/index.vue
  33. 63 62
      src/views/Publications/Catalogues/index.scss
  34. 73 27
      src/views/Publications/Catalogues/index.vue
  35. 57 49
      src/views/Publications/Detail/index.scss
  36. 56 23
      src/views/Publications/Detail/index.vue
  37. 118 116
      src/views/Publications/Magazines/index.scss
  38. 142 76
      src/views/Publications/Magazines/index.vue
  39. 69 47
      src/views/Publications/index.vue
  40. 240 216
      src/views/Visit/Calendar/components/Calendar.vue
  41. 251 254
      src/views/Visit/Calendar/components/DateTable/index.vue

+ 42 - 0
src/api/collections.ts

@@ -0,0 +1,42 @@
+import {
+  requestByGet,
+  requestByPost,
+  type PaginationParams,
+} from "@dage/service";
+
+export interface CollectionThumbListItem {
+  id: number;
+  type: string;
+  thumb: string;
+}
+
+export interface CollectionListParams extends PaginationParams {
+  type?: string;
+}
+
+export interface CollectionListItem {
+  id: number;
+  name: string;
+  thumb: string;
+  /** 年代 */
+  dictAge: string;
+  /** 摘要 */
+  digest: string;
+}
+
+export interface CollectionDetail extends CollectionListItem {
+  rtf: string;
+  size: string;
+}
+
+export const getCollectionThumbListApi = () => {
+  return requestByGet<CollectionThumbListItem[]>("/api/show/collection/thumb");
+};
+
+export const getCollectionListApi = (params: CollectionListParams) => {
+  return requestByPost("/api/show/collection/pageList", params);
+};
+
+export const getCollectionDetailApi = (id: number) => {
+  return requestByGet<CollectionDetail>(`/api/show/collection/detail/${id}`);
+};

+ 29 - 0
src/api/exhibition.ts

@@ -0,0 +1,29 @@
+import { requestByPost, type PaginationParams } from "@dage/service";
+
+export interface GetExhibitionListParams extends PaginationParams {
+  /**
+   * 展览时间, temp:临时展览(需要添加开始-结束时间) | long:常设展览
+   */
+  type?: "temp" | "long";
+  searchKey?: string;
+  /**
+   * 展览地址,inland:国内 | foreign:国外
+   */
+  addrType?: "inland" | "foreign";
+  year?: string;
+  /**
+   * 当前展览, 0: 过去展览 | 1:当前展览
+   */
+  isCurrent?: 0 | 1;
+}
+
+export interface ExhibitionsListItem {
+  id: number;
+  name: string;
+  thumb: string;
+  digest: string;
+}
+
+export const getExhibitionListApi = (params: GetExhibitionListParams) => {
+  return requestByPost("/api/show/exhibition/pageList", params);
+};

+ 3 - 0
src/api/index.ts

@@ -13,3 +13,6 @@ export const getBannerApi = (id: number) => {
 };
 
 export * from "./learn";
+export * from "./publications";
+export * from "./collections";
+export * from "./exhibition";

+ 42 - 0
src/api/publications.ts

@@ -0,0 +1,42 @@
+import {
+  requestByGet,
+  requestByPost,
+  type PaginationParams,
+} from "@dage/service";
+
+export interface PublishListParams extends PaginationParams {
+  type?: "Magazines" | "Exhibition";
+  searchKey?: string;
+  year?: number;
+}
+
+/**
+ * 书刊类型
+ */
+export interface PubicationItem {
+  id: number;
+  thumb: string;
+  name: string;
+}
+
+/**
+ * 展会会刊类型
+ */
+export interface ExhibitionCatalogue extends PubicationItem {
+  filePath: string;
+}
+
+/**
+ * 书刊详情类型
+ */
+export interface PublicationDetail extends PubicationItem {
+  rtf: string;
+}
+
+export const getPublishListApi = async (params: PublishListParams) => {
+  return requestByPost("/api/show/publish/pageList", params);
+};
+
+export const getPublishDetailApi = async (id: string | number) => {
+  return requestByGet<PublicationDetail>(`/api/show/publish/detail/${id}`);
+};

+ 7 - 8
src/components/Layout/index.vue

@@ -6,7 +6,7 @@
       @hide="loveSwitch = false"
     />
 
-    <div class="aria-control-target">
+    <div class="aria-control-target layout-main">
       <!-- 顶部top -->
       <div class="topnav-wrap">
         <div class="topnav">
@@ -295,12 +295,11 @@ const handleSearch = (txt: string) => {
     --white-bg: black;
     --white-text-color: #ff0;
   }
-}
-
-.aria-control-target {
-  position: relative;
-  min-height: 100vh;
-  padding-bottom: 45px;
+  &-main {
+    position: relative;
+    min-height: 100vh;
+    padding-bottom: 45px;
+  }
 }
 
 .topnav {
@@ -476,7 +475,7 @@ const handleSearch = (txt: string) => {
   right: 0;
   height: 164px;
   width: 60px;
-  z-index: 990;
+  z-index: var(--el-index-popper);
   .rightIco {
     width: 100%;
     height: 100%;

文件差異過大導致無法顯示
+ 55 - 0
src/data.ts


+ 5 - 0
src/router/index.ts

@@ -164,6 +164,11 @@ const router = createRouter({
           component: () => import("../views/About/index.vue"),
         },
         {
+          path: "/Layout/About/link",
+          name: "AboutLink",
+          component: () => import("../views/About/Link.vue"),
+        },
+        {
           path: "/Layout/Search",
           name: "Search",
           component: () => import("../views/Search/index.vue"),

+ 57 - 0
src/utils/usePagination.ts

@@ -0,0 +1,57 @@
+import { computed, ref } from "vue";
+
+export enum PaginationType {
+  DEFAULT = 0,
+  CONCAT = 1,
+}
+
+export const usePagination = <T>(
+  handler: (params: { pageNum: number; pageSize: number }) => any,
+  type = PaginationType.DEFAULT,
+  size = 10
+) => {
+  const pageNum = ref(1);
+  const total = ref(0);
+  const loading = ref(false);
+  const list = ref<T[]>([]);
+  const noData = computed(() => !total.value && !loading.value);
+  const noMore = computed(() => size * pageNum.value < total.value);
+
+  const getList = async () => {
+    try {
+      loading.value = true;
+
+      const data = await handler({
+        pageNum: pageNum.value,
+        pageSize: size,
+      });
+      total.value = data.total;
+
+      if (type === PaginationType.DEFAULT) {
+        list.value = data.records;
+      } else {
+        list.value =
+          pageNum.value > 1 ? list.value.concat(data.records) : data.records;
+      }
+    } finally {
+      loading.value = false;
+    }
+  };
+
+  const resetParams = () => {
+    pageNum.value = 1;
+    total.value = 0;
+    list.value = [];
+  };
+
+  return {
+    pageNum,
+    loading,
+    list,
+    total,
+    noData,
+    noMore,
+    getList,
+    resetParams,
+  };
+};

+ 139 - 0
src/views/About/Link.vue

@@ -0,0 +1,139 @@
+<template>
+  <div class="about-link">
+    <img
+      class="about-link-banner"
+      aria-label="Image"
+      aria-description="You've reached the banner area of the About page; this page has one image; please use the tab key to go through the content."
+      :src="bannerUrl"
+    />
+
+    <div style="background: var(--white-bg)">
+      <div class="container">
+        <Breadcrumb
+          :parents="[
+            {
+              label: 'About',
+              routeParams: { name: 'About' },
+            },
+          ]"
+          :cur-route="{ name: 'Link' }"
+        />
+
+        <div
+          class="about-link-title"
+          tabindex="0"
+          aria-description="Partners & Connections"
+        />
+      </div>
+
+      <div
+        v-for="(item, index) in linkData"
+        :key="item.title"
+        :class="index % 2 === 0 ? 'about-link-line' : 'about-link-line2'"
+      >
+        <div class="container">
+          <h2 :title="item.title" tabindex="0">{{ item.title }}</h2>
+
+          <ul>
+            <li
+              v-for="subitem in item.son"
+              :key="subitem.pop"
+              class="about-link-item"
+            >
+              <a
+                :href="subitem.url"
+                target="_blank"
+                aria-label="Image link"
+                :aria-description="subitem.pop"
+              >
+                <img :src="subitem.img" />
+              </a>
+            </li>
+          </ul>
+        </div>
+      </div>
+    </div>
+
+    <div
+      class="about-link-footer"
+      data-aria-viewport-area
+      tabindex="0"
+      aria-description="You've reached the Join us section of the Partners & COnnections page, please use the tab key to navigate through the content."
+    >
+      <img
+        src="./images/botTxt.gif"
+        alt=""
+        tabindex="0"
+        aria-label=""
+        aria-description="Join US Telephone +86(10)63393339 E-mail ICD@capitalmuseum.org.cn"
+      />
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import Breadcrumb from "@/components/Breadcrumb/index.vue";
+import { useBaseStore } from "@/stores/base";
+import { getBaseURL } from "@dage/service";
+import { computed } from "vue";
+import { linkData } from "./data";
+
+const baseUrl = getBaseURL();
+const baseStore = useBaseStore();
+const bannerUrl = computed(() => {
+  return (
+    baseUrl + baseStore.bannerList.find((i) => i.name === "About")?.thumbPc
+  );
+});
+</script>
+
+<style lang="scss" scoped>
+.about-link {
+  &-banner {
+    margin-top: -60px;
+    display: block;
+    width: 100%;
+    height: 300px;
+    object-fit: cover;
+  }
+  &-title {
+    height: 132px;
+    background: url("./images/linktop.jpg") no-repeat left 50%;
+    border-bottom: 1px solid #cacaca;
+  }
+  &-line2 {
+    background: url("./images/acBac.jpg");
+  }
+  &-line,
+  &-line2 {
+    h2 {
+      font-size: 18px;
+      line-height: 30px;
+      padding: 30px 0 18px;
+      color: #101010;
+    }
+
+    ul {
+      display: flex;
+      padding-bottom: 35px;
+
+      li {
+        margin: 5px;
+        width: 386px;
+        height: 143px;
+        filter: grayscale(100%);
+
+        &:hover {
+          filter: grayscale(0);
+        }
+      }
+    }
+  }
+  &-footer {
+    height: 173px;
+    padding-top: 86px;
+    background: url("./images/botBac.jpg") repeat-x top;
+    text-align: center;
+  }
+}
+</style>

+ 84 - 56
src/views/About/components/Connections.vue

@@ -1,56 +1,84 @@
-<template>
-  <div class="about-connections">
-    <div class="container">
-      <ul>
-        <li class="about-connections-item">
-          <a
-            href=""
-            target="_blank"
-            aria-label="Image link"
-            aria-description="Edo-Tokyo Museum"
-          >
-            <img src="../images/link/8.jpg" />
-          </a>
-        </li>
-      </ul>
-
-      <div class="about-connections__more">See More</div>
-    </div>
-  </div>
-</template>
-
-<style lang="scss" scoped>
-.about-connections {
-  position: relative;
-  padding-top: 115px;
-  height: 559px;
-  background: url("../images/a6.jpg") center top no-repeat;
-
-  ul {
-    display: flex;
-    flex-wrap: wrap;
-    margin: -10px -5px;
-  }
-  &-item {
-    margin: 10px 5px;
-    width: calc(33.3333% - 10px);
-    height: 143px;
-    filter: grayscale(100%);
-
-    &:hover {
-      filter: grayscale(0);
-    }
-  }
-  &__more {
-    margin: 20px auto 0;
-    width: 148px;
-    height: 48px;
-    text-align: center;
-    line-height: 48px;
-    color: white;
-    font-size: 24px;
-    background: var(--van-primary-color);
-    cursor: pointer;
-  }
-}
-</style>
+<template>
+  <div class="about-connections">
+    <div class="container">
+      <ul>
+        <li
+          v-for="item in links"
+          :key="item.pop"
+          class="about-connections-item"
+        >
+          <a
+            :href="item.url"
+            target="_blank"
+            aria-label="Image link"
+            :aria-description="item.pop"
+          >
+            <img :src="item.img" />
+          </a>
+        </li>
+      </ul>
+
+      <div
+        class="about-connections__more"
+        @click="$router.push({ name: 'AboutLink' })"
+      >
+        See More
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { linkData } from "../data";
+
+const list = linkData.reduce<(typeof linkData)[0]["son"]>((prev, cur) => {
+  prev.push(...cur.son);
+  return prev;
+}, []);
+const links = list.filter((i) =>
+  [
+    "Edo-Tokyo Museum",
+    "Russian State Historical Museum",
+    "Royal Ontario Museum",
+    "Seoul Museum of History",
+    "Museums Victoria",
+    "Virginia Museum of Fine Arts",
+  ].includes(i.pop)
+);
+</script>
+
+<style lang="scss" scoped>
+.about-connections {
+  position: relative;
+  padding-top: 115px;
+  height: 559px;
+  background: url("../images/a6.jpg") center top no-repeat;
+
+  ul {
+    display: flex;
+    flex-wrap: wrap;
+    margin: -10px -5px;
+  }
+  &-item {
+    margin: 10px 5px;
+    width: calc(33.3333% - 10px);
+    height: 143px;
+    filter: grayscale(100%);
+
+    &:hover {
+      filter: grayscale(0);
+    }
+  }
+  &__more {
+    margin: 20px auto 0;
+    width: 148px;
+    height: 48px;
+    text-align: center;
+    line-height: 48px;
+    color: white;
+    font-size: 24px;
+    background: var(--van-primary-color);
+    cursor: pointer;
+  }
+}
+</style>

+ 176 - 106
src/views/About/components/History.vue

@@ -1,106 +1,176 @@
-<template>
-  <div class="about-history">
-    <div class="container">
-      <p class="about-history__title">History</p>
-
-      <ul>
-        <li
-          v-for="(item, idx) in LIST"
-          :key="idx"
-          class="about-history-item"
-          :style="{ backgroundImage: `url(${item.bg})` }"
-        >
-          <div class="about-history-item-hover">
-            <p>{{ item.label }}</p>
-
-            <div class="about-history-item-hover__btn">Enter</div>
-          </div>
-        </li>
-      </ul>
-    </div>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import Bg1Img from "../images/his/1.png";
-
-const LIST = [
-  {
-    bg: Bg1Img,
-    label: "The Capital Museum",
-  },
-];
-</script>
-
-<style lang="scss" scoped>
-.about-history {
-  position: relative;
-  padding-top: 40px;
-  height: 501px;
-  background: url("../images/au3.png") 100% 100%;
-
-  &__title {
-    margin-bottom: 30px;
-    font-size: 30px;
-    font-weight: bold;
-    text-align: center;
-  }
-  ul {
-    display: flex;
-    justify-content: space-between;
-  }
-  &-item {
-    position: relative;
-    width: 280px;
-    height: 360px;
-    overflow: hidden;
-    transition: all 0.1s ease 0s;
-    background-position: center top;
-    background-size: cover;
-    border-radius: 8px;
-    cursor: pointer;
-    transition: all 0.1s ease 0s;
-
-    &:hover {
-      transform: scale(1.1);
-
-      .about-history-item-hover {
-        opacity: 1;
-      }
-    }
-    &-hover {
-      position: absolute;
-      top: 0;
-      left: 0;
-      right: 0;
-      bottom: 0;
-      padding: 10px 10px 70px;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-      background: var(--van-primary-color);
-      opacity: 0;
-
-      p {
-        color: white;
-        font-size: 18px;
-        line-height: 26px;
-      }
-      &__btn {
-        display: flex;
-        justify-content: center;
-        align-items: center;
-        position: absolute;
-        left: 50%;
-        bottom: 40px;
-        width: 130px;
-        height: 42px;
-        color: white;
-        font-size: 20px;
-        border: 1px solid white;
-        transform: translateX(-50%);
-      }
-    }
-  }
-}
-</style>
+<template>
+  <div class="about-history">
+    <div class="container">
+      <p class="about-history__title">History</p>
+
+      <ul>
+        <li
+          v-for="(item, idx) in hisData"
+          :key="idx"
+          class="about-history-item"
+          :style="{ backgroundImage: `url(${item.bg})` }"
+          @click="checkedItem = item"
+        >
+          <div class="about-history-item-hover">
+            <p>{{ item.title }}</p>
+
+            <div class="about-history-item-hover__btn">Enter</div>
+          </div>
+        </li>
+
+        <div v-if="checkedItem" class="about-history-info">
+          <p class="about-history-info__label">{{ checkedItem.title }}</p>
+          <div v-html="checkedItem.txt" />
+
+          <img
+            class="about-history-info__close"
+            src="../images/his/back.png"
+            @click="checkedItem = null"
+          />
+        </div>
+      </ul>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from "vue";
+import { hisData } from "../data";
+
+const checkedItem = ref<(typeof hisData)[0] | null>(null);
+</script>
+
+<style lang="scss" scoped>
+.about-history {
+  position: relative;
+  padding-top: 40px;
+  height: 501px;
+  background: url("../images/au3.png") 100% 100%;
+
+  &-info {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    display: flex;
+    flex-direction: column;
+    padding: 20px 20px 40px;
+    box-shadow: 0 0 8px 4px #ccc;
+    background: var(--white-bg);
+    border-radius: 4px;
+    z-index: 1;
+
+    &__label {
+      margin-bottom: 15px;
+      padding: 0 20px;
+      color: var(--van-primary-color);
+      line-height: 26px;
+      font-size: 22px;
+      font-weight: bold;
+      text-align: center;
+    }
+    div {
+      flex: 1;
+      height: 0;
+      overflow-y: auto;
+      line-height: 20px;
+      font-size: 14px;
+
+      &::-webkit-scrollbar {
+        width: 6px;
+        height: 1px;
+      }
+      &::-webkit-scrollbar-thumb {
+        border-radius: 10px;
+        background: #d9d9d9;
+      }
+      &::-webkit-scrollbar-track {
+        border-radius: 10px;
+        background: transparent;
+      }
+      :deep(p) {
+        margin-bottom: 15px;
+
+        b {
+          display: block;
+          font-weight: bold;
+        }
+      }
+    }
+    &__close {
+      position: absolute;
+      left: 50%;
+      bottom: -20px;
+      width: 40px;
+      height: 40px;
+      cursor: pointer;
+      transform: translateX(-50%);
+    }
+  }
+  &__title {
+    margin-bottom: 30px;
+    font-size: 30px;
+    font-weight: bold;
+    text-align: center;
+  }
+  ul {
+    display: flex;
+    justify-content: space-between;
+    position: relative;
+  }
+  &-item {
+    position: relative;
+    width: 280px;
+    height: 360px;
+    overflow: hidden;
+    transition: all 0.1s ease 0s;
+    background-position: center top;
+    background-size: cover;
+    border-radius: 8px;
+    cursor: pointer;
+    transition: all 0.1s ease 0s;
+
+    &:hover {
+      transform: scale(1.1);
+
+      .about-history-item-hover {
+        opacity: 1;
+      }
+    }
+    &-hover {
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      padding: 10px 10px 70px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      background: var(--van-primary-color);
+      opacity: 0;
+
+      p {
+        color: white;
+        font-size: 18px;
+        line-height: 26px;
+      }
+      &__btn {
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        position: absolute;
+        left: 50%;
+        bottom: 40px;
+        width: 130px;
+        height: 42px;
+        color: white;
+        font-size: 20px;
+        border: 1px solid white;
+        transform: translateX(-50%);
+      }
+    }
+  }
+}
+</style>

文件差異過大導致無法顯示
+ 282 - 0
src/views/About/data.ts


二進制
src/views/About/images/acBac.jpg


二進制
src/views/About/images/botBac.jpg


二進制
src/views/About/images/botTxt.gif


二進制
src/views/About/images/linktop.jpg


+ 61 - 52
src/views/About/index.vue

@@ -1,52 +1,61 @@
-<template>
-  <div class="about">
-    <img
-      class="about-banner"
-      aria-label="Image"
-      aria-description="You've reached the banner area of the About page; this page has one image; please use the tab key to go through the content."
-      src="./images/banner.jpg"
-    />
-
-    <div class="container">
-      <Breadcrumb :parents="[]" :cur-route="{ name: 'About' }" />
-    </div>
-
-    <Director />
-
-    <History />
-
-    <Connections />
-
-    <Contact />
-  </div>
-</template>
-
-<script lang="ts" setup>
-import Breadcrumb from "@/components/Breadcrumb/index.vue";
-import Director from "./components/Director.vue";
-import History from "./components/History.vue";
-import Connections from "./components/Connections.vue";
-import Contact from "./components/Contact.vue";
-import { useRoute } from "vue-router";
-import { watch, nextTick } from "vue";
-
-const route = useRoute();
-
-watch(
-  route,
-  (v) => {
-    if (v.query.scroll) {
-      nextTick(() => {
-        window.scrollTo({ top: Number(v.query.scroll), behavior: "smooth" });
-      });
-    }
-  },
-  {
-    immediate: true,
-  }
-);
-</script>
-
-<style lang="scss" scoped>
-@import "./index.scss";
-</style>
+<template>
+  <div class="about">
+    <img
+      class="about-banner"
+      aria-label="Image"
+      aria-description="You've reached the banner area of the About page; this page has one image; please use the tab key to go through the content."
+      :src="bannerUrl"
+    />
+
+    <div class="container">
+      <Breadcrumb :parents="[]" :cur-route="{ name: 'About' }" />
+    </div>
+
+    <Director />
+
+    <History />
+
+    <Connections />
+
+    <Contact />
+  </div>
+</template>
+
+<script lang="ts" setup>
+import Breadcrumb from "@/components/Breadcrumb/index.vue";
+import Director from "./components/Director.vue";
+import History from "./components/History.vue";
+import Connections from "./components/Connections.vue";
+import Contact from "./components/Contact.vue";
+import { useRoute } from "vue-router";
+import { watch, nextTick, computed } from "vue";
+import { useBaseStore } from "@/stores/base";
+import { getBaseURL } from "@dage/service";
+
+const route = useRoute();
+const baseUrl = getBaseURL();
+const baseStore = useBaseStore();
+const bannerUrl = computed(() => {
+  return (
+    baseUrl + baseStore.bannerList.find((i) => i.name === "About")?.thumbPc
+  );
+});
+
+watch(
+  route,
+  (v) => {
+    if (v.query.scroll) {
+      nextTick(() => {
+        window.scrollTo({ top: Number(v.query.scroll), behavior: "smooth" });
+      });
+    }
+  },
+  {
+    immediate: true,
+  }
+);
+</script>
+
+<style lang="scss" scoped>
+@import "./index.scss";
+</style>

+ 81 - 73
src/views/Collections/components/DetailDialog/index.scss

@@ -1,73 +1,81 @@
-.collection-detail {
-  --el-dialog-margin-top: 8vh;
-  --el-dialog-bg-color: transparent;
-  --el-dialog-box-shadow: none;
-
-  .el-dialog__body {
-    display: flex;
-    align-items: flex-start;
-    gap: 30px;
-  }
-  .el-dialog__close {
-    --el-color-info: white;
-    width: 40px;
-    height: 40px;
-    border-radius: 50%;
-    background: var(--van-primary-color);
-
-    svg {
-      width: 22px;
-      height: 22px;
-    }
-  }
-  .el-dialog__headerbtn {
-    top: 26px;
-    right: -40px;
-
-    &:hover .el-dialog__close {
-      color: white;
-    }
-  }
-  &-preview,
-  &-info {
-    padding: 20px;
-    background: var(--white-bg);
-    box-shadow: 0 0 10px #000;
-  }
-  &-preview {
-    width: 540px;
-    color: var(--black-text-color);
-
-    h3 {
-      font-weight: 700;
-      font-size: 36px;
-      line-height: 100%;
-    }
-    .el-image {
-      margin: 20px 0;
-      width: 100%;
-    }
-    &__btn {
-      margin: 0 auto;
-      width: 160px;
-      height: 40px;
-      font-size: 16px;
-      line-height: 40px;
-      text-align: center;
-      border: 1px solid var(--black-text-color);
-      cursor: pointer;
-    }
-  }
-  &-info {
-    flex: 1;
-    width: 440px;
-  }
-  &__mask {
-    position: absolute;
-    top: 0;
-    left: 0;
-    width: 64px;
-    height: 133px;
-    background: url("../../images/mark.png") no-repeat center / contain;
-  }
-}
+.collection-detail {
+  --el-dialog-margin-top: 8vh;
+  --el-dialog-bg-color: transparent;
+  --el-dialog-box-shadow: none;
+
+  .el-dialog__body {
+    display: flex;
+    align-items: flex-start;
+    gap: 30px;
+  }
+  .el-dialog__close {
+    --el-color-info: white;
+    width: 40px;
+    height: 40px;
+    border-radius: 50%;
+    background: var(--van-primary-color);
+
+    svg {
+      width: 22px;
+      height: 22px;
+    }
+  }
+  .el-dialog__headerbtn {
+    top: 26px;
+    right: -40px;
+
+    &:hover .el-dialog__close {
+      color: white;
+    }
+  }
+  &-preview,
+  &-info {
+    padding: 20px;
+    background: var(--white-bg);
+    box-shadow: 0 0 10px #000;
+  }
+  &-preview {
+    width: 540px;
+    color: var(--black-text-color);
+
+    h3 {
+      font-weight: 700;
+      font-size: 36px;
+      line-height: 100%;
+    }
+    .el-image {
+      margin: 20px 0;
+      width: 100%;
+    }
+    &__btn {
+      margin: 0 auto;
+      width: 160px;
+      height: 40px;
+      font-size: 16px;
+      line-height: 40px;
+      text-align: center;
+      border: 1px solid var(--black-text-color);
+      cursor: pointer;
+    }
+  }
+  &-info {
+    flex: 1;
+    padding: 20px 20px 0px;
+    width: 440px;
+
+    p {
+      margin-bottom: 24px;
+      font-size: 14px;
+      color: var(--black-text-color);
+      line-height: 160%;
+    }
+  }
+  &__mask {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 64px;
+    height: 133px;
+    background: url("../../images/mark.png") no-repeat center / contain;
+  }
+}

+ 141 - 87
src/views/Collections/components/DetailDialog/index.vue

@@ -1,87 +1,141 @@
-<template>
-  <ElDialog v-model="show" class="collection-detail" :width="1010 + 32">
-    <div class="collection-detail-preview">
-      <h3 tabindex="0" aria-description="">
-        Gold-inlaid Incense Burner with Cord-shaped Ears and Persian Inscription
-      </h3>
-
-      <div ref="maskRef" style="position: relative">
-        <ElImage
-          tabindex="0"
-          aria-label="Image"
-          aria-description=""
-          src="http://localhost:8080/data/Collections/Bronzes/big25.png"
-        />
-
-        <span class="collection-detail__mask" :style="maskStyle" />
-      </div>
-
-      <div
-        class="collection-detail-preview__btn"
-        tabindex="0"
-        aria-label="Button"
-      >
-        Enlarge
-      </div>
-    </div>
-
-    <div class="collection-detail-info"></div>
-  </ElDialog>
-</template>
-
-<script lang="ts" setup>
-import { ref, watch, reactive, computed, nextTick, onBeforeMount } from "vue";
-
-const props = defineProps<{
-  visible: boolean;
-}>();
-const emits = defineEmits(["update:visible"]);
-const maskStyle = reactive({
-  opacity: 1,
-  top: 0,
-  left: 0,
-});
-
-const show = computed({
-  get() {
-    return props.visible;
-  },
-  set(v: boolean) {
-    emits("update:visible", v);
-  },
-});
-
-let timer: number;
-const maskRef = ref();
-
-watch(show, (v) => {
-  if (v) {
-    nextTick(() => {
-      const maxLeft = maskRef.value.offsetWidth - 64;
-      const maxTop = maskRef.value.offsetHeight - 133;
-
-      timer = setInterval(() => {
-        maskStyle.opacity = 0;
-
-        setTimeout(() => {
-          Object.assign(maskStyle, {
-            opacity: 1,
-            top: Math.ceil(Math.random() * maxTop) + "px",
-            left: Math.ceil(Math.random() * maxLeft) + "px",
-          });
-        }, 500);
-      }, 3000);
-    });
-  } else {
-    clearInterval(timer);
-  }
-});
-
-onBeforeMount(() => {
-  clearInterval(timer);
-});
-</script>
-
-<style lang="scss">
-@import "./index.scss";
-</style>
+<template>
+  <ElDialog v-model="show" class="collection-detail" :width="1010 + 32">
+    <div v-loading="loading" class="collection-detail-preview">
+      <h3 tabindex="0" :aria-description="detail?.name">
+        {{ detail?.name }}
+      </h3>
+
+      <div ref="maskRef" style="position: relative">
+        <ElImage
+          tabindex="0"
+          aria-label="Image"
+          :aria-description="detail?.name"
+          :src="imgUrl"
+          :preview-src-list="[imgUrl]"
+        />
+
+        <span class="collection-detail__mask" :style="maskStyle" />
+      </div>
+
+      <div
+        class="collection-detail-preview__btn"
+        tabindex="0"
+        aria-label="Button"
+        @click="handlePreview"
+      >
+        Enlarge
+      </div>
+    </div>
+
+    <div v-if="detail" class="collection-detail-info">
+      <p v-if="detail.dictAge">{{ detail.dictAge }}</p>
+
+      <p v-if="detail.size">{{ detail.size }}</p>
+
+      <p v-if="detail.digest">{{ detail.digest }}</p>
+
+      <div v-for="item in rtf" :key="item.id" v-html="item.txt" />
+    </div>
+  </ElDialog>
+
+  <!-- 点击预览大图 -->
+  <ElImageViewer
+    v-if="showViewer"
+    :url-list="previewSrcList"
+    @close="closeViewer"
+  />
+</template>
+
+<script lang="ts" setup>
+import { getCollectionDetailApi, type CollectionDetail } from "@/api";
+import { getBaseURL } from "@dage/service";
+import { ref, watch, reactive, computed, nextTick, onBeforeUnmount } from "vue";
+
+const props = defineProps<{
+  visible: boolean;
+  id: number | null;
+}>();
+const emits = defineEmits(["update:visible"]);
+const baseUrl = getBaseURL();
+const maskStyle = reactive({
+  opacity: 1,
+  top: 0,
+  left: 0,
+});
+
+const show = computed({
+  get() {
+    return props.visible;
+  },
+  set(v: boolean) {
+    emits("update:visible", v);
+  },
+});
+
+let timer: number;
+const maskRef = ref();
+const loading = ref(false);
+const rtf = ref<{ id: number; txt: string }[]>([]);
+const detail = ref<CollectionDetail | null>(null);
+const imgUrl = computed(() =>
+  detail.value ? baseUrl + detail.value.thumb : ""
+);
+
+const showViewer = ref(false);
+const previewSrcList = ref<string[]>([]);
+
+const closeViewer = () => {
+  showViewer.value = false;
+};
+
+const handlePreview = () => {
+  previewSrcList.value = [imgUrl.value];
+  showViewer.value = true;
+};
+
+const getDetail = async () => {
+  if (!props.id || (detail.value && detail.value.id === props.id)) return;
+
+  try {
+    loading.value = true;
+    const data = await getCollectionDetailApi(props.id);
+    detail.value = data;
+    rtf.value = JSON.parse(data.rtf).txtArr;
+  } finally {
+    loading.value = false;
+  }
+};
+
+watch(show, (v) => {
+  if (v) {
+    getDetail();
+
+    nextTick(() => {
+      const maxLeft = maskRef.value.offsetWidth - 64;
+      const maxTop = maskRef.value.offsetHeight - 133;
+
+      timer = setInterval(() => {
+        maskStyle.opacity = 0;
+
+        setTimeout(() => {
+          Object.assign(maskStyle, {
+            opacity: 1,
+            top: Math.ceil(Math.random() * maxTop) + "px",
+            left: Math.ceil(Math.random() * maxLeft) + "px",
+          });
+        }, 500);
+      }, 3000);
+    });
+  } else {
+    clearInterval(timer);
+  }
+});
+
+onBeforeUnmount(() => {
+  clearInterval(timer);
+});
+</script>
+
+<style lang="scss">
+@import "./index.scss";
+</style>

+ 70 - 43
src/views/Collections/components/Menu.vue

@@ -1,43 +1,70 @@
-<template>
-  <ul class="collection-menu">
-    <li
-      v-for="item in NAV_LIST"
-      :key="item.type"
-      tabindex="0"
-      :aria-description="item.name"
-    >
-      <p>{{ item.name }}</p>
-    </li>
-  </ul>
-</template>
-
-<script lang="ts" setup>
-import { NAV_LIST } from "../constants";
-</script>
-
-<style lang="scss" scoped>
-.collection-menu {
-  li {
-    position: relative;
-    border-bottom: 1px solid #fff;
-
-    &:last-child {
-      border: none;
-    }
-    &:hover {
-      p {
-        background: rgba(254, 24, 24, 0.7);
-      }
-    }
-    p {
-      height: 47px;
-      line-height: 47px;
-      text-indent: 10px;
-      font-size: 14px;
-      color: white;
-      background: #181818 no-repeat right center;
-      cursor: pointer;
-    }
-  }
-}
-</style>
+<template>
+  <ul class="collection-menu">
+    <li
+      v-for="item in NAV_LIST"
+      :key="item.type"
+      tabindex="0"
+      :aria-description="item.name"
+      :class="{
+        active: $route.params.type === item.type,
+      }"
+      :style="{
+        backgroundImage: `url(${baseUrl}${
+          list.find((i) => i.type === item.name)?.thumb
+        })`,
+      }"
+      @click="
+        $router.push({ name: 'Collections', params: { type: item.type } })
+      "
+    >
+      <p>{{ item.name }}</p>
+    </li>
+  </ul>
+</template>
+
+<script lang="ts" setup>
+import { getCollectionThumbListApi, type CollectionThumbListItem } from "@/api";
+import { NAV_LIST } from "../constants";
+import { onMounted, ref } from "vue";
+import { getBaseURL } from "@dage/service";
+
+const baseUrl = getBaseURL();
+const list = ref<CollectionThumbListItem[]>([]);
+
+onMounted(() => {
+  getThumbList();
+});
+
+const getThumbList = async () => {
+  const data = await getCollectionThumbListApi();
+  list.value = data;
+};
+</script>
+
+<style lang="scss" scoped>
+.collection-menu {
+  li {
+    position: relative;
+    border-bottom: 1px solid #fff;
+    background: #181818 no-repeat 100%;
+
+    &:last-child {
+      border: none;
+    }
+    &:hover,
+    &.active {
+      p {
+        background: rgba(254, 24, 24, 0.7);
+      }
+    }
+    p {
+      height: 47px;
+      line-height: 47px;
+      text-indent: 10px;
+      font-size: 14px;
+      color: white;
+      cursor: pointer;
+    }
+  }
+}
+</style>

+ 80 - 79
src/views/Collections/index.scss

@@ -1,79 +1,80 @@
-.collections {
-  background-color: var(--white-bg);
-
-  &-banner {
-    margin-top: -60px;
-    display: block;
-    width: 100%;
-    height: 300px;
-    object-fit: cover;
-  }
-
-  &-main {
-    display: flex;
-    gap: 10px;
-    padding-bottom: 20px;
-
-    .collection-menu {
-      flex-shrink: 0;
-      width: 210px;
-    }
-    &__right {
-      flex: 1;
-      padding-left: 10px;
-      overflow: hidden;
-      border-left: 1px solid #d8d8d8;
-    }
-  }
-
-  .waterfall-list {
-    background-color: var(--white-bg);
-  }
-
-  &-item {
-    width: 100%;
-    cursor: pointer;
-
-    img {
-      display: block;
-      width: 100%;
-    }
-    &__inner {
-      position: relative;
-      padding: 20px 15px;
-
-      p:first-child {
-        font-size: 18px;
-        color: var(--black-text-color);
-        line-height: 22px;
-        padding-bottom: 5px;
-        font-weight: 700;
-      }
-      p:last-child {
-        font-size: 14px;
-        color: var(--gray-text-color);
-      }
-    }
-    &:hover {
-      box-shadow: 0 0 10px #000;
-
-      .collections-item__inner {
-        background: var(--van-primary-color);
-
-        &::before {
-          content: "";
-          position: absolute;
-          top: -59px;
-          left: 30px;
-          border-top: 30px solid transparent;
-          border-left: 18px solid transparent;
-          border-right: 18px solid transparent;
-          border-bottom: 30px solid var(--van-primary-color);
-        }
-        p {
-          color: white !important;
-        }
-      }
-    }
-  }
-}
+.collections {
+  background-color: var(--white-bg);
+
+  &-banner {
+    margin-top: -60px;
+    display: block;
+    width: 100%;
+    height: 300px;
+    object-fit: cover;
+  }
+
+  &-main {
+    display: flex;
+    gap: 10px;
+    padding-bottom: 20px;
+
+    .collection-menu {
+      flex-shrink: 0;
+      width: 210px;
+    }
+    &__right {
+      flex: 1;
+      position: relative;
+      padding-left: 10px;
+      overflow: hidden;
+      border-left: 1px solid #d8d8d8;
+    }
+  }
+
+  .waterfall-list {
+    background-color: var(--white-bg);
+  }
+
+  &-item {
+    width: 100%;
+    cursor: pointer;
+
+    img {
+      display: block;
+      width: 100%;
+    }
+    &__inner {
+      position: relative;
+      padding: 20px 15px;
+
+      p:first-child {
+        font-size: 18px;
+        color: var(--black-text-color);
+        line-height: 22px;
+        padding-bottom: 5px;
+        font-weight: 700;
+      }
+      p:last-child {
+        font-size: 14px;
+        color: var(--gray-text-color);
+      }
+    }
+    &:hover {
+      box-shadow: 0 0 10px #000;
+
+      .collections-item__inner {
+        background: var(--van-primary-color);
+
+        &::before {
+          content: "";
+          position: absolute;
+          top: -59px;
+          left: 30px;
+          border-top: 30px solid transparent;
+          border-left: 18px solid transparent;
+          border-right: 18px solid transparent;
+          border-bottom: 30px solid var(--van-primary-color);
+        }
+        p {
+          color: white !important;
+        }
+      }
+    }
+  }
+}

+ 131 - 100
src/views/Collections/index.vue

@@ -1,100 +1,131 @@
-<template>
-  <div class="collections">
-    <img
-      class="collections-banner"
-      tabindex="0"
-      aria-description="You've reached the banner area of the Collections section; this section has one image; please use the tab key to go through the content."
-      src="./images/banner.jpg"
-    />
-
-    <div class="container">
-      <Breadcrumb
-        :parents="[
-          {
-            label: 'Collections',
-            routeParams: {
-              name: 'Collections',
-              params: { type: NAV_LIST[0].type },
-            },
-          },
-        ]"
-        :cur-route="curRoute"
-      />
-
-      <div class="collections-main">
-        <Menu />
-
-        <div class="collections-main__right">
-          <Waterfall :list="list" :gutter="30" :hasAroundGutter="false">
-            <template #item="{ item, url, index }">
-              <div
-                class="collections-item"
-                aria-label="Link"
-                @click="handleItemClick(item)"
-              >
-                <img alt="" :src="url" aria-label="Image link" />
-                <div class="collections-item__inner">
-                  <p tabindex="0" aria-description="">
-                    Bronze He Wine Vessel Inscribed by Ke
-                  </p>
-                  <p tabindex="0" aria-description="">
-                    This vessel has a domed lid with a loop handle that has tiny
-                    animal faces (eyes and horns) at its ends. A smaller loop at
-                    one side of the lid is connected by a link to another loop
-                    at the top of the handle.
-                  </p>
-                </div>
-              </div>
-            </template>
-          </Waterfall>
-        </div>
-      </div>
-    </div>
-  </div>
-
-  <DetailDialog v-model:visible="visible" />
-</template>
-
-<script lang="ts" setup>
-import { Waterfall } from "vue-waterfall-plugin-next";
-import type { ViewCard } from "vue-waterfall-plugin-next/dist/types/types/waterfall";
-import { useRoute } from "vue-router";
-import { computed, ref } from "vue";
-import Breadcrumb from "@/components/Breadcrumb/index.vue";
-import { NAV_LIST } from "./constants";
-import Menu from "./components/Menu.vue";
-import DetailDialog from "./components/DetailDialog/index.vue";
-import "vue-waterfall-plugin-next/dist/style.css";
-
-const route = useRoute();
-const curRoute = computed(() =>
-  NAV_LIST.find((i) => i.type === (route.params.type as string))
-);
-const list = ref<ViewCard[]>([
-  {
-    id: "1",
-    src: "http://localhost:8080/data/Collections/Bronzes/big1.png",
-  },
-  {
-    id: "2",
-    src: "http://localhost:8080/data/Collections/Bronzes/12.png",
-  },
-  {
-    id: "3",
-    src: "http://localhost:8080/data/Collections/Bronzes/22.png",
-  },
-  {
-    id: "4",
-    src: "http://localhost:8080/data/Collections/Bronzes/2.png",
-  },
-]);
-const visible = ref(false);
-
-const handleItemClick = (item: any) => {
-  visible.value = true;
-};
-</script>
-
-<style lang="scss" scoped>
-@import "./index.scss";
-</style>
+<template>
+  <div class="collections">
+    <img
+      class="collections-banner"
+      tabindex="0"
+      aria-description="You've reached the banner area of the Collections section; this section has one image; please use the tab key to go through the content."
+      :src="bannerUrl"
+    />
+
+    <div class="container">
+      <Breadcrumb
+        :parents="[
+          {
+            label: 'Collections',
+            routeParams: {
+              name: 'Collections',
+              params: { type: NAV_LIST[0].type },
+            },
+          },
+        ]"
+        :cur-route="curRoute"
+      />
+
+      <div class="collections-main">
+        <Menu />
+
+        <div v-loading="loading" class="collections-main__right">
+          <Waterfall :list="list" :gutter="30" :hasAroundGutter="false">
+            <template #item="{ item, url }">
+              <div
+                class="collections-item"
+                aria-label="Link"
+                @click="handleItemClick(item)"
+              >
+                <img alt="" :src="url" aria-label="Image link" />
+                <div class="collections-item__inner">
+                  <p tabindex="0" :aria-description="item.name">
+                    {{ item.name }}
+                  </p>
+                  <p tabindex="0" :aria-description="item.digest">
+                    {{ item.digest }}
+                  </p>
+                </div>
+              </div>
+            </template>
+          </Waterfall>
+
+          <p v-if="noData" class="no-more">no more</p>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <DetailDialog v-model:visible="visible" :id="checkedItemId" />
+</template>
+
+<script lang="ts" setup>
+import { Waterfall } from "vue-waterfall-plugin-next";
+import type { ViewCard } from "vue-waterfall-plugin-next/dist/types/types/waterfall";
+import { useRoute } from "vue-router";
+import { computed, ref, watch } from "vue";
+import { getBaseURL } from "@dage/service";
+import { useBaseStore } from "@/stores/base";
+import Breadcrumb from "@/components/Breadcrumb/index.vue";
+import { getCollectionListApi, type CollectionListItem } from "@/api";
+import Menu from "./components/Menu.vue";
+import DetailDialog from "./components/DetailDialog/index.vue";
+import { NAV_LIST } from "./constants";
+import "vue-waterfall-plugin-next/dist/style.css";
+import { PaginationType, usePagination } from "@/utils/usePagination";
+
+const baseUrl = getBaseURL();
+const baseStore = useBaseStore();
+const bannerUrl = computed(() => {
+  return (
+    baseUrl +
+    baseStore.bannerList.find((i) => i.name === "Collections")?.thumbPc
+  );
+});
+const route = useRoute();
+const curRoute = computed(() =>
+  NAV_LIST.find((i) => i.type === (route.params.type as string))
+);
+const visible = ref(false);
+const checkedItemId = ref<number | null>(null);
+
+const {
+  list: sourceList,
+  noData,
+  loading,
+  resetParams,
+  getList,
+} = usePagination<CollectionListItem>(
+  (params) => {
+    return getCollectionListApi({
+      type: curRoute.value?.name,
+      ...params,
+    });
+  },
+  PaginationType.DEFAULT,
+  20
+);
+const list = computed<any>(() =>
+  sourceList.value.map((i: CollectionListItem) => ({
+    ...i,
+    src: baseUrl + i.thumb,
+  }))
+);
+
+const handleItemClick = (item: any) => {
+  checkedItemId.value = item.id;
+  visible.value = true;
+};
+
+watch(
+  route,
+  () => {
+    if (route.name !== "Collections") return;
+
+    resetParams();
+    getList();
+  },
+  {
+    immediate: true,
+  }
+);
+</script>
+
+<style lang="scss" scoped>
+@import "./index.scss";
+</style>

+ 92 - 92
src/views/Exhibitions/Detail/index.vue

@@ -1,92 +1,92 @@
-<template>
-  <div class="exh-detail">
-    <img
-      class="exh-detail-banner"
-      tabindex="0"
-      data-aria-viewport-area
-      aria-description="You've reached the banner area of the tertiary exhibition page; this area has one image; please use the tab key to go through the content."
-      src="https://en.capitalmuseum.org.cn/data/Exhibitions/Current/infoTop2.jpg"
-    />
-
-    <div class="exh-detail-breadcrumb">
-      <Breadcrumb
-        :parents="[
-        {
-          label: 'Exhibitions',
-          routeParams: {name: 'Exhibitions', params: {type: 1}}
-        },
-        {
-          label: NAV_LIST[0].name,
-          routeParams: {name: 'Exhibitions', params: {type: $route.query.k as string}}
-        },
-      ]"
-      />
-    </div>
-
-    <div ref="containerRef" class="container">
-      <!-- 悬浮目录 -->
-      <FloatDirectory :list="directory" />
-
-      <Overview />
-
-      <Objects ref="objectsRef" />
-
-      <Galleries ref="galleriesRef" />
-    </div>
-
-    <!-- 回到顶部 -->
-    <button
-      class="exh-detail__top-btn"
-      tabindex="0"
-      @click="backTop"
-      @keydown.enter.passive="backTop"
-    >
-      Back to top
-    </button>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { computed, ref } from "vue";
-import FloatDirectory from "@/components/FloatDirectory/index.vue";
-import Breadcrumb from "@/components/Breadcrumb/index.vue";
-import Overview from "./components/Overview.vue";
-import Objects from "./components/Objects.vue";
-import Galleries from "./components/Galleries.vue";
-import { NAV_LIST } from "../constants";
-
-const containerRef = ref();
-const objectsRef = ref();
-const galleriesRef = ref();
-
-const directory = computed(() => {
-  const containerOffsetTop = containerRef.value?.offsetTop || 0;
-
-  const stack = [
-    {
-      label: "Exhibition Overview",
-      offsetTop: containerOffsetTop,
-    },
-    {
-      label: "Exhibition Objects",
-      offsetTop:
-        (objectsRef.value?.wrapRef?.offsetTop || 0) + containerOffsetTop,
-    },
-    {
-      label: "Exhibition Galleries",
-      offsetTop:
-        (galleriesRef.value?.wrapRef?.offsetTop || 0) + containerOffsetTop,
-    },
-  ];
-
-  return stack;
-});
-
-const backTop = () => {
-  window.scrollTo({ top: 0, behavior: "smooth" });
-};
-</script>
-
-<style lang="scss">
-@import "./index.scss";
-</style>
+<template>
+  <div class="exh-detail">
+    <img
+      class="exh-detail-banner"
+      tabindex="0"
+      data-aria-viewport-area
+      aria-description="You've reached the banner area of the tertiary exhibition page; this area has one image; please use the tab key to go through the content."
+      src="https://en.capitalmuseum.org.cn/data/Exhibitions/Current/infoTop2.jpg"
+    />
+
+    <div class="exh-detail-breadcrumb">
+      <Breadcrumb
+        :parents="[
+        {
+          label: 'Exhibitions',
+          routeParams: {name: 'Exhibitions', params: {type: 1}}
+        },
+        {
+          label: NAV_LIST[Number($route.query.k as string)].name,
+          routeParams: {name: 'Exhibitions', params: {type: $route.query.k as string}}
+        },
+      ]"
+      />
+    </div>
+
+    <div ref="containerRef" class="container">
+      <!-- 悬浮目录 -->
+      <FloatDirectory :list="directory" />
+
+      <Overview />
+
+      <Objects ref="objectsRef" />
+
+      <Galleries ref="galleriesRef" />
+    </div>
+
+    <!-- 回到顶部 -->
+    <button
+      class="exh-detail__top-btn"
+      tabindex="0"
+      @click="backTop"
+      @keydown.enter.passive="backTop"
+    >
+      Back to top
+    </button>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref } from "vue";
+import FloatDirectory from "@/components/FloatDirectory/index.vue";
+import Breadcrumb from "@/components/Breadcrumb/index.vue";
+import Overview from "./components/Overview.vue";
+import Objects from "./components/Objects.vue";
+import Galleries from "./components/Galleries.vue";
+import { NAV_LIST } from "../constants";
+
+const containerRef = ref();
+const objectsRef = ref();
+const galleriesRef = ref();
+
+const directory = computed(() => {
+  const containerOffsetTop = containerRef.value?.offsetTop || 0;
+
+  const stack = [
+    {
+      label: "Exhibition Overview",
+      offsetTop: containerOffsetTop,
+    },
+    {
+      label: "Exhibition Objects",
+      offsetTop:
+        (objectsRef.value?.wrapRef?.offsetTop || 0) + containerOffsetTop,
+    },
+    {
+      label: "Exhibition Galleries",
+      offsetTop:
+        (galleriesRef.value?.wrapRef?.offsetTop || 0) + containerOffsetTop,
+    },
+  ];
+
+  return stack;
+});
+
+const backTop = () => {
+  window.scrollTo({ top: 0, behavior: "smooth" });
+};
+</script>
+
+<style lang="scss">
+@import "./index.scss";
+</style>

+ 65 - 40
src/views/Exhibitions/components/ImgItem/index.vue

@@ -1,40 +1,65 @@
-<template>
-  <div class="img-list">
-    <img src="" aria-label="Image link" :aria-description="1" />
-    <p>123456789</p>
-  </div>
-</template>
-
-<style lang="scss" scoped>
-.img-list {
-  position: relative;
-  overflow: hidden;
-  width: 33.3333%;
-  height: 393px;
-  cursor: pointer;
-
-  &:hover {
-    p {
-      bottom: 0;
-    }
-  }
-  img {
-    width: 100%;
-    height: 100%;
-  }
-  p {
-    position: absolute;
-    left: 0;
-    right: 0;
-    bottom: -300px;
-    font-weight: 700;
-    font-size: 16px;
-    padding: 15px 25px;
-    color: var(--white-text-color);
-    background: var(--topnav-bg-color);
-    transition: all linear 0.3s;
-    will-change: bottom;
-    z-index: 1;
-  }
-}
-</style>
+<template>
+  <div
+    v-for="item in list"
+    :key="item.id"
+    class="img-list"
+    @click="
+      $router.push({
+        name: 'ExhibitionsDetail',
+        query: { id: item.id, k: $route.params.type },
+      })
+    "
+  >
+    <ElImage
+      :src="baseUrl + item.thumb"
+      aria-label="Image link"
+      :aria-description="item.name"
+    />
+    <p>{{ item.name }}</p>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import type { ExhibitionsListItem } from "@/api";
+import { getBaseURL } from "@dage/service";
+
+defineProps<{
+  list: ExhibitionsListItem[];
+}>();
+
+const baseUrl = getBaseURL();
+</script>
+
+<style lang="scss" scoped>
+.img-list {
+  position: relative;
+  overflow: hidden;
+  width: 33.3333%;
+  height: 393px;
+  cursor: pointer;
+
+  &:hover {
+    p {
+      bottom: 0;
+    }
+  }
+  img {
+    width: 100%;
+    height: 100%;
+  }
+  p {
+    position: absolute;
+    left: 0;
+    right: 0;
+    bottom: -300px;
+    font-weight: 700;
+    font-size: 16px;
+    padding: 15px 25px;
+    color: var(--white-text-color);
+    background: var(--topnav-bg-color);
+    transition: all linear 0.3s;
+    will-change: bottom;
+    z-index: 1;
+  }
+}
+</style>

+ 70 - 55
src/views/Exhibitions/components/ListItem/index.vue

@@ -1,55 +1,70 @@
-<template>
-  <div
-    class="list-item"
-    @click="
-      $router.push({ name: 'ExhibitionsDetail', query: { id: 100, k: 1 } })
-    "
-  >
-    <img src="" aria-label="Image link" :aria-description="1" />
-
-    <div class="list-item__inner" aria-label="Link">
-      <p class="list-item__title">Splendid Central Axis of Beijing</p>
-      <p class="list-item__content">
-        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>
-.list-item {
-  display: flex;
-  height: 240px;
-  background: var(--white-bg);
-  color: var(--black-text-color);
-  cursor: pointer;
-
-  > img {
-    width: 240px;
-    height: 100%;
-  }
-  &__inner {
-    flex: 1;
-    padding: 0 25px;
-    border: 1px solid #c7c7c7;
-    border-left: 0;
-  }
-  &__title {
-    font-size: 18px;
-    line-height: 22px;
-    padding: 16px 0;
-    margin-bottom: 13px;
-    font-weight: bold;
-  }
-  &__content {
-    font-size: 14px;
-    line-height: 20px;
-    color: var(--gray-text-color);
-  }
-}
-</style>
+<template>
+  <div
+    v-for="item in list"
+    :key="item.id"
+    class="list-item"
+    @click="
+      $router.push({
+        name: 'ExhibitionsDetail',
+        query: { id: item.id, k: $route.params.type },
+      })
+    "
+  >
+    <ElImage
+      :src="baseUrl + item.thumb"
+      aria-label="Image link"
+      :aria-description="item.name"
+    />
+
+    <div class="list-item__inner" aria-label="Link">
+      <p class="list-item__title">{{ item.name }}</p>
+      <p class="list-item__content">
+        {{ item.digest }}
+      </p>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import type { ExhibitionsListItem } from "@/api";
+import { getBaseURL } from "@dage/service";
+
+defineProps<{
+  list: ExhibitionsListItem[];
+}>();
+
+const baseUrl = getBaseURL();
+</script>
+
+<style lang="scss" scoped>
+.list-item {
+  display: flex;
+  height: 240px;
+  background: var(--white-bg);
+  color: var(--black-text-color);
+  cursor: pointer;
+
+  > img {
+    width: 240px;
+    height: 100%;
+  }
+  &__inner {
+    flex: 1;
+    padding: 0 25px;
+    border: 1px solid #c7c7c7;
+    border-left: 0;
+  }
+  &__title {
+    font-size: 18px;
+    line-height: 22px;
+    padding: 16px 0;
+    margin-bottom: 13px;
+    font-weight: bold;
+  }
+  &__content {
+    font-size: 14px;
+    line-height: 20px;
+    color: var(--gray-text-color);
+  }
+}
+</style>

+ 88 - 86
src/views/Exhibitions/index.scss

@@ -1,86 +1,88 @@
-.exhibitions {
-  .container {
-    width: 1180px;
-  }
-
-  &-banner {
-    margin-top: -60px;
-    display: block;
-    width: 100%;
-    height: 300px;
-    object-fit: cover;
-  }
-
-  .breadcrumb {
-    margin-top: 0;
-  }
-
-  &-filter {
-    display: flex;
-    align-items: center;
-
-    &__input {
-      display: flex;
-      align-items: center;
-      height: 48px;
-      background: var(--white-bg);
-      border: 1px solid #c7c7c7;
-
-      input {
-        padding: 0px 10px;
-        width: 500px;
-        height: 100%;
-        color: var(--black-text-color);
-        border: none;
-        outline: none;
-        background: transparent;
-      }
-      .svg-icon {
-        padding: 0 10px;
-        width: 30px;
-        height: 30px;
-        cursor: pointer;
-      }
-    }
-    &__year {
-      margin-left: 30px;
-      padding-left: 10px;
-      width: 180px;
-      height: 48px;
-      color: var(--black-text-color);
-      font-size: 18px;
-      background: var(--white-bg);
-      border-color: #c0c4cc;
-      cursor: pointer;
-
-      option {
-        cursor: pointer;
-        color: var(--black-text-color);
-        font-size: 18px;
-      }
-    }
-    &-right {
-      display: flex;
-
-      img {
-        display: block;
-        cursor: pointer;
-      }
-    }
-  }
-  &-list {
-    margin: 20px 0 40px;
-  }
-  &__more {
-    height: 38px;
-    width: 160px;
-    margin: 0 auto 60px;
-    color: var(--black-text-color);
-    border: 1px solid var(--black-text-color);
-    font-size: 16px;
-    line-height: 38px;
-    text-align: center;
-    font-weight: bold;
-    cursor: pointer;
-  }
-}
+.exhibitions {
+  .container {
+    width: 1180px;
+  }
+
+  &-banner {
+    margin-top: -60px;
+    display: block;
+    width: 100%;
+    height: 300px;
+    object-fit: cover;
+  }
+
+  .breadcrumb {
+    margin-top: 0;
+  }
+
+  &-filter {
+    display: flex;
+    align-items: center;
+
+    &__input {
+      display: flex;
+      align-items: center;
+      height: 48px;
+      background: var(--white-bg);
+      border: 1px solid #c7c7c7;
+
+      input {
+        padding: 0px 10px;
+        width: 500px;
+        height: 100%;
+        color: var(--black-text-color);
+        border: none;
+        outline: none;
+        background: transparent;
+      }
+      .svg-icon {
+        padding: 0 10px;
+        width: 30px;
+        height: 30px;
+        cursor: pointer;
+      }
+    }
+    &__year {
+      margin-left: 30px;
+      padding-left: 10px;
+      width: 180px;
+      height: 48px;
+      color: var(--black-text-color);
+      font-size: 18px;
+      background: var(--white-bg);
+      border-color: #c0c4cc;
+      cursor: pointer;
+
+      option {
+        cursor: pointer;
+        color: var(--black-text-color);
+        font-size: 18px;
+      }
+    }
+    &-right {
+      display: flex;
+
+      img {
+        display: block;
+        cursor: pointer;
+      }
+    }
+  }
+  &-list {
+    position: relative;
+    margin: 20px 0 40px;
+    min-height: 240px;
+  }
+  &__more {
+    height: 38px;
+    width: 160px;
+    margin: 0 auto 60px;
+    color: var(--black-text-color);
+    border: 1px solid var(--black-text-color);
+    font-size: 16px;
+    line-height: 38px;
+    text-align: center;
+    font-weight: bold;
+    cursor: pointer;
+  }
+}

+ 201 - 129
src/views/Exhibitions/index.vue

@@ -1,129 +1,201 @@
-<template>
-  <div class="exhibitions">
-    <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="exhibitions-banner"
-      src="@/assets/images/Exhibitions/banner_1.jpg"
-    />
-
-    <PageNav :list="NAV_LIST" :cur-route-name="curRoute.name" />
-
-    <div class="container">
-      <Breadcrumb
-        :parents="[
-          {
-            label: 'Exhibitions',
-            routeParams: {
-              name: 'Exhibitions',
-              params: { type: 1 },
-            },
-          },
-        ]"
-        :cur-route="curRoute"
-      />
-
-      <PageTitle :title="curRoute?.name" />
-
-      <div
-        class="exhibitions-filter"
-        data-aria-interaction-area
-        tabindex="2"
-        aria-description="You've reached search box under the Exhibitions page; please use the tab key to go through the content."
-      >
-        <div class="exhibitions-filter__input">
-          <input
-            type="text"
-            autocomplete="off"
-            v-model="keyword"
-            tabindex="3"
-            :aria-description="keyword || 'search'"
-          />
-
-          <SvgIcon name="search" color="var(--van-primary-color)" />
-        </div>
-
-        <!-- 使用el-select组件会无法在选择年份过程中得到选项的无障碍信息,所以改用原生元素 -->
-        <select
-          v-model="year"
-          tabindex="4"
-          class="exhibitions-filter__year"
-          aria-label="Select"
-          aria-description="Select Year"
-        >
-          <option value="">Select Year</option>
-          <option value="2021">2021</option>
-          <option value="2020">2020</option>
-          <option value="2019">2019</option>
-          <option value="2018">2018</option>
-          <option value="2017">2017</option>
-          <option value="2016">2016</option>
-          <option value="2015">2015</option>
-        </select>
-
-        <div style="flex: 1" />
-
-        <div class="exhibitions-filter-right">
-          <img
-            :src="mode === MODE.LIST ? ActListModeIcon : ListModeIcon"
-            alt="Button: List mode"
-            aria-label="Button"
-            aria-description="List mode"
-            @click="mode = MODE.LIST"
-            @keydown.enter.passive="mode = MODE.LIST"
-          />
-          <img
-            :src="mode === MODE.IMG ? ActImgModeIcon : ImgModeIcon"
-            alt="Button: Image mode"
-            aria-label="Button"
-            aria-description="Image mode"
-            @click="mode = MODE.IMG"
-            @keydown.enter.passive="mode = MODE.IMG"
-          />
-        </div>
-      </div>
-
-      <div
-        class="exhibitions-list"
-        data-aria-viewport-area
-        :aria-description="`You've reached the content area of the ${curRoute.name} page. To browse the content, please use the tab key.`"
-      >
-        <ListItem v-if="mode === MODE.LIST" />
-        <ImgItem v-else />
-      </div>
-
-      <div class="exhibitions__more">Show More</div>
-    </div>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { useRoute } from "vue-router";
-import { computed, ref } from "vue";
-import PageNav from "@/components/PageNav/index.vue";
-import PageTitle from "@/components/PageTitle/index.vue";
-import ListModeIcon from "@/assets/images/Exhibitions/cut1.png";
-import ActListModeIcon from "@/assets/images/Exhibitions/cut1Ac.png";
-import ImgModeIcon from "@/assets/images/Exhibitions/cut2.png";
-import ActImgModeIcon from "@/assets/images/Exhibitions/cut2Ac.png";
-import Breadcrumb from "@/components/Breadcrumb/index.vue";
-import ListItem from "./components/ListItem/index.vue";
-import ImgItem from "./components/ImgItem/index.vue";
-import { NAV_LIST } from "./constants";
-
-enum MODE {
-  LIST = 0,
-  IMG = 1,
-}
-
-const route = useRoute();
-const curRoute = computed(() => NAV_LIST[Number(route.params.type) - 1]);
-
-const keyword = ref("");
-const year = ref("");
-
-const mode = ref(MODE.LIST);
-</script>
-
-<style lang="scss" scoped>
-@import "./index.scss";
-</style>
+<template>
+  <div class="exhibitions">
+    <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="exhibitions-banner"
+      :src="bannerUrl"
+    />
+
+    <PageNav :list="NAV_LIST" :cur-route-name="curRoute.name" />
+
+    <div class="container">
+      <Breadcrumb
+        :parents="[
+          {
+            label: 'Exhibitions',
+            routeParams: {
+              name: 'Exhibitions',
+              params: { type: 1 },
+            },
+          },
+        ]"
+        :cur-route="curRoute"
+      />
+
+      <PageTitle :title="curRoute?.name" />
+
+      <div
+        class="exhibitions-filter"
+        data-aria-interaction-area
+        tabindex="0"
+        aria-description="You've reached search box under the Exhibitions page; please use the tab key to go through the content."
+      >
+        <div class="exhibitions-filter__input">
+          <input
+            type="text"
+            autocomplete="off"
+            v-model="keyword"
+            tabindex="0"
+            :aria-description="keyword || 'search'"
+            @keydown.enter.passive="handleSearch"
+          />
+
+          <SvgIcon name="search" color="var(--van-primary-color)" />
+        </div>
+
+        <!-- 使用el-select组件会无法在选择年份过程中得到选项的无障碍信息,所以改用原生元素 -->
+        <select
+          v-model="year"
+          tabindex="0"
+          class="exhibitions-filter__year"
+          aria-label="Select"
+          aria-description="Select Year"
+          @change="handleSearch"
+        >
+          <option value="">Select Year</option>
+          <option v-for="y in yearArr" :key="y" :value="y">{{ y }}</option>
+        </select>
+
+        <div style="flex: 1" />
+
+        <div class="exhibitions-filter-right">
+          <img
+            :src="mode === MODE.LIST ? ActListModeIcon : ListModeIcon"
+            alt="Button: List mode"
+            aria-label="Button"
+            aria-description="List mode"
+            @click="mode = MODE.LIST"
+            @keydown.enter.passive="mode = MODE.LIST"
+          />
+          <img
+            :src="mode === MODE.IMG ? ActImgModeIcon : ImgModeIcon"
+            alt="Button: Image mode"
+            aria-label="Button"
+            aria-description="Image mode"
+            @click="mode = MODE.IMG"
+            @keydown.enter.passive="mode = MODE.IMG"
+          />
+        </div>
+      </div>
+
+      <div
+        v-loading="loading"
+        class="exhibitions-list"
+        data-aria-viewport-area
+        :aria-description="`You've reached the content area of the ${curRoute.name} page. To browse the content, please use the tab key.`"
+      >
+        <ListItem v-if="mode === MODE.LIST" :list="list" />
+        <ImgItem v-else :list="list" />
+
+        <p v-if="noData" class="no-more">no more</p>
+      </div>
+
+      <div v-if="noMore" class="exhibitions__more" @click="handleMore">
+        Show More
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { useRoute } from "vue-router";
+import { computed, onMounted, ref, watch } from "vue";
+import PageNav from "@/components/PageNav/index.vue";
+import PageTitle from "@/components/PageTitle/index.vue";
+import ListModeIcon from "@/assets/images/Exhibitions/cut1.png";
+import ActListModeIcon from "@/assets/images/Exhibitions/cut1Ac.png";
+import ImgModeIcon from "@/assets/images/Exhibitions/cut2.png";
+import ActImgModeIcon from "@/assets/images/Exhibitions/cut2Ac.png";
+import Breadcrumb from "@/components/Breadcrumb/index.vue";
+import ListItem from "./components/ListItem/index.vue";
+import ImgItem from "./components/ImgItem/index.vue";
+import { NAV_LIST } from "./constants";
+import { getBaseURL } from "@dage/service";
+import { useBaseStore } from "@/stores/base";
+import {
+  getExhibitionListApi,
+  type ExhibitionsListItem,
+  type GetExhibitionListParams,
+} from "@/api";
+import { PaginationType, usePagination } from "@/utils/usePagination";
+
+enum MODE {
+  LIST = 0,
+  IMG = 1,
+}
+
+const baseUrl = getBaseURL();
+const baseStore = useBaseStore();
+const bannerUrl = computed(() => {
+  return (
+    baseUrl + baseStore.bannerList.find((i) => i.name === "Exhibition")?.thumbPc
+  );
+});
+
+const route = useRoute();
+const curRoute = computed(() => NAV_LIST[Number(route.params.type) - 1]);
+
+const curYear = new Date().getFullYear();
+const yearArr: number[] = [];
+for (let year = curYear; year >= 2015; year--) {
+  yearArr.push(year);
+}
+
+const keyword = ref("");
+const year = ref("");
+
+const mode = ref(MODE.LIST);
+
+const { pageNum, noMore, loading, list, noData, getList, resetParams } =
+  usePagination<ExhibitionsListItem>((params) => {
+    const _params: GetExhibitionListParams = {
+      searchKey: keyword.value,
+      year: year.value,
+      ...params,
+    };
+
+    switch (route.params.type) {
+      case "1":
+        _params.isCurrent = 1;
+        _params.addrType = "inland";
+        break;
+      case "2":
+        _params.type = "long";
+        break;
+      case "3":
+        _params.isCurrent = 0;
+        _params.addrType = "inland";
+        break;
+      case "4":
+        _params.addrType = "foreign";
+        break;
+    }
+
+    return getExhibitionListApi(_params);
+  }, PaginationType.CONCAT);
+
+onMounted(() => {
+  getList();
+});
+
+const handleMore = () => {
+  pageNum.value++;
+  getList();
+};
+
+const handleSearch = async () => {
+  pageNum.value = 1;
+  await getList();
+};
+
+watch(route, () => {
+  if (route.name === "Exhibitions") {
+    resetParams();
+    getList();
+  }
+});
+</script>
+
+<style lang="scss" scoped>
+@import "./index.scss";
+</style>

+ 85 - 67
src/views/JoinSupport/Give/detail.vue

@@ -1,67 +1,85 @@
-<template>
-  <div class="give-detail">
-    <div class="give-detail__main">
-      <h1 tabindex="0">Corporations & Institutions</h1>
-
-      <div class="give-detail-contact">
-        <p tabindex="1">86 (10) 63370488</p>
-        <p tabindex="1">86 (10) 63363388 to extension 6223</p>
-      </div>
-    </div>
-  </div>
-</template>
-
-<style lang="scss" scoped>
-.give-detail {
-  position: relative;
-  padding-bottom: 100px;
-
-  &__main {
-    position: relative;
-    padding: 30px 30px 100px;
-    background-color: rgb(31, 29, 29);
-    z-index: 2;
-
-    > h1 {
-      position: relative;
-      padding: 0 0 20px 20px;
-      font-size: 24px;
-      line-height: 26px;
-      color: white;
-
-      &::before {
-        content: "";
-        position: absolute;
-        top: 12px;
-        left: 0;
-        width: 11px;
-        height: 11px;
-        background: var(--van-primary-color);
-      }
-    }
-  }
-  &-contact {
-    position: absolute;
-    left: 23px;
-    bottom: -60px;
-    padding: 76px 0px 0px 112px;
-    width: 529px;
-    height: 181px;
-    color: white;
-    font-size: 24px;
-    line-height: 36px;
-    font-weight: bold;
-    background: url("../images/dianhua.png") no-repeat center / contain;
-  }
-  &::after {
-    content: "";
-    position: absolute;
-    top: 0;
-    left: 50%;
-    bottom: 0;
-    width: 100vw;
-    background: url("../images/bg4.gif") no-repeat center bottom / 100% auto;
-    transform: translateX(-50%);
-  }
-}
-</style>
+<template>
+  <div class="give-detail">
+    <div class="give-detail__main">
+      <h1 tabindex="0">{{ detail?.h3 }}</h1>
+
+      <div class="give-detail__txt" v-html="detail?.txt" />
+
+      <div class="give-detail-contact">
+        <p tabindex="1">86 (10) 63370488</p>
+        <p tabindex="1">86 (10) 63363388 to extension 6223</p>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { JoinSupport } from "@/data";
+import { useRoute } from "vue-router";
+
+const route = useRoute();
+const detail = JoinSupport.Give.find((i) => i.id === Number(route.query.id));
+</script>
+
+<style lang="scss" scoped>
+.give-detail {
+  position: relative;
+  color: #ccc;
+  padding-bottom: 100px;
+
+  &__txt {
+    :deep(p) {
+      font-size: 18px;
+      line-height: 26px;
+      margin-bottom: 60px;
+    }
+  }
+  &__main {
+    position: relative;
+    padding: 30px 30px 100px;
+    background-color: rgb(31, 29, 29);
+    z-index: 2;
+
+    > h1 {
+      position: relative;
+      padding: 0 0 20px 20px;
+      font-size: 24px;
+      line-height: 26px;
+      color: white;
+
+      &::before {
+        content: "";
+        position: absolute;
+        top: 12px;
+        left: 0;
+        width: 11px;
+        height: 11px;
+        background: var(--van-primary-color);
+      }
+    }
+  }
+  &-contact {
+    position: absolute;
+    left: 23px;
+    bottom: -60px;
+    padding: 76px 0px 0px 112px;
+    width: 529px;
+    height: 181px;
+    color: white;
+    font-size: 24px;
+    line-height: 36px;
+    font-weight: bold;
+    background: url("../images/dianhua.png") no-repeat center / contain;
+  }
+  &::after {
+    content: "";
+    position: absolute;
+    top: 0;
+    left: 50%;
+    bottom: 0;
+    width: 100vw;
+    background: url("../images/bg4.gif") no-repeat center bottom / 100% auto;
+    transform: translateX(-50%);
+  }
+}
+</style>

+ 57 - 52
src/views/JoinSupport/Give/index.vue

@@ -1,52 +1,57 @@
-<template>
-  <div
-    class="give"
-    tabindex="0"
-    data-aria-viewport-area
-    aria-description="You've reached the content area of the Ways to Give page, please use the tab key to go through the content."
-  >
-    <div class="give-row individuals">
-      <div class="give-row__banner">
-        <div
-          tabindex="0"
-          aria-label="Link"
-          @click="$router.push({ name: 'GiveDetail' })"
-        >
-          Individuals
-        </div>
-      </div>
-
-      <div class="give-row__img">
-        <p>
-          Since the establishment of Capital Museum, we have got sufficient
-          supports from people from all walks of life. Due to the limited space,
-          here we only give a few examples, and we show our respect to all units
-          and individuals who have supported the development of Chinese museums
-          and have given help and supports to Capital Museum.
-        </p>
-      </div>
-    </div>
-
-    <div class="give-row institutions">
-      <div class="give-row__img">
-        <p>
-          The century-old Beijing Match Factory donated to Capital Museum a
-          large number of files and real objects which record the development of
-          Beijing's light industry in modern history, including 518 sets of high
-          standard real samples of matches, 40 volumes data, and 3 volumes of
-          text materials of sparks, etc.
-        </p>
-      </div>
-
-      <div class="give-row__banner">
-        <div tabindex="0" aria-label="Link" style="width: 370px">
-          Corporations & Institutions
-        </div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<style lang="scss" scoped>
-@import "./index.scss";
-</style>
+<template>
+  <div
+    class="give"
+    tabindex="0"
+    data-aria-viewport-area
+    aria-description="You've reached the content area of the Ways to Give page, please use the tab key to go through the content."
+  >
+    <div class="give-row individuals">
+      <div class="give-row__banner">
+        <div
+          tabindex="0"
+          aria-label="Link"
+          @click="$router.push({ name: 'GiveDetail', query: { id: 4 } })"
+        >
+          Individuals
+        </div>
+      </div>
+
+      <div class="give-row__img">
+        <p @click="$router.push({ name: 'GiveDetail', query: { id: 4 } })">
+          Since the establishment of Capital Museum, we have got sufficient
+          supports from people from all walks of life. Due to the limited space,
+          here we only give a few examples, and we show our respect to all units
+          and individuals who have supported the development of Chinese museums
+          and have given help and supports to Capital Museum.
+        </p>
+      </div>
+    </div>
+
+    <div class="give-row institutions">
+      <div class="give-row__img">
+        <p @click="$router.push({ name: 'GiveDetail', query: { id: 5 } })">
+          The century-old Beijing Match Factory donated to Capital Museum a
+          large number of files and real objects which record the development of
+          Beijing's light industry in modern history, including 518 sets of high
+          standard real samples of matches, 40 volumes data, and 3 volumes of
+          text materials of sparks, etc.
+        </p>
+      </div>
+
+      <div class="give-row__banner">
+        <div
+          tabindex="0"
+          aria-label="Link"
+          style="width: 370px"
+          @click="$router.push({ name: 'GiveDetail', query: { id: 5 } })"
+        >
+          Corporations & Institutions
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+@import "./index.scss";
+</style>

+ 96 - 20
src/views/JoinSupport/Volunteer/detail.vue

@@ -1,20 +1,96 @@
-<template>
-  <div class="volunteer-detail">
-    <h1 aria-label="Volunteer Team Introduction">
-      Volunteer Team Introduction
-    </h1>
-  </div>
-</template>
-
-<style lang="scss" scoped>
-.volunteer-detail {
-  padding-bottom: 100px;
-
-  h1 {
-    padding: 20px 0;
-    text-align: center;
-    font-size: 30px;
-    font-weight: bold;
-  }
-}
-</style>
+<template>
+  <div class="volunteer-detail">
+    <h1 :aria-label="detail?.h3">
+      {{ detail?.h3 }}
+    </h1>
+
+    <div v-html="detail?.txt" class="volunteer-detail__txt" tabindex="0" />
+
+    <template v-if="detail?.card">
+      <div
+        v-for="item in detail.card"
+        :key="item.h2"
+        class="volunteer-detail-card"
+      >
+        <h2 tabindex="0">{{ item.h2 }}</h2>
+        <div class="volunteer-detail-card__inner" v-html="item.pp" />
+      </div>
+    </template>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { JoinSupport } from "@/data";
+import { useRoute } from "vue-router";
+
+const route = useRoute();
+const detail = JoinSupport.Volunteer.find(
+  (i) => i.id === Number(route.query.id)
+);
+</script>
+
+<style lang="scss" scoped>
+.volunteer-detail {
+  padding-bottom: 100px;
+  color: #333;
+
+  h1 {
+    padding-top: 20px;
+    text-align: center;
+    font-size: 30px;
+    font-weight: bold;
+  }
+  &__txt {
+    font-size: 18px;
+    line-height: 30px;
+
+    :deep(p) {
+      margin-bottom: 22px;
+    }
+    :deep(.txt-title) {
+      margin-top: 50px;
+      font-weight: bold;
+    }
+  }
+  &-card {
+    position: relative;
+    margin: 20px 0 50px;
+    padding: 40px 130px 40px 40px;
+    background: var(--white-bg);
+    box-shadow: 0 1px 3px 1px #ccc;
+
+    &::after {
+      content: "";
+      position: absolute;
+      top: 50%;
+      right: 30px;
+      width: 30px;
+      height: 30px;
+      border-radius: 50%;
+      border: 2px solid var(--van-primary-color);
+      transform: translateY(-50%);
+    }
+    h2 {
+      position: relative;
+      line-height: 40px;
+      font-size: 30px;
+      font-weight: bold;
+
+      &::after {
+        content: "";
+        position: absolute;
+        left: 0;
+        bottom: -2px;
+        width: 65px;
+        height: 3px;
+        background: var(--van-primary-color);
+      }
+    }
+    :deep(p) {
+      margin-top: 22px;
+      font-size: 18px;
+      line-height: 30px;
+    }
+  }
+}
+</style>

+ 82 - 72
src/views/JoinSupport/index.vue

@@ -1,72 +1,82 @@
-<template>
-  <div class="join-support">
-    <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="join-support-banner"
-      src="./images/banner.jpg"
-    />
-
-    <PageNav
-      :active-index="activeNavIndex"
-      :list="NAV_LIST"
-      :cur-route-name="curRoute?.name"
-    />
-
-    <div class="container">
-      <Breadcrumb :parents="parentRoutes" :cur-route="curRoute" />
-
-      <RouterView />
-    </div>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { useRoute } from "vue-router";
-import { computed } from "vue";
-import PageNav from "@/components/PageNav/index.vue";
-import Breadcrumb from "@/components/Breadcrumb/index.vue";
-import type { BreadcrumbParent } from "@/components/Breadcrumb/constants";
-import { NAV_LIST, VOLUNTEER_DETAIL_NAV } from "./constants";
-
-const route = useRoute();
-const activeNavIndex = computed(() => {
-  if (route.name === "VolunteerDetail") return 0;
-  if (route.name === "GiveDetail") return 1;
-
-  return undefined;
-});
-const parentRoutes = computed(() => {
-  const stack: BreadcrumbParent[] = [
-    {
-      label: "Join & Support",
-      routeParams: {
-        name: "JoinSupport",
-      },
-    },
-  ];
-
-  if (["VolunteerDetail", "GiveDetail"].includes(route.name as string)) {
-    const item = NAV_LIST[route.name === "VolunteerDetail" ? 0 : 1];
-
-    stack.push({
-      label: item.name,
-      routeParams: item.routeParams,
-    });
-  }
-
-  return stack;
-});
-const curRoute = computed(() => {
-  if (route.name === "VolunteerDetail") {
-    return {
-      name: VOLUNTEER_DETAIL_NAV[Number(route.query.id || 1) - 1].label,
-    };
-  }
-
-  return NAV_LIST.find((item) => item.routeParams.name === route.name);
-});
-</script>
-
-<style lang="scss" scoped>
-@import "./index.scss";
-</style>
+<template>
+  <div class="join-support">
+    <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="join-support-banner"
+      :src="bannerUrl"
+    />
+
+    <PageNav
+      :active-index="activeNavIndex"
+      :list="NAV_LIST"
+      :cur-route-name="curRoute?.name"
+    />
+
+    <div class="container">
+      <Breadcrumb :parents="parentRoutes" :cur-route="curRoute" />
+
+      <RouterView />
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { useRoute } from "vue-router";
+import { computed } from "vue";
+import PageNav from "@/components/PageNav/index.vue";
+import Breadcrumb from "@/components/Breadcrumb/index.vue";
+import type { BreadcrumbParent } from "@/components/Breadcrumb/constants";
+import { NAV_LIST, VOLUNTEER_DETAIL_NAV } from "./constants";
+import { getBaseURL } from "@dage/service";
+import { useBaseStore } from "@/stores/base";
+
+const baseUrl = getBaseURL();
+const baseStore = useBaseStore();
+const bannerUrl = computed(() => {
+  return (
+    baseUrl +
+    baseStore.bannerList.find((i) => i.name === "Join&Support")?.thumbPc
+  );
+});
+const route = useRoute();
+const activeNavIndex = computed(() => {
+  if (route.name === "VolunteerDetail") return 0;
+  if (route.name === "GiveDetail") return 1;
+
+  return undefined;
+});
+const parentRoutes = computed(() => {
+  const stack: BreadcrumbParent[] = [
+    {
+      label: "Join & Support",
+      routeParams: {
+        name: "JoinSupport",
+      },
+    },
+  ];
+
+  if (["VolunteerDetail", "GiveDetail"].includes(route.name as string)) {
+    const item = NAV_LIST[route.name === "VolunteerDetail" ? 0 : 1];
+
+    stack.push({
+      label: item.name,
+      routeParams: item.routeParams,
+    });
+  }
+
+  return stack;
+});
+const curRoute = computed(() => {
+  if (route.name === "VolunteerDetail") {
+    return {
+      name: VOLUNTEER_DETAIL_NAV[Number(route.query.id || 1) - 1].label,
+    };
+  }
+
+  return NAV_LIST.find((item) => item.routeParams.name === route.name);
+});
+</script>
+
+<style lang="scss" scoped>
+@import "./index.scss";
+</style>

+ 20 - 21
src/views/LearnEngage/List/index.vue

@@ -2,14 +2,14 @@
   <div v-loading="loading" class="learn-engage-container">
     <Item v-for="item in list" :key="item.id" :item="item" />
 
-    <p v-if="!total && !loading" class="no-more">no more</p>
+    <p v-if="noData" class="no-more">no more</p>
   </div>
 
   <div
     v-if="total"
     style="display: flex; justify-content: center; padding: 30px 0"
   >
-    <Pagination :total="total" />
+    <Pagination :total="total" :page-size="PAGE_SIZE" @change="handlePage" />
   </div>
 </template>
 
@@ -18,29 +18,28 @@ import { ref, watch } from "vue";
 import { getLearnPageListApi, type LearnPageItem } from "@/api";
 import Pagination from "@/components/Pagination/index.vue";
 import Item from "../components/Item.vue";
+import { PaginationType, usePagination } from "@/utils/usePagination";
 
 const props = defineProps<{
   curType?: string;
 }>();
-const pageNum = ref(1);
-const total = ref(0);
-const loading = ref(false);
-const list = ref<LearnPageItem[]>([]);
-
-const getList = async () => {
-  try {
-    loading.value = true;
-    const data = await getLearnPageListApi({
-      pageNum: pageNum.value,
-      pageSize: 9,
-      type: props.curType,
-    });
-
-    total.value = data.total;
-    list.value = data.records;
-  } finally {
-    loading.value = false;
-  }
+
+const PAGE_SIZE = 9;
+const { pageNum, total, loading, list, noData, getList } =
+  usePagination<LearnPageItem>(
+    (params) => {
+      return getLearnPageListApi({
+        type: props.curType,
+        ...params,
+      });
+    },
+    PaginationType.DEFAULT,
+    PAGE_SIZE
+  );
+
+const handlePage = (page: number) => {
+  pageNum.value = page;
+  getList();
 };
 
 watch(

+ 63 - 62
src/views/Publications/Catalogues/index.scss

@@ -1,62 +1,63 @@
-.catalogues {
-  overflow: hidden;
-  margin-bottom: 30px;
-
-  &__border {
-    display: block;
-    margin-top: 20px;
-    width: 100%;
-  }
-  &__list {
-    display: flex;
-    flex-wrap: wrap;
-    margin: -7.5px;
-  }
-  &-item {
-    position: relative;
-    margin: 7.5px;
-    width: calc(25% - 15px);
-    height: 375px;
-
-    &:hover {
-      .catalogues-item__ft {
-        height: 50%;
-        opacity: 1;
-      }
-    }
-    img {
-      width: inherit;
-      height: inherit;
-      object-fit: cover;
-      object-position: center;
-    }
-    &__ft {
-      position: absolute;
-      left: 0;
-      right: 0;
-      bottom: 0;
-      display: flex;
-      flex-direction: column;
-      align-items: center;
-      justify-content: center;
-      gap: 20px;
-      height: 0;
-      color: var(--white-text-color);
-      background: var(--topnav-bg-color);
-      cursor: pointer;
-      font-weight: 700;
-      font-size: 20px;
-      opacity: 0;
-      transition: all 0.6s ease 0s;
-      z-index: 1;
-
-      img {
-        width: 36px;
-        height: 30px;
-      }
-      span {
-        width: 175px;
-      }
-    }
-  }
-}
+.catalogues {
+  overflow: hidden;
+  margin-bottom: 30px;
+
+  &__border {
+    display: block;
+    margin-top: 20px;
+    width: 100%;
+  }
+  &__list {
+    display: flex;
+    flex-wrap: wrap;
+    margin: -7.5px;
+    min-height: 375px;
+  }
+  &-item {
+    position: relative;
+    margin: 7.5px;
+    width: calc(25% - 15px);
+    height: 375px;
+
+    &:hover {
+      .catalogues-item__ft {
+        height: 50%;
+        opacity: 1;
+      }
+    }
+    img {
+      width: inherit;
+      height: inherit;
+      object-fit: cover;
+      object-position: center;
+    }
+    &__ft {
+      position: absolute;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      gap: 20px;
+      height: 0;
+      color: var(--white-text-color);
+      background: var(--topnav-bg-color);
+      cursor: pointer;
+      font-weight: 700;
+      font-size: 20px;
+      opacity: 0;
+      transition: all 0.6s ease 0s;
+      z-index: 1;
+
+      img {
+        width: 36px;
+        height: 30px;
+      }
+      span {
+        width: 175px;
+      }
+    }
+  }
+}

+ 73 - 27
src/views/Publications/Catalogues/index.vue

@@ -1,27 +1,73 @@
-<template>
-  <div class="catalogues">
-    <img class="catalogues__border" :src="HengImg" />
-
-    <ul class="catalogues__list">
-      <li v-for="key in 8" :key="key" class="catalogues-item">
-        <ElImage
-          src="https://en.capitalmuseum.org.cn/data/Publications/Exhibition/1.jpg"
-        />
-
-        <a href="" target="_blank" class="catalogues-item__ft">
-          <img :src="HrefImg" />
-          <span>Click here to open the catalogue</span>
-        </a>
-      </li>
-    </ul>
-  </div>
-</template>
-
-<script setup lang="ts">
-import HengImg from "../images/heng.png";
-import HrefImg from "../images/href.png";
-</script>
-
-<style lang="scss" scoped>
-@import "./index.scss";
-</style>
+<template>
+  <div class="catalogues container">
+    <img class="catalogues__border" :src="HengImg" />
+
+    <ul class="catalogues__list">
+      <li v-for="item in list" :key="item.id" class="catalogues-item">
+        <ElImage :src="baseUrl + item.thumb" />
+
+        <a
+          :href="baseUrl + item.filePath"
+          target="_blank"
+          class="catalogues-item__ft"
+        >
+          <img :src="HrefImg" />
+          <span>Click here to open the catalogue</span>
+        </a>
+      </li>
+
+      <p v-if="!total && !loading" class="no-more">no more</p>
+    </ul>
+
+    <div
+      v-if="hasMore"
+      style="display: flex; justify-content: center; padding: 30px 0"
+    >
+      <Pagination :total="total" :page-size="PAGE_SIZE" @change="handlePage" />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, ref } from "vue";
+import { getBaseURL } from "@dage/service";
+import { getPublishListApi, type ExhibitionCatalogue } from "@/api";
+import HengImg from "../images/heng.png";
+import HrefImg from "../images/href.png";
+
+const PAGE_SIZE = 8;
+const baseUrl = getBaseURL();
+const pageNum = ref(1);
+const total = ref(0);
+const hasMore = computed(() => total.value > PAGE_SIZE);
+const loading = ref(false);
+const list = ref<ExhibitionCatalogue[]>([]);
+
+onMounted(() => {
+  getList();
+});
+
+const getList = async () => {
+  try {
+    loading.value = true;
+    const data = await getPublishListApi({
+      pageSize: PAGE_SIZE,
+      pageNum: pageNum.value,
+      type: "Exhibition",
+    });
+    list.value = data.records;
+    total.value = data.total;
+  } finally {
+    loading.value = false;
+  }
+};
+
+const handlePage = (page: number) => {
+  pageNum.value = page;
+  getList();
+};
+</script>
+
+<style lang="scss" scoped>
+@import "./index.scss";
+</style>

+ 57 - 49
src/views/Publications/Detail/index.scss

@@ -1,49 +1,57 @@
-.publications-detail {
-  &__hd {
-    position: relative;
-    margin-bottom: 23px;
-    color: white;
-    height: 329px;
-
-    > div {
-      position: absolute;
-      left: 50%;
-      top: 0;
-      width: 100vw;
-      height: inherit;
-      background: url("../images/infoBan.jpg") 0 0 no-repeat #7f694a;
-      transform: translateX(-50%);
-
-      .container {
-        display: flex;
-        padding-top: 15px;
-        gap: 30px;
-        width: 1125px;
-        font-size: 42px;
-        line-height: 60px;
-        font-weight: 700;
-        color: white;
-
-        p {
-          padding-top: 70px;
-          flex: 1;
-        }
-        img {
-          flex-shrink: 0;
-          width: 275px;
-          height: 314px;
-          object-fit: contain;
-          object-position: center bottom;
-        }
-      }
-    }
-  }
-
-  &__title {
-    margin-bottom: 15px;
-    font-size: 24px;
-    line-height: 64px;
-    text-indent: 30px;
-    background: url("../images/infoCon.png") no-repeat center / contain;
-  }
-}
+.publications-detail {
+  padding-bottom: 30px;
+
+  &__rtf {
+    :deep(p) {
+      margin-bottom: 20px;
+      font-size: 18px;
+      line-height: 22px;
+    }
+  }
+  &__hd {
+    position: relative;
+    margin-bottom: 23px;
+    color: white;
+    height: 329px;
+
+    > div {
+      position: absolute;
+      left: 0;
+      top: 0;
+      width: 100%;
+      height: inherit;
+      background: url("../images/infoBan.jpg") 0 0 no-repeat #7f694a;
+
+      .container {
+        display: flex;
+        padding-top: 15px;
+        gap: 30px;
+        width: 1125px;
+        font-size: 42px;
+        line-height: 60px;
+        font-weight: 700;
+        color: white;
+
+        p {
+          padding-top: 70px;
+          flex: 1;
+        }
+        img {
+          flex-shrink: 0;
+          width: 275px;
+          height: 314px;
+          object-fit: contain;
+          object-position: center bottom;
+        }
+      }
+    }
+  }
+
+  &__title {
+    margin-bottom: 15px;
+    font-size: 24px;
+    line-height: 64px;
+    text-indent: 30px;
+    background: url("../images/infoCon.png") no-repeat center / contain;
+  }
+}

+ 56 - 23
src/views/Publications/Detail/index.vue

@@ -1,23 +1,56 @@
-<template>
-  <div class="publications-detail">
-    <div class="publications-detail__hd">
-      <div>
-        <div class="container">
-          <p>Cultural Relics Conservation and Security</p>
-
-          <img
-            src="https://en.capitalmuseum.org.cn/data/Publications/Magazines/35.jpg"
-          />
-        </div>
-      </div>
-    </div>
-
-    <div class="publications-detail__title">Contents</div>
-  </div>
-</template>
-
-<script lang="ts" setup></script>
-
-<style lang="scss" scoped>
-@import "./index.scss";
-</style>
+<template>
+  <div v-loading="loading" class="publications-detail">
+    <div class="publications-detail__hd">
+      <div>
+        <div class="container">
+          <p>{{ detail?.name }}</p>
+
+          <img v-if="detail" :src="baseUrl + detail.thumb" />
+        </div>
+      </div>
+    </div>
+
+    <div class="container">
+      <div class="publications-detail__title">Contents</div>
+
+      <div
+        v-for="item in rtf"
+        :key="item.id"
+        v-html="item.txt"
+        class="publications-detail__rtf"
+      />
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { getPublishDetailApi, type PublicationDetail } from "@/api";
+import { getBaseURL } from "@dage/service";
+import { onMounted, ref } from "vue";
+import { useRoute } from "vue-router";
+
+const route = useRoute();
+const baseUrl = getBaseURL();
+const loading = ref(false);
+const detail = ref<PublicationDetail | null>(null);
+const rtf = ref<{ id: number; txt: string }[]>([]);
+
+onMounted(() => {
+  getDetail();
+});
+
+const getDetail = async () => {
+  try {
+    loading.value = true;
+    const data = await getPublishDetailApi(route.query.id as string);
+    detail.value = data;
+    rtf.value = JSON.parse(data.rtf).txtArr;
+  } finally {
+    loading.value = false;
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "./index.scss";
+</style>

+ 118 - 116
src/views/Publications/Magazines/index.scss

@@ -1,116 +1,118 @@
-.magazines {
-  &-info {
-    display: flex;
-    gap: 30px;
-    margin: 70px 0 40px;
-
-    &-sidebar {
-      width: 252px;
-      border-right: 1px solid var(--van-primary-color);
-
-      li {
-        position: relative;
-        display: flex;
-        align-items: center;
-        margin-bottom: 48px;
-        gap: 10px;
-        font-size: 14px;
-        color: var(--gray-text-color);
-        line-height: 54px;
-        height: 54px;
-        font-weight: 700;
-        cursor: pointer;
-
-        &.active {
-          color: var(--van-primary-color);
-
-          &::after {
-            content: "";
-            position: absolute;
-            top: 50%;
-            right: -9px;
-            width: 9px;
-            height: 10px;
-            background: url("../images/ac.jpg") no-repeat center / contain;
-            transform: translateY(-50%);
-          }
-        }
-      }
-    }
-    &-main {
-      flex: 1;
-
-      :deep(p) {
-        margin-bottom: 22px;
-        line-height: 22px;
-        font-size: 14px;
-
-        i {
-          font-style: italic;
-        }
-      }
-    }
-  }
-
-  &-our {
-    margin-bottom: 25px;
-
-    &__title {
-      margin: 25px 0;
-      height: 64px;
-      text-align: center;
-      background: url("../images/bg_20.png") no-repeat center / contain;
-      line-height: 64px;
-      font-size: 24px;
-      font-weight: 700;
-    }
-    &__nav {
-      display: flex;
-      align-items: center;
-      justify-content: center;
-      margin-bottom: 20px;
-
-      li {
-        width: 140px;
-        height: 40px;
-        line-height: 40px;
-        text-align: center;
-        cursor: pointer;
-
-        &.active {
-          color: white;
-          background: #232323;
-        }
-      }
-    }
-    &__list {
-      display: flex;
-      flex-wrap: wrap;
-      margin: -8px;
-
-      li {
-        margin: 8px;
-        padding: 10px;
-        width: 283px;
-        height: 455px;
-        background: rgba(127, 105, 74, 0.6);
-        transition: background-color linear 0.3s;
-        cursor: pointer;
-
-        &:hover {
-          background: rgba(127, 105, 74);
-        }
-        .el-image {
-          margin-bottom: 15px;
-          width: 100%;
-          height: 358px;
-        }
-        p {
-          font-size: 16px;
-          color: white;
-          line-height: 24px;
-        }
-      }
-    }
-  }
-}
+.magazines {
+  &-info {
+    display: flex;
+    gap: 30px;
+    margin: 70px 0 40px;
+
+    &-sidebar {
+      width: 252px;
+      border-right: 1px solid var(--van-primary-color);
+
+      li {
+        position: relative;
+        display: flex;
+        align-items: center;
+        margin-bottom: 48px;
+        gap: 10px;
+        font-size: 14px;
+        color: var(--gray-text-color);
+        line-height: 54px;
+        height: 54px;
+        font-weight: 700;
+        cursor: pointer;
+
+        &.active {
+          color: var(--van-primary-color);
+
+          &::after {
+            content: "";
+            position: absolute;
+            top: 50%;
+            right: -9px;
+            width: 9px;
+            height: 10px;
+            background: url("../images/ac.jpg") no-repeat center / contain;
+            transform: translateY(-50%);
+          }
+        }
+      }
+    }
+    &-main {
+      flex: 1;
+
+      :deep(p) {
+        margin-bottom: 22px;
+        line-height: 22px;
+        font-size: 14px;
+
+        i {
+          font-style: italic;
+        }
+      }
+    }
+  }
+
+  &-our {
+    margin-bottom: 25px;
+
+    &__title {
+      margin: 25px 0;
+      height: 64px;
+      text-align: center;
+      background: url("../images/bg_20.png") no-repeat center / contain;
+      line-height: 64px;
+      font-size: 24px;
+      font-weight: 700;
+    }
+    &__nav {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      margin-bottom: 20px;
+
+      li {
+        width: 140px;
+        height: 40px;
+        line-height: 40px;
+        text-align: center;
+        cursor: pointer;
+
+        &.active {
+          color: white;
+          background: #232323;
+        }
+      }
+    }
+    &__list {
+      position: relative;
+      display: flex;
+      flex-wrap: wrap;
+      margin: -8px;
+      min-height: 455px;
+
+      li {
+        margin: 8px;
+        padding: 10px;
+        width: 283px;
+        height: 455px;
+        background: rgba(127, 105, 74, 0.6);
+        transition: background-color linear 0.3s;
+        cursor: pointer;
+
+        &:hover {
+          background: rgba(127, 105, 74);
+        }
+        .el-image {
+          margin-bottom: 15px;
+          width: 100%;
+          height: 358px;
+        }
+        p {
+          font-size: 16px;
+          color: white;
+          line-height: 24px;
+        }
+      }
+    }
+  }
+}

+ 142 - 76
src/views/Publications/Magazines/index.vue

@@ -1,76 +1,142 @@
-<template>
-  <div class="magazines">
-    <div class="magazines-info">
-      <ul class="magazines-info-sidebar">
-        <li
-          v-for="(item, idx) in MAGAZINES_INFO"
-          :key="idx"
-          :class="{ active: idx === curInfoIndex }"
-          tabindex="0"
-          aria-label="Link"
-          @click="curInfoIndex = idx"
-        >
-          <img :src="item.icon" />
-          <p>{{ item.name }}</p>
-        </li>
-      </ul>
-
-      <div
-        class="magazines-info-main"
-        v-html="MAGAZINES_INFO[curInfoIndex].conten"
-      />
-    </div>
-
-    <div
-      class="magazines-our"
-      data-aria-viewport-area
-      tabindex="0"
-      aria-description="You've reached the Our Magazines section, please use the tab key to go through the content."
-    >
-      <div class="magazines-our__title">Our Magazines</div>
-
-      <ul class="magazines-our__nav">
-        <li
-          v-for="(date, idx) in DATE"
-          :key="date"
-          :class="{ active: idx === curDateIndex }"
-          tabindex="0"
-          aria-label="Link"
-          @mouseenter="curDateIndex = idx"
-        >
-          {{ date }}
-        </li>
-      </ul>
-
-      <ul class="magazines-our__list">
-        <li
-          v-for="key in 10"
-          :key="key"
-          tabindex="0"
-          aria-label="Image link"
-          @click="
-            $router.push({ name: 'PublicationsDetail', query: { id: key } })
-          "
-        >
-          <ElImage
-            src="https://en.capitalmuseum.org.cn/data/Publications/Magazines/2021_3.jpg"
-          />
-          <p>Exhibition of Revolutionary Relics and Education</p>
-        </li>
-      </ul>
-    </div>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { ref } from "vue";
-import { MAGAZINES_INFO } from "../constants";
-
-const DATE = [2023, 2022, 2021, 2020, 2019, 2018, 2017];
-const curInfoIndex = ref(0);
-const curDateIndex = ref(0);
-</script>
-
-<style lang="scss" scoped>
-@import "./index.scss";
-</style>
+<template>
+  <div class="magazines container">
+    <div class="magazines-info">
+      <ul class="magazines-info-sidebar">
+        <li
+          v-for="(item, idx) in MAGAZINES_INFO"
+          :key="idx"
+          :class="{ active: idx === curInfoIndex }"
+          tabindex="0"
+          aria-label="Link"
+          @click="curInfoIndex = idx"
+        >
+          <img :src="item.icon" />
+          <p>{{ item.name }}</p>
+        </li>
+      </ul>
+
+      <div
+        class="magazines-info-main"
+        v-html="MAGAZINES_INFO[curInfoIndex].conten"
+      />
+    </div>
+
+    <div
+      class="magazines-our"
+      data-aria-viewport-area
+      tabindex="0"
+      aria-description="You've reached the Our Magazines section, please use the tab key to go through the content."
+    >
+      <div class="magazines-our__title">Our Magazines</div>
+
+      <!-- 年份 -->
+      <ul class="magazines-our__nav">
+        <li
+          v-for="(date, idx) in DATE"
+          :key="date"
+          :class="{ active: idx === curDateIndex }"
+          tabindex="0"
+          aria-label="Link"
+          @mouseenter="
+            () => {
+              curDateIndex = idx;
+              debounceSearch();
+            }
+          "
+        >
+          {{ date }}
+        </li>
+      </ul>
+
+      <!-- 列表 -->
+      <ul v-loading="loading" class="magazines-our__list">
+        <li
+          v-for="item in list"
+          :key="item.id"
+          tabindex="0"
+          aria-label="Image link"
+          @click="
+            $router.push({
+              name: 'PublicationsDetail',
+              query: {
+                id: item.id,
+                type: 0,
+                title: encodeURIComponent(item.name),
+              },
+            })
+          "
+        >
+          <ElImage :src="baseUrl + item.thumb" />
+          <p>{{ item.name }}</p>
+        </li>
+
+        <p v-if="!total && !loading" class="no-more">no more</p>
+      </ul>
+
+      <div
+        v-if="hasMore"
+        style="display: flex; justify-content: center; padding: 30px 0"
+      >
+        <Pagination
+          :total="total"
+          :page-size="PAGE_SIZE"
+          @change="handlePage"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { computed, onMounted, ref } from "vue";
+import { MAGAZINES_INFO } from "../constants";
+import { getPublishListApi, type PubicationItem } from "@/api";
+import { getBaseURL } from "@dage/service";
+import { debounce } from "lodash-unified";
+
+const DATE = [2023, 2022, 2021, 2020, 2019, 2018, 2017];
+const PAGE_SIZE = 8;
+const curInfoIndex = ref(0);
+const curDateIndex = ref(0);
+const baseUrl = getBaseURL();
+
+const pageNum = ref(1);
+const total = ref(0);
+const hasMore = computed(() => total.value > PAGE_SIZE);
+const loading = ref(false);
+const list = ref<PubicationItem[]>([]);
+
+onMounted(() => {
+  getList();
+});
+
+const debounceSearch = debounce(() => {
+  pageNum.value = 1;
+  getList();
+}, 500);
+
+const getList = async () => {
+  try {
+    loading.value = true;
+    const data = await getPublishListApi({
+      pageSize: PAGE_SIZE,
+      pageNum: pageNum.value,
+      year: DATE[curDateIndex.value],
+      type: "Magazines",
+    });
+    total.value = data.total;
+    list.value = data.records;
+  } finally {
+    loading.value = false;
+  }
+};
+
+const handlePage = (page: number) => {
+  pageNum.value = page;
+  getList();
+};
+</script>
+
+<style lang="scss" scoped>
+@import "./index.scss";
+</style>

+ 69 - 47
src/views/Publications/index.vue

@@ -1,47 +1,69 @@
-<template>
-  <div class="publications">
-    <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="publications-banner"
-      src="./images/banner.jpg"
-    />
-
-    <PageNav :list="NAV_LIST" :cur-route-name="curRoute?.name" />
-
-    <div class="container">
-      <Breadcrumb
-        :parents="[
-          {
-            label: 'Publications',
-            routeParams: {
-              name: 'Publications',
-            },
-          },
-        ]"
-        :cur-route="curRoute"
-      />
-
-      <PageTitle v-if="curRoute" :title="curRoute.name" />
-
-      <RouterView />
-    </div>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { useRoute } from "vue-router";
-import { computed } from "vue";
-import PageNav from "@/components/PageNav/index.vue";
-import PageTitle from "@/components/PageTitle/index.vue";
-import Breadcrumb from "@/components/Breadcrumb/index.vue";
-import { NAV_LIST } from "./constants";
-
-const route = useRoute();
-const curRoute = computed(() =>
-  NAV_LIST.find((item) => item.routeParams.name === route.name)
-);
-</script>
-
-<style lang="scss" scoped>
-@import "./index.scss";
-</style>
+<template>
+  <div class="publications">
+    <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="publications-banner"
+      :src="bannerUrl"
+    />
+
+    <PageNav :list="NAV_LIST" :cur-route-name="curRouteName" />
+
+    <div class="container">
+      <Breadcrumb
+        :parents="[
+          {
+            label: 'Publications',
+            routeParams: {
+              name: 'Publications',
+            },
+          },
+        ]"
+        :cur-route="curRoute"
+      />
+
+      <PageTitle v-if="curRoute" :title="curRoute.name" />
+    </div>
+
+    <RouterView />
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { useRoute } from "vue-router";
+import { computed } from "vue";
+import PageNav from "@/components/PageNav/index.vue";
+import PageTitle from "@/components/PageTitle/index.vue";
+import Breadcrumb from "@/components/Breadcrumb/index.vue";
+import { getBaseURL } from "@dage/service";
+import { useBaseStore } from "@/stores/base";
+import { NAV_LIST } from "./constants";
+
+const baseUrl = getBaseURL();
+const baseStore = useBaseStore();
+const route = useRoute();
+const curRoute = computed(() => {
+  if (route.query.title) {
+    return {
+      name: decodeURIComponent(route.query.title as string),
+    };
+  }
+
+  return NAV_LIST.find((item) => item.routeParams.name === route.name);
+});
+const curRouteName = computed(() =>
+  route.query.type
+    ? NAV_LIST[Number(route.query.type)].name
+    : curRoute.value?.name
+);
+
+const bannerUrl = computed(() => {
+  return (
+    baseUrl +
+    baseStore.bannerList.find((i) => i.name === "Publications")?.thumbPc
+  );
+});
+</script>
+
+<style lang="scss" scoped>
+@import "./index.scss";
+</style>

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

@@ -1,216 +1,240 @@
-<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>
+<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 #dateCell="{ data }">
+        {{ data.data.day.split("-")[2] }}
+        <div class="dots">
+          <div class="dot" />
+          <div class="dot events" />
+          <div class="dot learn" />
+        </div>
+      </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;
+
+    .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;
+        }
+      }
+    }
+    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>

+ 251 - 254
src/views/Visit/Calendar/components/DateTable/index.vue

@@ -1,254 +1,251 @@
-<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>
+<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">
+            <slot name="dateCell" :data="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 }) {
+      const day = getFormateDate(text, type);
+      const date = new Date(day);
+      const data = {
+        isSelected: selectedDay.value === day,
+        type: `${type}-month`,
+        day,
+      };
+      return { 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>