소스 검색

🚧 重建整理优化项目5 传统村落页面迁移

快乐的梦鱼 1 개월 전
부모
커밋
9845ce84df

+ 1 - 0
components.d.ts

@@ -26,6 +26,7 @@ declare module 'vue' {
     ImagePreview: typeof import('./src/components/small/ImagePreview.vue')['default']
     ImageSwiper: typeof import('./src/components/small/ImageSwiper.vue')['default']
     NavBar: typeof import('./src/components/content/NavBar.vue')['default']
+    Page: typeof import('./src/components/parts/Page.vue')['default']
     PageLeftTitleRightContent: typeof import('./src/components/parts/PageLeftTitleRightContent.vue')['default']
     PageRoot: typeof import('./src/components/parts/PageRoot.vue')['default']
     PageTopTitleBottomContent: typeof import('./src/components/parts/PageTopTitleBottomContent.vue')['default']

+ 98 - 0
src/api/village/VillageApi.ts

@@ -0,0 +1,98 @@
+import { DataModel, transformArrayDataModel } from '@imengyu/js-request-transform';
+import { AppServerRequestModule } from '../RequestModules';
+
+export class VillageListItem extends DataModel<VillageListItem> {
+  constructor() {
+    super(VillageListItem, "村落列表");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      images: { clientSide: 'forceArray' }
+    }
+    this._nameMapperServer = {
+      name: 'villageName',
+    };
+    this._convertKeyType = (key, direction) => {
+      if (key.endsWith('At'))
+        return {
+          clientSide: 'date',
+          serverSide: 'string',
+        };
+      return undefined;
+    };
+    this._afterSolveServer = () => {
+      if (this.images && this.images && this.images.length > 0  ) {
+        this.image = this.images[0]
+      }
+    }
+  }
+
+  id !: number;
+  villageVolunteerId = null as number|null;
+  villageId = null as number|null;
+  claimReason = '';
+  status = '';
+  statusText = '';
+  createdAt = null as Date|null;
+  updatedAt = null as Date|null;
+  deleteAt = null as Date|null;
+  image = '';
+  images = [] as string[];
+  villageName = '';
+  volunteerName = '';
+}
+
+export class VillageMenuListItem extends DataModel<VillageMenuListItem> {
+  constructor() {
+    super(VillageMenuListItem, "村落菜单列表");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+    }
+    this._nameMapperServer = {
+    };
+    this._convertKeyType = (key, direction) => {
+      if (key.endsWith('At'))
+        return {
+          clientSide: 'date',
+          serverSide: 'string',
+        };
+      return undefined;
+    };
+
+  }
+
+  id = 0;
+  name = '';
+  logo = '';
+  region = 0;
+  modelId = 0;
+  mainBodyColumnId = 0;
+}
+
+export class VillageApi extends AppServerRequestModule<DataModel> {
+
+  constructor() {
+    super();
+  }
+
+  async getVillageList(level?: number) {
+    return (this.get('/village/village/getList', '村落列表', {
+      history_level: level,
+    })) 
+      .then(res => transformArrayDataModel<VillageListItem>(VillageListItem, res.data2, `村落`, true))
+      .catch(e => { throw e });
+  }
+  async getVillageMenuList(id: number) {
+    return (this.get('/village/menu/getList', '村落菜单列表', {
+      platform: 1,
+      village_id: id,
+    })) 
+      .then(res => transformArrayDataModel<VillageMenuListItem>(VillageMenuListItem, res.data2, `村落菜单`, true))
+      .catch(e => { throw e });
+  }
+
+  
+}
+
+export default new VillageApi();

BIN
src/assets/images/IconMap.png


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

