Selaa lähdekoodia

📦 按要求修改问题

imengyu 1 kuukausi sitten
vanhempi
commit
3a6605185f

+ 4 - 0
1.cjs

@@ -0,0 +1,4 @@
+const v8 = require('v8');
+const totalHeapSize = v8.getHeapStatistics().total_available_size;
+let totalHeapSizeInGB = (totalHeapSize / 1024 / 1024 / 1024).toFixed(2);
+console.log(`Total heap size: ${totalHeapSizeInGB} GB`);

+ 2 - 0
src/App.vue

@@ -1,5 +1,6 @@
 <template>
   <a-config-provider
+    :locale="zhCN"
     :theme="{
       token: {
         colorPrimary: '#bd4b36',
@@ -20,6 +21,7 @@ import { RouterView, useRoute } from 'vue-router'
 import NavBar from './components/NavBar.vue';
 import Footer from './components/Footer.vue';
 import { useAuthStore } from './stores/auth';
+import zhCN from 'ant-design-vue/es/locale/zh_CN';
 
 const authStore = useAuthStore();
 

+ 6 - 6
src/api/CommonContent.ts

@@ -150,14 +150,14 @@ export class GetContentListItem extends DataModel<GetContentListItem> {
       isCollect: { clientSide: 'boolean', serverSide: 'number' },
       latitude: { clientSide: 'number', serverSide: 'number' },
       longitude: { clientSide: 'number', serverSide: 'number' },
-      publish_at: { clientSide: 'date', serverSide: 'string' },
+      publishAt: { clientSide: 'date', serverSide: 'string' },
       flag: { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
       tags: { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
       keywords: { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
       type: { clientSide: 'number', serverSide: 'number' },
     };
     this._convertKeyType = (key, direction) => {
-      if (key.endsWith('Time'))
+      if (key.endsWith('Time') || key.endsWith('At'))
         return {
           clientSide: 'date',
           serverSide: 'string',
@@ -190,7 +190,7 @@ export class GetContentListItem extends DataModel<GetContentListItem> {
   collects = 0;
   dislikes = 0;
   district = '';
-  publish_at = new Date();
+  publishAt = new Date();
 }
 export class GetContentDetailItem extends DataModel<GetContentDetailItem> {
   constructor() {
@@ -205,13 +205,13 @@ export class GetContentDetailItem extends DataModel<GetContentDetailItem> {
       isComment: { clientSide: 'boolean', serverSide: 'number' },
       isLike: { clientSide: 'boolean', serverSide: 'number' },
       isCollect: { clientSide: 'boolean', serverSide: 'number' },
-      publish_at: { clientSide: 'date', serverSide: 'string' },
+      publishAt: { clientSide: 'date', serverSide: 'string' },
       flag: { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
       tags: { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
       type: { clientSide: 'number', serverSide: 'number' },
     }
     this._convertKeyType = (key, direction) => {
-      if (key.endsWith('Time'))
+      if (key.endsWith('Time') || key.endsWith('At'))
         return {
           clientSide: 'date',
           serverSide: 'string',
@@ -254,7 +254,7 @@ export class GetContentDetailItem extends DataModel<GetContentDetailItem> {
   content = '';
   value = '';
   intro = '';
-  publish_at = new Date();
+  publishAt = new Date();
 }
 
 export class CategoryListItem extends DataModel<CategoryListItem> {

+ 2 - 1
src/assets/scss/fix.scss

@@ -1,7 +1,8 @@
 .carousel-light {
   --vc-nav-color: #fff;
   --vc-clr-primary: #fff;
-  --vc-clr-secondary: #cfcfcf7f;
+  --vc-clr-secondary: #cfcfcfc4;
+  --vc-pgn-background-color: #cfcfcfc4;
   --vc-clr-white: #333333;
   --vc-pgn-active-color: var(--vc-clr-primary)
 }

+ 62 - 0
src/assets/scss/main.scss

@@ -90,6 +90,7 @@ $small-banner-height: 445px;
       color: $text-color-light;
       text-align: center;
       line-height: 56px;
+      cursor: pointer;
 
       &.active {
         background-color: $primary-color;
@@ -225,6 +226,39 @@ $small-banner-height: 445px;
   }
 }
 
+.main-stats {
+  display: flex;
+  flex-direction: column;
+  font-family: SourceHanSerifCNBold;
+
+  h4 {
+    margin: 50px 0 10px 0;
+    font-size: 1rem;
+    color: $text-second-color;
+  }
+
+  .descs {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: space-around;
+
+    div {
+      text-align: center;
+    }
+
+    h5 {
+      margin: 0;
+      font-size: 4.5rem;
+      font-weight: bold;
+    }
+    p {
+      margin: 0;
+      font-size: 0.9rem;
+    }
+  }
+}
+
 //
 
 @media (max-width: 1280px) {
@@ -261,6 +295,20 @@ $small-banner-height: 445px;
       padding-bottom: 20px;
     }
   }
+  .main-stats {
+    h4 {
+      margin: 20px 0 10px 0;
+      font-size: 1rem;
+    }
+    .descs {
+      h5 {
+        font-size: 3rem;
+      }
+      p {
+        font-size: 0.9rem;
+      }
+    }
+  }
 }
 @media (max-width: 425px) {
   .main-section {
@@ -280,4 +328,18 @@ $small-banner-height: 445px;
   .main-section.fit-small-header {
     height: $large-banner-height;
   }
+  .main-stats {
+    h4 {
+      margin: 10px 0 5px 0;
+      font-size: 0.7rem;
+    }
+    .descs {
+      h5 {
+        font-size: 2rem;
+      }
+      p {
+        font-size: 0.8rem;
+      }
+    }
+  }
 }

+ 3 - 3
src/components/NavBar.vue

@@ -16,13 +16,13 @@
       >
         <div>
           <RouterLink to="/">首页</RouterLink>
+          <RouterLink to="/about">保护区概况</RouterLink> 
           <RouterLink to="/news">资讯动态</RouterLink>
           <RouterLink to="/introduction">文化概况</RouterLink>
           <RouterLink to="/inheritor">保护传承</RouterLink>
           <RouterLink to="/communicate">传播交流</RouterLink>
           <RouterLink to="/research">理论研究</RouterLink>
           <RouterLink to="/fusion">文旅融合</RouterLink>
-          <RouterLink to="/about">关于我们</RouterLink> 
         </div>
       </div>
     </Teleport>
@@ -39,9 +39,9 @@
     <div></div>
     <div class="group">
       <RouterLink to="/">首页</RouterLink>
+      <RouterLink to="/about">保护区概况</RouterLink> 
       <RouterLink to="/news">资讯动态</RouterLink>
       <RouterLink to="/introduction">文化概况</RouterLink>
-      <RouterLink to="/inheritor">保护传承</RouterLink>
     </div>
     <div class="group center">
       <div class="headerlogos">
@@ -53,10 +53,10 @@
       </div>
     </div>
     <div class="group">
+      <RouterLink to="/inheritor">保护传承</RouterLink>
       <RouterLink to="/communicate">传播交流</RouterLink>
       <RouterLink to="/research">理论研究</RouterLink>
       <RouterLink to="/fusion">文旅融合</RouterLink>
-      <RouterLink to="/about">关于我们</RouterLink> 
     </div>
     <div></div>
   </nav>

+ 1 - 1
src/components/content/CommonListBlock.vue

@@ -63,7 +63,7 @@
           <TitleDescBlock
             :title="item.title"
             :desc="item.desc || item.title"
-            :date="DateUtils.formatDate(item.publish_at, DateUtils.FormatStrings.YearCommon)"
+            :date="DateUtils.formatDate(item.publishAt, DateUtils.FormatStrings.YearCommon)"
           >
             <template #addon>
               <div v-if="item.addItems" class="extra">

+ 56 - 0
src/components/content/ImageGrid.vue

@@ -0,0 +1,56 @@
+<template>
+  <div class="w-100 d-flex flex-row flex-wrap" :style="{ gap: gap }">
+    <slot 
+      name="item"
+      v-for="(v, k) in data"
+      :key="k"
+      :item="v"
+      :index="k"
+      :width="`calc(${100 / rowCount}% - ${gap})`"
+      :height="imageHeight"
+      :url="imagekey ? v[imagekey] : v" 
+    >
+      <img 
+        :src="imagekey ? v[imagekey] : v" 
+        :style="{ 
+          width: `calc(${100 / rowCount}% - ${gap})`,
+          height: imageHeight,
+          borderRadius: '5px',
+          objectFit: 'cover',
+        }"
+        @click="()=>emit('itemClick', v)"
+      />
+    </slot>
+  </div>
+</template>
+
+<script setup lang="ts">
+import type { PropType } from 'vue';
+
+defineProps({	
+  rowCount : {
+    type: Number,
+    default: 3,
+  },
+  imagekey : {
+    type: String,
+    default: undefined,
+  },
+  imageHeight : {
+    type: String,
+    default: undefined,
+  },
+  gap: {
+    type: String,
+    default: '10px',
+  },
+  data: {
+    type: Object as PropType<any[]>,
+    default: null,
+  },
+})
+
+const emit = defineEmits([	
+  "itemClick"	
+])
+</script>

+ 9 - 0
src/components/parts/ImageTitleBlock.vue

@@ -3,6 +3,7 @@
     :class="[
       'ImageTitleBlock',
       title ? 'has-title' : '',
+      fit? 'fit' : ''
     ]" 
     :style="{ backgroundImage: `url('${image}')` }"
     @click="$emit('click')"
@@ -28,6 +29,10 @@ defineProps({
     type: String,
     default: '' 
   },
+  fit: {
+    type: Boolean,
+    default: false
+  }
 })
 defineEmits([
   'click',
@@ -46,6 +51,10 @@ defineEmits([
   height: 270px;
   margin-right: 24px;
 
+  &.fit {
+    width: 100%;
+    margin-right: 0;
+  }
   &.has-title {
     &::before {
       content: '';

+ 60 - 3
src/components/parts/LeftRightBox.vue

@@ -1,7 +1,27 @@
 <template>
   <div class="main-box main-left-right-box row">
     <div class="col col-12 col-lg-6 col-md-6">
-      <img v-if="left" :src="image" alt="image" @click="emit('moreClick')" />
+      <template v-if="left">
+        <Carousel v-if="rightItems && rightItems.length > 0" v-bind="carousel2Config" class="carousel-light">
+          <Slide 
+            v-for="(item, index) in rightItems"
+            :key="index"
+            @click="() => item.onClick ? item.onClick() : emit('rightItemDefaultClick', item)"
+          >
+            <ImageTitleBlock 
+              fit
+              :image="item.image"
+              :title="item.title"
+              :desc="item.desc"
+            />
+          </Slide>
+          <template #addons>
+            <Navigation />
+            <Pagination />
+          </template>
+        </Carousel>
+        <img v-else :src="image" alt="image" @click="emit('moreClick')" />
+      </template>
       <TitleDescBlock 
         v-else 
         :title="title"
@@ -22,13 +42,41 @@
         :more="showMore"
         @moreClick="emit('moreClick')"
       />
-      <img v-else :src="image" alt="image" @click="emit('moreClick')" />
+      <template v-else>
+        <Carousel v-if="rightItems && rightItems.length > 0" v-bind="carousel2Config" class="carousel-light">
+          <Slide 
+            v-for="(item, index) in rightItems"
+            :key="index"
+            @click="() => item.onClick ? item.onClick() : emit('rightItemDefaultClick', item)"
+          >
+            <ImageTitleBlock fit
+              :image="item.image"
+              :title="item.title"
+              :desc="item.desc"
+            />
+          </Slide>
+          <template #addons>
+            <Navigation />
+            <Pagination />
+          </template> 
+        </Carousel>
+        <img v-else :src="image" alt="image" @click="emit('moreClick')" />
+      </template>
     </div>
   </div>
 </template>
 
 <script setup lang="ts">
+import type { PropType } from 'vue';
+import { Carousel, Slide, Pagination, Navigation } from 'vue3-carousel'
 import TitleDescBlock from './TitleDescBlock.vue';
+import ImageTitleBlock from '@/components/parts/ImageTitleBlock.vue';
+
+const carousel2Config = {
+  itemsToShow: 1,
+  mouseWheel: true,
+  wrapAround: true
+}
 
 defineProps({	
   title : {
@@ -43,6 +91,15 @@ defineProps({
     type: String,
     default: '',
   },
+  rightItems: {
+    type: Object as PropType<Array<{ 
+      title: string, 
+      desc: string, 
+      image: string ,
+      onClick?: () => void,
+    }>|null>,
+    default: null,
+  },
   descLines: {
     type: Number,
     default: 3,
@@ -62,7 +119,7 @@ defineProps({
 })
 
 const emit = defineEmits([	
-  "moreClick"	
+  "moreClick"	, 'rightItemDefaultClick'
 ])
 </script>
 

+ 5 - 0
src/router/index.ts

@@ -214,6 +214,11 @@ const router = createRouter({
       component: () => import('../views/details/ArtifactDetailView.vue'),
     },
     {
+      path: '/inheritor/intangible-detail',
+      name: 'intangible-detail',
+      component: () => import('../views/details/IntangibleDetailView.vue'),
+    },
+    {
       path: '/inheritor/moveable',
       name: 'InheritorMoveable',
       component: () => import('../views/inheritor/moveable.vue'),

+ 100 - 7
src/views/AboutView.vue

@@ -13,8 +13,33 @@
     </Carousel>
 
 
+    <!-- 头部搜索 -->
+    <section class="main-section absolute light fit-small-header">
+      <div class="content row">
+        <div class="col-md-12 col-lg-8 col-xl-6">
+          <div class="main-header-title d-flex flex-column">
+          </div>
+        </div>
+        <div class="col-md-12 col-lg-4 col-xl-6">
+        </div>
+      </div>
+      <!-- 头部TAB -->
+      <div class="main-header-tab">
+        <div class="list">
+          <div 
+            v-for="(tab, k) in mainTabs"
+            :key="k"
+            :class="[ mainTabActive === tab.value ? 'active' : '' ]"
+            @click="mainTabActive = tab.value"
+          >
+            {{ tab.title }}
+          </div>
+        </div>
+      </div>
+    </section>
+
     <!-- 新闻 -->
-    <section class="main-section main-background main-background-type0">
+    <section v-if="mainTabActive <= 2" class="main-section main-background main-background-type0">
       <div class="content news-list">
         <!-- 新闻列表 -->
         <SimplePageContentLoader :loader="newsLoader">
@@ -28,7 +53,7 @@
             <TitleDescBlock
               :title="item.title"
               :desc="item.desc || item.title"
-              :date="DateUtils.formatDate(item.publish_at, DateUtils.FormatStrings.YearCommon)"
+              :date="DateUtils.formatDate(item.publishAt, DateUtils.FormatStrings.YearCommon)"
             />
           </div>
         </SimplePageContentLoader>
@@ -39,19 +64,49 @@
         />
       </div>
     </section>
+
+    <!-- 法律法规 -->
+    <section v-if="mainTabActive == 3" class="main-section">
+      <div class="content">
+        <div class="title left-right">
+          <h2>法律法规</h2>
+          <div class="small-more" @click="navTo('/introduction/policy')">
+            <span>更多信息</span>
+            <img src="@/assets/images/index/ButtonMore.png" alt="更多" />
+          </div>
+        </div>
+        <SimplePageContentLoader :loader="lawsData">
+          <ImageTextSmallBlock
+            v-for="(item, index) in lawsData.content.value"
+            :key="index"
+            :title="item.title"
+            :image="item.image"
+            :date="item.date"
+            @click="navTo('/news/detail', { id: item.id })"
+          />
+        </SimplePageContentLoader>
+      </div>
+    </section>
   </div>
 </template>
 
 <script setup lang="ts">
 import { Carousel, Slide, Pagination, Navigation } from 'vue3-carousel'
-import { onMounted, ref } from 'vue';
+import { onMounted, ref, watch } from 'vue';
 import { useSimplePagerDataLoader } from '@/composeable/SimplePagerDataLoader';
 import Pagination2 from '@/components/controls/Pagination.vue';
 import TitleDescBlock from '@/components/parts/TitleDescBlock.vue';
+import ImageTextSmallBlock from '@/components/parts/ImageTextSmallBlock.vue';
 import SimplePageContentLoader from '@/components/content/SimplePageContentLoader.vue';
 import CommonContent, { GetContentListParams } from '@/api/CommonContent';
 import DateUtils from '@/common/utils/DateUtils';
+import PolicyContent from '@/api/introduction/PolicyContent';
+import LawsTest from '@/assets/images/inheritor/LawsTest.jpg'
 import { useRouter } from 'vue-router';
+import { useSimpleDataLoader } from '@/composeable/SimpleDataLoader';
+import { usePageAction } from '@/composeable/PageAction';
+
+const { navTo } = usePageAction();
 
 const carouselConfig = {
   itemsToShow: 1,
@@ -59,19 +114,57 @@ const carouselConfig = {
   autoPlay: 5000,
 }
 
+const mainTabs = [
+  { title: '闽南文化概况', value: 0 },
+  { title: '闽南文化生态保护区概况', value: 1 },
+  { title: '世界闽南文化交流中心', value: 2 },
+  { title: '法律法规', value: 3 },
+]
+const mainTabActive = ref(0);
+
 const router = useRouter();
 
 const newsLoader = useSimplePagerDataLoader(10, async (page, pageSize) => {
-  const res = await CommonContent.getContentList(new GetContentListParams()
-    .setModelId(17)
-    .setMainBodyColumnId([ 255, 256, 283, 284, ])
-  , page, pageSize);
+  let res;
+  switch (mainTabActive.value) {
+    default:
+    case 0:
+      res = await CommonContent.getContentList(new GetContentListParams()
+        .setModelId(17)
+        .setMainBodyColumnId([ ])
+      , page, pageSize);
+      break;
+    case 1:
+      res = await CommonContent.getContentList(new GetContentListParams()
+        .setModelId(17)
+        .setMainBodyColumnId([ 255, 256, 283, 284, ])
+      , page, pageSize);
+      break;
+    case 2:
+      res = await CommonContent.getContentList(new GetContentListParams()
+        .setModelId(17)
+        .setMainBodyColumnId([ 232 ])
+      , page, pageSize);
+      break;
+  }
   return {
     data: res.list,
     total: res.total,
   };
 });
 
+const lawsData = useSimpleDataLoader(async () => 
+  (await PolicyContent.getContentList(new GetContentListParams(), 1, 8))
+    .list?.map(item => ({
+      id: item.id,
+      title: item.title,
+      image: item.image || LawsTest,
+      date: DateUtils.formatDate(item.publishAt, DateUtils.FormatStrings.YearCommon),
+    }))
+)
+
+watch(mainTabActive, () => newsLoader.loadData(undefined, true))
+
 onMounted(async () => {
   newsLoader.loadData(undefined, true);
 })

+ 1 - 1
src/views/HomeView.vue

@@ -259,7 +259,7 @@ const recommend1Data = useSimpleDataLoader<GetContentListItem[]>(async () => {
   return (await UnmoveableContent.getContentList(new GetContentListParams(), 1, 8)).list;
 });
 const recommend2Data = useSimpleDataLoader<GetContentListItem[]>(async () => {
-  return (await ProductsContent.getContentList(new GetContentListParams(), 1, 8)).list
+  return (await ProjectContent.getContentList(new GetContentListParams(), 1, 8)).list
 });
 const recommend3Data = useSimpleDataLoader<GetContentListItem[]>(async () => {
   return (await CommonContent.getContentList(new GetContentListParams()

+ 52 - 65
src/views/InheritorView.vue

@@ -20,22 +20,15 @@
         </div>
 
         <SimplePageContentLoader :loader="statsData">
-          <Carousel ref="carousel3Ref" v-bind="carousel3Config">
-            <Slide v-for="(stat,key) in statsData.content.value" :key="key">
-              <div :class="`main-card-box type${stat.type}`">
-                <h4>{{ stat.title }}</h4>
-                <div class="descs">
-                  <div v-for="(data, key2) in stat.datas" :key="key2">
-                    <h5>{{ data.title }}</h5>
-                    <p>{{ data.value }}</p>
-                  </div>
-                </div>
+          <div v-for="(stat,key) in statsData.content.value" :key="key" class="main-stats">
+            <h4>{{ stat.title }}</h4>
+            <div class="descs">
+              <div v-for="(data, key2) in stat.datas" :key="key2">
+                <h5>{{ data.value }}</h5>
+                <p>{{ data.title }}</p>
               </div>
-            </Slide>
-            <template #addons>
-              <Navigation />
-            </template>
-          </Carousel>
+            </div>
+          </div>
         </SimplePageContentLoader>
       </div>
     </section>
@@ -60,77 +53,62 @@
       </div>
     </section>
 
-    <!-- 自然遗产 -->
+    <!-- 世界文化遗产 -->
     <section class="main-section pb-0">
       <div class="content">
         <div class="title">
-          <h2>自然遗产</h2>
+          <h2>世界文化遗产</h2>
         </div>
         <LeftRightBox 
-          title="自然遗产"
+          title="世界文化遗产"
           :desc="overviewsLoader.content.value?.[0]"
           :image="Image9"
           :showExpand="false"
+          :rightItems="heritageData.content.value"
+          @rightItemDefaultClick="(item) => navTo('/news/detail', { id: item.id })"
           @moreClick="navTo('/inheritor/heritage')"
         />
       </div>
     </section>
 
-    <!-- 重要相关历史风貌区 -->
+    <!-- 重要相关文物古迹 -->
     <section class="main-section pb-0">
       <div class="content">
         <div class="title">
-          <h2>重要相关历史风貌区</h2>
+          <h2>重要相关文物古迹</h2>
         </div>
         <LeftRightBox 
-          title="历史风貌区"
+          title="重要相关文物古迹"
           :desc="overviewsLoader.content.value?.[1]"
           :image="Image11"
           :showExpand="false"
           left
+          :rightItems="areaData.content.value"
+          @rightItemDefaultClick="(item) => navTo('/news/detail', { id: item.id })"
           @moreClick="navTo('/inheritor/area')"
         />
       </div>
     </section>
 
-    <!-- 重要相关文物和古迹 -->
+    <!-- 历史文化街区 -->
     <section class="main-section pb-0">
       <div class="content">
         <div class="title">
-          <h2>重要相关文物和古迹</h2>
+          <h2>历史文化街区</h2>
         </div>
         <LeftRightBox 
-          title="传统村落"
+          title="历史文化街区"
           :desc="overviewsLoader.content.value?.[2]"
           :image="Image10"
           :showExpand="false"
+          :rightItems="blockData.content.value"
+          @rightItemDefaultClick="(item) => navTo('/news/detail', { id: item.id })"
           @moreClick="navTo('/village/index')"
         />
       </div>
     </section>
 
-    <!-- 法律法规 -->
-    <section class="main-section pb-0">
-      <div class="content">
-        <div class="title left-right">
-          <h2>法律法规</h2>
-          <div class="small-more" @click="navTo('/introduction/policy')">
-            <span>更多信息</span>
-            <img src="@/assets/images/index/ButtonMore.png" alt="更多" />
-          </div>
-        </div>
-        <SimplePageContentLoader :loader="lawsData">
-          <ImageTextSmallBlock
-            v-for="(item, index) in lawsData.content.value"
-            :key="index"
-            :title="item.title"
-            :image="item.image"
-            :date="item.date"
-            @click="navTo('/news/detail', { id: item.id })"
-          />
-        </SimplePageContentLoader>
-      </div>
-    </section>
+
 
     <!-- 申报入口 -->
     <section class="main-section">
@@ -167,15 +145,15 @@ import Image10 from '@/assets/images/inheritor/Image10.jpg'
 import Image11 from '@/assets/images/inheritor/Image11.jpg'
 import LeftRightBox from '@/components/parts/LeftRightBox.vue';
 import ThreeImageList from '@/components/parts/ThreeImageList.vue';
-import ImageTextSmallBlock from '@/components/parts/ImageTextSmallBlock.vue';
 import { usePageAction } from '@/composeable/PageAction';
 import { useSimpleDataLoader } from '@/composeable/SimpleDataLoader';
 import PolicyContent from '@/api/introduction/PolicyContent';
-import { GetColumListParams, GetContentListParams } from '@/api/CommonContent';
+import CommonContent, { GetColumListParams, GetContentListParams } from '@/api/CommonContent';
 import DateUtils from '@/common/utils/DateUtils';
 import SimplePageContentLoader from '@/components/content/SimplePageContentLoader.vue';
 import { NO_CONTENT_STRING } from '@/common/ConstStrings';
 import IndexContent from '@/api/introduction/IndexContent';
+import { Item } from 'ant-design-vue/es/menu';
 
 const { navTo } = usePageAction();
 
@@ -241,14 +219,23 @@ const list2 = [
   },
 ]
 
-const lawsData = useSimpleDataLoader(async () => 
-  (await PolicyContent.getContentList(new GetContentListParams(), 1, 8))
-    .list?.map(item => ({
-      id: item.id,
-      title: item.title,
-      image: item.image || LawsTest,
-      date: DateUtils.formatDate(item.publish_at, DateUtils.FormatStrings.YearCommon),
-    }))
+const areaData = useSimpleDataLoader(async () => 
+ (await CommonContent.getContentList(new GetContentListParams()
+    .setModelId(17)
+    .setMainBodyColumnId(286)
+  , 1, 6)).list
+)
+const heritageData = useSimpleDataLoader(async () => 
+ (await CommonContent.getContentList(new GetContentListParams()
+    .setModelId(17)
+    .setMainBodyColumnId(310)
+  , 1, 6)).list
+)
+const blockData = useSimpleDataLoader(async () => 
+ (await CommonContent.getContentList(new GetContentListParams()
+    .setModelId(17)
+    .setMainBodyColumnId(286)
+  , 1, 6)).list
 )
 const overviewsLoader = useSimpleDataLoader(async () => {
   return [
@@ -324,9 +311,9 @@ const statsData = useSimpleDataLoader(async () => {
       })
     },
     {
-      title: '闽南文化重要相关文物古迹',
-      type: '2',
-      datas: data.minnanCr.map((item: any) => {
+      title: '传习中心',
+      type: '3',
+      datas: data.ichCenter.map((item: any) => {
         return {
           title: item.title,
           value: item.total
@@ -334,9 +321,9 @@ const statsData = useSimpleDataLoader(async () => {
       })
     },
     {
-      title: '重要相关历史风貌区',
-      type: '1',
-      datas: data.historyData.map((item: any) => {
+      title: '闽南文化重要相关文物古迹',
+      type: '2',
+      datas: data.minnanCr.map((item: any) => {
         return {
           title: item.title,
           value: item.total
@@ -344,9 +331,9 @@ const statsData = useSimpleDataLoader(async () => {
       })
     },
     {
-      title: '传习中心',
-      type: '3',
-      datas: data.ichCenter.map((item: any) => {
+      title: '重要相关历史风貌区',
+      type: '1',
+      datas: data.historyData.map((item: any) => {
         return {
           title: item.title,
           value: item.total

+ 14 - 1
src/views/IntrodView.vue

@@ -23,6 +23,8 @@
           title="闽南历史和地理背景"
           :desc="overviewsLoader.content.value?.[0]"
           :image="Image1"
+          :rightItems="historyData.content.value"
+          @rightItemDefaultClick="(item) => navTo('/news/detail', { id: item.id })"
           @moreClick="navTo('/introduction/history')"
         />
       </div>
@@ -40,6 +42,8 @@
           :desc="overviewsLoader.content.value?.[1]"
           :image="Image2"
           left
+          :rightItems="languageData.content.value"
+          @rightItemDefaultClick="(item) => navTo('/news/detail', { id: item.id })"
           @moreClick="navTo('/introduction/language')"
         />
       </div>
@@ -73,9 +77,11 @@ import CategoryImage6 from '@/assets/images/introduction/CategoryImage6.png';
 import LeftRightBox from '@/components/parts/LeftRightBox.vue';
 import ThreeImageList from '@/components/parts/ThreeImageList.vue';
 import { useSimpleDataLoader } from '@/composeable/SimpleDataLoader';
-import { GetColumListParams } from '@/api/CommonContent';
+import { GetColumListParams, GetContentListParams } from '@/api/CommonContent';
 import { NO_CONTENT_STRING } from '@/common/ConstStrings';
 import { usePageAction } from '@/composeable/PageAction';
+import HistoryContent from '@/api/introduction/HistoryContent';
+import LanguageContent from '@/api/introduction/LanguageContent';
 
 const { navTo } = usePageAction();
 
@@ -123,6 +129,13 @@ const list = [
   }
 ]
 
+const historyData = useSimpleDataLoader(async () => 
+ (await HistoryContent.getContentList(new GetContentListParams().setMainBodyColumnId([ 233, 250, 251 ]), 1, 6)).list
+)
+const languageData = useSimpleDataLoader(async () => 
+ (await LanguageContent.getContentList(new GetContentListParams(), 1, 6)).list
+)
+
 const overviewsLoader = useSimpleDataLoader(async () => {
   return [
     (await IndexContent.getColumList(

+ 1 - 1
src/views/NewsDetailView.vue

@@ -19,7 +19,7 @@
               <span class="small-info">作者:{{ newsLoader.content.value.author || '管理员' }}</span>
             </div>
             <div class="col-12 col-md-4 col-lg-4 col-xl-4 d-flex justify-content-center">
-              <span class="small-info">时间:{{ DateUtils.formatDate(newsLoader.content.value.publish_at, DateUtils.FormatStrings.YearCommon) }}</span>
+              <span class="small-info">时间:{{ DateUtils.formatDate(newsLoader.content.value.publishAt, DateUtils.FormatStrings.YearCommon) }}</span>
             </div>
             <div class="col-12 col-md-4 col-lg-4 col-xl-4 d-flex justify-content-center">
               <span class="small-info">浏览量:{{ newsLoader.content.value.views }}</span>

+ 4 - 4
src/views/NewsView.vue

@@ -70,7 +70,7 @@
             <TitleDescBlock
               :title="item.title"
               :desc="item.desc || item.title"
-              :date="DateUtils.formatDate(item.publish_at, DateUtils.FormatStrings.YearCommon)"
+              :date="DateUtils.formatDate(item.publishAt, DateUtils.FormatStrings.YearCommon)"
             />
           </div>
         </SimplePageContentLoader>
@@ -106,9 +106,9 @@ const carouselConfig = {
   autoPlay: 5000,
 }
 const mainTabs = [
-  { title: '活动公告', value: 0  },
-  { title: '文化热搜榜', value: 1  },
-  { title: '活动报名', value: 2  },
+  { title: '活动公告', value: 0 },
+  { title: '文化热搜榜', value: 1 },
+  { title: '活动报名', value: 2 },
 ]
 const mainTabActive = ref(0);
 

+ 49 - 120
src/views/details/ArtifactDetailView.vue

@@ -1,131 +1,60 @@
 <template>
   <!-- 文物详情页 -->
-  <div class="main-background">
-    <div class="nav-placeholder"></div>
-    <!-- 新闻 -->
-    <section class="main-section main-background main-background-type0 small-h">
-      <SimplePageContentLoader :loader="loader">
-        <div v-if="loader.content.value" class="content news-detail">
-          <div class="d-flex flex-row justify-content-start">
-            <div class="back-button2" @click="back">
-              <img src="@/assets/images/news/IconBack.png" />
-              <span>返回列表</span>
-            </div>
-          </div>
-
-          <h1>{{ loader.content.value.title }}</h1>
-          <p class="small-info">
-            {{ loader.content.value.address }} 
-          </p>
-
-          <!-- 轮播 -->
-          <Carousel 
-            :itemsToShow="1"
-            wrapAround
-            :autoPlay="5000"
-            class="carousel"
-          >
-            <Slide v-for="(image, key) in loader.content.value.images" :key="key">
-              <img :src="image" />
-            </Slide>
-            <template #addons>
-              <Navigation />
-              <Pagination />
-            </template>
-          </Carousel>
-
-          <div class="info-list mb-2">
-            <div class="entry">
-              <div class="label">开放时间:</div>
-              <div class="value">{{ loader.content.value.openStatusText || '暂无' }}</div>
-            </div>
-            <div class="entry">
-              <div class="label">年代:</div>
-              <div class="value">{{ loader.content.value.age || '暂无' }}</div>
-            </div>
-            <div class="entry">
-              <div class="label">文物类型:</div>
-              <div class="value">{{ loader.content.value.crTypeText || '暂无' }}</div>
-            </div>
-            <div class="entry">
-              <div class="label">所属区域:</div>
-              <div class="value">{{ loader.content.value.regionText || '暂无' }}</div>
-            </div>
-            <div class="entry">
-              <div class="label">级别:</div>
-              <div class="value">{{ loader.content.value.levelText || '暂无' }}</div>
-            </div>
-            <div class="entry">
-              <div class="label">保护范围:</div>
-              <div class="value">
-                <SimpleRichHtml 
-                  v-if="loader.content.value.protectedArea"
-                  :contents="[loader.content.value.protectedArea as string]" 
-                />
-                <span v-else>暂无</span>
-              </div>
-            </div>
-          </div>
-
-          <SimpleRichHtml 
-            class="news-content"
-            :contents="[
-              loader.content.value.intro,
-              loader.content.value.value,
-              loader.content.value.content,
-            ]" 
-          />
-          <ContentNode />
-
-          <div class="row d-flex justify-content-center">
-            <div class="back-button" @click="back">
-              <img src="@/assets/images/news/IconBack.png" />
-              <span>返回列表</span>
-            </div>
+  <TabDetailView 
+    :load="loadData"
+  >
+    <template #extraInfo="{ content }">
+      <div class="info-list mb-2">
+        <div class="entry">
+          <div class="label">开放时间:</div>
+          <div class="value">{{content.openStatusText || '暂无' }}</div>
+        </div>
+        <div class="entry">
+          <div class="label">年代:</div>
+          <div class="value">{{content.age || '暂无' }}</div>
+        </div>
+        <div class="entry">
+          <div class="label">文物类型:</div>
+          <div class="value">{{content.crTypeText || '暂无' }}</div>
+        </div>
+        <div class="entry">
+          <div class="label">所属区域:</div>
+          <div class="value">{{content.regionText || '暂无' }}</div>
+        </div>
+        <div class="entry">
+          <div class="label">级别:</div>
+          <div class="value">{{content.levelText || '暂无' }}</div>
+        </div>
+        <div class="entry">
+          <div class="label">保护范围:</div>
+          <div class="value">
+            <SimpleRichHtml 
+              v-if="content.protectedArea"
+              :contents="[content.protectedArea as string]" 
+            />
+            <span v-else>暂无</span>
           </div>
         </div>
-      </SimplePageContentLoader>
-    </section>
-  </div>
+      </div>
+    </template>
+  </TabDetailView>
 </template>
 
 <script setup lang="ts">
-import { Carousel, Slide, Pagination, Navigation } from 'vue3-carousel'
-import type { GetContentDetailItem } from '@/api/CommonContent';
 import UnmoveableContent from '@/api/inheritor/UnmoveableContent';
-import SimplePageContentLoader from '@/components/content/SimplePageContentLoader.vue';
-import SimpleRichHtml from '@/components/display/SimpleRichHtml.vue';
-import { useLoadQuerys } from '@/composeable/PageQuerys';
-import { useSimpleDataLoader } from '@/composeable/SimpleDataLoader';
-import { useRouter } from 'vue-router';
-import ContentNode from '@/components/content/ContentNode.vue';
-
-const router = useRouter();
-const loader = useSimpleDataLoader<
-  GetContentDetailItem, 
-  { id: number }
->(async (params) => {
-  if (!params)
-    throw new Error("!params");
-  return await UnmoveableContent.getContentDetail(params.id);
-});
-
-useLoadQuerys({
-  id: 0
-}, async ({ id }) => {
-  if (id <= 0) {
-    router.push({ name: 'NotFound' });
-    return;
-  }
-  loader.loadData({ id });
-})
-
-function back() {
-  router.back();
+import TabDetailView from './TabDetailView.vue';
+
+async function loadData(id: number) {
+  const res = await UnmoveableContent.getContentDetail(id);
+  res.contentProps = {
+    tabs: [
+      { text: '文物基础信息', visible: true },
+      { text: '文物相册', visible: true },
+      { text: '文物视频', visible: true },
+    ]
+  };
+  return res;
 }
-</script>
 
-<style lang="scss">
-
-</style>
+</script>
 

+ 43 - 0
src/views/details/IntangibleDetailView.vue

@@ -0,0 +1,43 @@
+<template>
+  <!-- 文物详情页 -->
+  <TabDetailView 
+    :load="loadData"
+  >
+    <template #extraInfo="{ content }">
+      <div class="info-list mb-2">
+        <div class="entry">
+          <div class="label">非遗类型:</div>
+          <div class="value">{{content.ichTypeText || '暂无' }}</div>
+        </div>
+        <div class="entry">
+          <div class="label">所属区域:</div>
+          <div class="value">{{content.regionText || '暂无' }}</div>
+        </div>
+        <div class="entry">
+          <div class="label">级别:</div>
+          <div class="value">{{content.levelText || '暂无' }}</div>
+        </div>
+      </div>
+    </template>
+  </TabDetailView>
+</template>
+
+<script setup lang="ts">
+import TabDetailView from './TabDetailView.vue';
+import ProjectsContent from '@/api/inheritor/ProjectsContent';
+
+async function loadData(id: number) {
+  const res = await ProjectsContent.getContentDetail(id);
+  res.contentProps = {
+    tabs: [
+      { text: '非遗基础信息', visible: true },
+      { text: '非遗相册', visible: true },
+      { text: '非遗视频', visible: true },
+      { text: '非遗音频', visible: true },
+    ]
+  };
+  return res;
+}
+
+</script>
+

+ 187 - 0
src/views/details/TabDetailView.vue

@@ -0,0 +1,187 @@
+<template>
+  <!-- 文物详情页 -->
+  <div class="main-background">
+    <div class="nav-placeholder"></div>
+    <!-- 新闻 -->
+    <section class="main-section main-background main-background-type0 small-h">
+      <SimplePageContentLoader :loader="loader">
+        <div v-if="loader.content.value" class="content news-detail">
+          <div class="d-flex flex-row justify-content-start">
+            <div class="back-button2" @click="back">
+              <img src="@/assets/images/news/IconBack.png" />
+              <span>返回列表</span>
+            </div>
+          </div>
+
+          <h1>{{ loader.content.value.title }}</h1>
+          <p class="small-info">
+            {{ loader.content.value.address }} 
+          </p>
+
+          <!-- Tab -->
+          <TagBar
+            class="mb-3"
+            :tags="contentProps.tabs.filter(p => p.visible).map((p, i) => ({ id: i, name: p.text })) || []"
+            :margin="[30, 70]" 
+            v-model:selectedTag="currentTabIndex"
+          />
+
+          <!-- 基础信息 -->
+          <div v-if="currentTabIndex==0">
+            <!-- 轮播 -->
+            <Carousel 
+              :itemsToShow="1"
+              wrapAround
+              :autoPlay="5000"
+              class="carousel"
+            >
+              <Slide v-for="(image, key) in loader.content.value.images" :key="key">
+                <img :src="image" />
+              </Slide>
+              <template #addons>
+                <Navigation />
+                <Pagination />
+              </template>
+            </Carousel>
+
+            <slot name="extraInfo" :content="loader.content.value" />
+
+            <SimpleRichHtml 
+              class="news-content"
+              :contents="[
+                loader.content.value.intro,
+                loader.content.value.value,
+                loader.content.value.content,
+              ]" 
+            />
+          </div>
+          <!-- 图片 -->
+          <div v-else-if="currentTabIndex==1">
+            <ImageGrid
+              v-if="loader.content.value.images && loader.content.value.images.length > 0"
+              :data="loader.content.value.images"
+              imageHeight="300px"
+              @itemClick="handleShowImage"
+            >
+            </ImageGrid>    
+            <a-empty v-else />
+          </div>
+          <!-- 视频 -->
+          <div v-else-if="currentTabIndex==2">
+            <video 
+              v-if="loader.content.value.video"
+              class="news-video mt-3"
+              controls
+              :src="loader.content.value.video" 
+            />
+            <a-empty v-else />
+          </div>
+          <!-- 音频 -->
+          <div v-else-if="currentTabIndex==3">
+            <video 
+              v-if="loader.content.value.audio"
+              class="news-video mt-3"
+              controls
+              :src="loader.content.value.audio" 
+            />
+            <a-empty v-else />
+          </div>
+          <!-- 其他 -->
+          <div v-else>
+            <slot name="extraTab" :index="currentTabIndex" />
+          </div>
+
+          <ContentNode />
+
+          <div class="row d-flex justify-content-center">
+            <div class="back-button" @click="back">
+              <img src="@/assets/images/news/IconBack.png" />
+              <span>返回列表</span>
+            </div>
+          </div>
+        </div>
+      </SimplePageContentLoader>
+    </section>
+    <a-image
+      :width="200"
+      :style="{ display: 'none' }"
+      :preview="{
+        visible: imagePreviewVisible,
+        onVisibleChange: (v: boolean) => imagePreviewVisible = v,
+      }"
+      :src="imagePreviewSrc"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { Carousel, Slide, Pagination, Navigation } from 'vue3-carousel'
+import type { GetContentDetailItem } from '@/api/CommonContent';
+import SimplePageContentLoader from '@/components/content/SimplePageContentLoader.vue';
+import SimpleRichHtml from '@/components/display/SimpleRichHtml.vue';
+import { useLoadQuerys } from '@/composeable/PageQuerys';
+import { useSimpleDataLoader } from '@/composeable/SimpleDataLoader';
+import { useRouter } from 'vue-router';
+import ContentNode from '@/components/content/ContentNode.vue';
+import { computed, ref, type PropType } from 'vue';
+import TagBar from '@/components/content/TagBar.vue';
+import ImageGrid from '@/components/content/ImageGrid.vue';
+
+const props = defineProps({
+  load: {
+    type: Function as PropType<(id: number) => Promise<GetContentDetailItem>>,
+    default: null,
+  }
+})
+
+const imagePreviewVisible = ref(false);
+const imagePreviewSrc = ref('');
+
+function handleShowImage(url: string) {
+  imagePreviewVisible.value = true;
+  imagePreviewSrc.value = url;
+}
+
+const router = useRouter();
+const loader = useSimpleDataLoader<
+  GetContentDetailItem, 
+  { id: number }
+>(async (params) => {
+  if (!params)
+    throw new Error("!params");
+  if (!props.load)
+    throw new Error("!props.load");
+  return props.load(params.id);
+});
+
+const currentTabIndex = ref(0);
+const contentProps = computed(() => {
+  return loader.content.value?.contentProps as {
+    tabs: { 
+      text: string,
+      visible: boolean,
+    }[],
+  } ?? {
+    tabs: [],
+  };
+})
+
+useLoadQuerys({
+  id: 0
+}, async ({ id }) => {
+  if (id <= 0) {
+    router.push({ name: 'NotFound' });
+    return;
+  }
+  loader.loadData({ id });
+})
+
+function back() {
+  router.back();
+}
+</script>
+
+<style lang="scss">
+
+</style>
+

+ 2 - 2
src/views/inheritor/area.vue

@@ -1,7 +1,7 @@
 <template>
-  <!-- 文化传承 - 历史风貌区 -->
+  <!-- 文化传承 - 重要相关文物古迹 -->
   <CommonListPage
-    :title="'历史风貌区'"
+    :title="'重要相关文物古迹'"
     :prevPage="{ title: '文化传承' }"
     :dropDownNames="[]"
     :showSearch="true"

+ 1 - 0
src/views/inheritor/products.vue

@@ -9,6 +9,7 @@
     :loadDetail="loadDetail"
     :tagsData="tagsData"
     :defaultSelectTag="tagsData[0].id"
+    detailsPage="/inheritor/intangible-detail"
   />
 </template>
 

+ 1 - 0
src/views/inheritor/projects.vue

@@ -9,6 +9,7 @@
     :loadDetail="loadDetail"
     :tagsData="tagsData"
     :defaultSelectTag="tagsData[0].id"
+    detailsPage="/inheritor/intangible-detail"
   />
 </template>