@@ -193,6 +193,11 @@ main {
   }
 }
 .main-round-box {
+
+  &.flat {
+    padding: 0;
+  }
+
   padding: 1vw;
   border-radius: 1vw;
   box-sizing: border-box;

+ 15 - 0
src/components/parts/Page.vue

@@ -0,0 +1,15 @@
+<script setup lang="ts">
+import Header from './Header.vue';
+
+</script>
+
+<template>
+  <main class="main-content main-bg main-bg1">
+    <Header show-back absolute />
+    <slot name="out">
+      <div class="inner fill pt-0">
+        <slot />
+      </div>
+    </slot>
+  </main>
+</template>

+ 38 - 0
src/composeable/PageQuerys.ts

@@ -0,0 +1,38 @@
+import { nextTick, onMounted, ref, watch, type Ref } from "vue";
+import { useRoute } from "vue-router";
+
+export function useLoadQuerys<T extends Record<string, any>>(
+  defaults: T, 
+  afterLoad?: (querys: T) => void
+) {
+
+  const querys = ref<T>(defaults) as Ref<T>; 
+  const route = useRoute();
+
+  function loadQuerys() {
+    const _querys = route.query;
+    if (_querys) {
+      for (const key in querys.value) {
+        if (typeof defaults[key] === 'number')
+          (querys.value as Record<string, any>)[key] = Number(_querys[key]); 
+        else
+          querys.value[key] = _querys[key] as any;
+      }
+    }
+    afterLoad?.(querys.value);
+  }
+
+  watch(route, () => {
+    loadQuerys();
+  });
+
+  onMounted(() => {
+    nextTick(() => {
+      loadQuerys();
+    });
+  });
+
+  return {
+    querys,
+  }
+}

+ 10 - 0
src/router/index.ts

@@ -55,6 +55,16 @@ const router = createRouter({
       component: () => import('../views/Intangible/Map.vue'),
     },
 
+    {
+      path: '/village/list',
+      name: 'VillageList',
+      component: () => import('../views/Intangible/village/list.vue'),
+    },
+    {
+      path: '/village/detail',
+      name: 'VillageDetail',
+      component: () => import('../views/Intangible/village/detail.vue'),
+    },
 
   ],
 })

+ 75 - 0
src/views/Content/TabVillageList.vue

@@ -0,0 +1,75 @@
+<template>
+  <SimplePageListContentLoader class="d-flex flex-col flex-fill" :loader="loader">
+    <Tab :tabs="tagsData" v-model="tab" autoSize itemWidth="100px" />
+    <GridList :list="loader.content.value?.list" class="flex-fill" @item-click="handleItemClick"  />
+  </SimplePageListContentLoader>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, ref, watch } from 'vue';
+import { useSimpleDataLoader } from '@/composeable/SimpleDataLoader';
+import { useRouter } from 'vue-router';
+import SimplePageListContentLoader from '@/components/SimplePageListContentLoader.vue';
+import GridList from '@/components/small/GridList.vue';
+import { ScrollRect } from '@imengyu/vue-scroll-rect';
+import VillageApi from '@/api/village/VillageApi';
+import CommonContent from '@/api/CommonContent';
+import Tab from '@/components/small/Tab.vue';
+
+const emit = defineEmits(['itemClick']);
+const router = useRouter();
+const tab = ref(0);
+const tabId = computed(() => {
+  return tagsData.value[tab.value]?.id || 0;
+})
+const loader = useSimpleDataLoader(async () => {
+  const res = await VillageApi.getVillageList(tabId.value);
+  return { 
+    page: 1,
+    total: res.length,
+    list: res
+      .map((item, index) => {
+        return {
+          title: item.villageName,
+          desc: item.desc,
+          ...item,
+          addItems: [],
+          bottomTags: [
+            item.levelText, 
+            item.batchText,
+            item.historyLevelText,
+          ],
+        };
+      })
+    ,
+  }
+});
+
+watch(tab, () => loader.loadData(undefined, true))
+
+//子分类
+const tagsData = ref<{
+  id: number,
+  label: string,
+}[]>([]);
+
+onMounted(async () => {
+  const res = await CommonContent.getCategoryList(151);
+  const it1 = res.find(p => p.title == '国家级');
+  const it2 = res.find(p => p.title == '省级');
+  if (it1) it1.title = '特色村舍';
+  if (it2) it2.title = '传统村落';
+  tagsData.value = res.slice(1).map((p) => ({ id: p.id, label: p.title }));
+})
+
+function handleItemClick(item: any) {
+  localStorage.setItem('VillageTemp', JSON.stringify(item));
+  setTimeout(() => {
+    router.push({ name: 'VillageDetail', query: { id: item.id } });
+  }, 200);
+}
+</script>
+
+<style scoped lang="scss">
+</style>
+

+ 2 - 0
src/views/ContentView.vue

@@ -6,6 +6,7 @@ import TabCommonList from './Content/TabCommonList.vue';
 import Header from '@/components/parts/Header.vue';
 import TabCustomList from './Content/TabCustomList.vue';
 import TabInherit from './Content/TabInherit.vue';
+import TabVillageList from './Content/TabVillageList.vue';
 
 const route = useRoute();
 const router = useRouter();
@@ -105,6 +106,7 @@ watch(() => activeTab.value, (newVal) => {
           :mainBodyColumnId="[ 275,276,277,278 ]" 
           :mainBodyColumnAsTabs="[ '非遗旅游路线', '红色文化旅游路线', '美食旅游路线', '文旅融合示范点' ]"
         />
+        <TabVillageList v-else-if="activeTabId === 8" />
       </div>
     </div>
   </main>

+ 13 - 15
src/views/Intangible/List.vue

@@ -1,19 +1,16 @@
 <template>
-  <main class="main-content main-bg main-bg1">
-    <Header show-back />
-    <div class="inner fill pt-0">
-      <SimplePageListContentLoader class="list d-flex flex-col mt-3 flex-fill" :loader="loader">
-        <GridList 
-          class="flex-fill"
-          :list="loader.list.value"
-          :defaultImage="AppCofig.defaultImage" 
-          :playAudio="detailPageName === 'Play'"
-          @itemClick="handleClick"
-        />
-        <SimplePageListContentPager :loader="loader" />
-      </SimplePageListContentLoader>
-    </div>
-  </main>
+  <Page >
+    <SimplePageListContentLoader class="list d-flex flex-col mt-3 flex-fill" :loader="loader">
+      <GridList 
+        class="flex-fill"
+        :list="loader.list.value"
+        :defaultImage="AppCofig.defaultImage" 
+        :playAudio="detailPageName === 'Play'"
+        @itemClick="handleClick"
+      />
+      <SimplePageListContentPager :loader="loader" />
+    </SimplePageListContentLoader>
+  </Page>
 </template>
 
 <script setup lang="ts">
@@ -26,6 +23,7 @@ import SimplePageListContentPager from '@/components/SimplePageListContentPager.
 import GridList from '@/components/small/GridList.vue';
 import Header from '@/components/parts/Header.vue';
 import AppCofig from '@/common/config/AppCofig';
+import Page from '@/components/parts/Page.vue';
 
 const emit = defineEmits(['itemClick']);
 const route = useRoute();

+ 1 - 1
src/views/Intangible/Map.vue

@@ -4,7 +4,7 @@ import Header from '@/components/parts/Header.vue';
 import SimplePopup from '@/components/SimplePopup.vue';
 import { useSimpleDataLoader } from '@/composeable/SimpleDataLoader';
 import { ScrollRect } from '@imengyu/vue-scroll-rect';
-import { nextTick, ref } from 'vue';
+import { ref } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 import ArtifactDetail from '../Details/ArtifactDetail.vue';
 

+ 205 - 0
src/views/Intangible/village/detail.vue

@@ -0,0 +1,205 @@
+<template>
+  <Page>
+    <template #out>
+      <ScrollRect class="d-flex flex-column village-details" scroll="vertical">
+        <div
+          v-if="loading"
+          class="d-flex justify-content-center align-items-center"
+          style="min-height: 200px;"
+        >
+          <a-spin tip="加载中" />
+        </div>
+        <template v-else>
+          <!-- 轮播图 -->
+          <ImageSwiper
+            v-if="data.images && data.images.length > 0"
+            class="main-round-box flat"
+            :items="data.images"
+            :autoplay="2500"
+            style="height:300px"
+          >
+            <template #item="{ item }">
+              <img class="swiper-slide" :src="item" />
+            </template>
+          </ImageSwiper>
+          <img 
+            v-else 
+            :src="data.image" 
+            class="swiper-slide main-round-box flat"
+          />
+          <div class="mt-3" />
+          <div class="d-flex flex-col content-box">
+            <SimpleRichHtml
+              v-if="data.overview"
+              :contents="[ data.overview ]"
+              :tag-style="{
+                a: 'text-decoration: underline ; color: #fff;',
+                p: 'color: #fff; margin-bottom: 20px;',
+                img: 'border-radius: 10px;'
+              }"
+            />
+            <span v-else>无内容,请添加内容!</span>
+          </div>
+          <div class="mt-3" />
+          <div class="d-flex flex-row flex-wrap">
+            <div
+              v-for="(item, k) in tagsData"
+              :key="k"
+              class="tag-item main-any-button"
+              :tabindex="1"
+              @click="router.push({
+                path: '/village/list',
+                query: { 
+                  id: item.id,
+                  model_id: item.modelId,
+                  main_body_column_id: item.mainBodyColumnId,
+                  region: item.region,
+                }, 
+              })"
+            >
+              <img :src="item.logo" />
+              <span>{{ item.name }}</span>
+            </div>
+          </div>
+          <div class="mt-3" />
+          <div class="map-container main-round-box flat">
+            <el-amap
+              style="width: 100%;"
+              :center="center"
+              :zoom="zoom"
+              @init="handleInit"
+            />
+          </div>
+          <div style="height: 40px;flex-shrink: 0;" />
+        </template>
+      </ScrollRect>
+    </template>
+  </Page>
+</template>
+
+<script setup lang="ts">
+import { useRoute, useRouter } from 'vue-router';
+import { ref, watch } from 'vue';
+import { ElAmap } from '@vuemap/vue-amap';
+import { ScrollRect } from '@imengyu/vue-scroll-rect';
+import VillageApi, { VillageMenuListItem } from '@/api/village/VillageApi';
+import ImageSwiper from '@/components/small/ImageSwiper.vue';
+import Page from '@/components/parts/Page.vue';
+import SimpleRichHtml from '@/components/SimpleRichHtml.vue';
+import IconMap from '@/assets/images/IconMap.png';
+
+const tagsData = ref<VillageMenuListItem[]>([]);
+const route = useRoute();
+const router = useRouter();
+const data = ref({
+  image: '',
+  images: [],
+  overview: '',
+  longitude: "",
+  latitude: '',
+  modelId: 0,
+  mainBodyColumnId: 0,
+  region: 0,
+})
+const loading = ref(true);
+const zoom = ref(12);
+const center = ref([121.59996, 31.197646]);
+let map: any = null;
+
+function handleInit(mapRef: any) {
+  map = mapRef;
+}
+
+watch(route, () => {
+  loading.value = true;
+  setTimeout(() => {
+    loadInfo();
+  }, 500);
+}, { immediate: true })
+
+async function loadInfo() {
+
+  loading.value = true;
+  const id = Number(route.query.id);
+  data.value = {
+    ...data.value,
+    ...JSON.parse(localStorage.getItem('VillageTemp') || '{}'),
+  };
+
+  if (data.value.longitude && data.value.latitude) {
+    center.value = [Number(data.value.longitude), Number(data.value.latitude)];
+  } else {
+    center.value = [118.850895, 28.982787];
+  }  
+  map?.add(new AMap.Marker({
+    position: center.value as [number, number],
+    icon: IconMap,
+  }));
+
+  const menu = await VillageApi.getVillageMenuList(id);
+
+  loading.value = false;
+  tagsData.value = menu;
+}
+
+</script>
+    
+<style lang="scss">
+
+.village-details {
+  position: absolute;
+  top: 100px;
+  left:  15%;
+  right: 15%;
+  bottom: 5%;
+  width: unset;
+  height: unset;
+  font-size: 0.8rem;
+  color: var(--color-text);
+
+  .swiper-slide {
+    width: 100%;
+    height: 400px;
+    object-fit: cover;
+  }
+  .tag-item {
+    width: 13%;
+    margin-bottom: 0.8rem;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    text-decoration: none;
+    color: var(--color-text);
+
+    img {
+      width: 60px;
+      height: 60px;
+    }
+  }
+  .content-box {
+    width: 100%;
+    padding: 20px;
+    background-color: var(--color-light-bg);
+    border: 1px solid var(--color-border);
+    box-shadow: 0 0 10px 5px rgba(0, 0, 0, 0.1);
+
+    border-radius: 8px;
+  }
+  .map-container {
+    position: relative;
+    width: 100%;
+    height: 250px;
+    margin: 40px 0;
+    overflow: hidden;
+    border-radius: 8px;
+    flex-shrink: 0;
+
+    .el-vue-amap-container {
+      width: 100%;
+    }
+  }
+}
+
+</style>

+ 61 - 0
src/views/Intangible/village/list.vue

@@ -0,0 +1,61 @@
+<template>
+  <Page>
+    <div class="content">
+      <!-- 传统村落 -->
+      <TabCommonList
+        :loader="loadData"
+        class="h-100"
+      />
+    </div>
+  </Page>
+</template>
+
+<script setup lang="ts">
+import { ref, watch } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import CommonContent, { GetContentListParams } from '@/api/CommonContent';
+import TabCommonList from '@/views/Content/TabCommonList.vue';
+import Page from '@/components/parts/Page.vue';
+
+const router = useRouter();
+const route = useRoute();
+const list = ref();
+
+async function showDetail(item: any) {
+  router.push({ path: '/village/content', query: { id: item.id } });
+}
+async function loadData(
+  page: number, 
+  pageSize: number,
+) {
+
+  const res = await CommonContent.getContentList(new GetContentListParams()
+    .setModelId(Number(route.query.model_id))
+    .setMainBodyColumnId(Number(route.query.main_body_column_id))
+    .setSelfValues({
+      region: Number(route.query.region),
+    })
+  , page, pageSize);
+
+  return { 
+    page: page,
+    total: res.total,
+    list: res.list
+  }
+}
+
+watch(route, (newVal) => {
+  list.value.reload();
+})
+</script>
+
+<style scoped>
+.content {
+  position: absolute;
+  top: 100px;
+  left:  15%;
+  right: 15%;
+  bottom: 5%;
+}
+</style>
+