Selaa lähdekoodia

对接内容1

快乐的梦鱼 2 kuukautta sitten
vanhempi
commit
3d921fa6fc

+ 68 - 0
src/common/components/SimpleDropDownPicker.vue

@@ -0,0 +1,68 @@
+<template>
+  <view 
+    class="simple-dropdown-box" 
+    @click="show=true"
+  >
+    <picker @change="bindPickerChange" :value="selectedIndex" :range="columns" range-key="name">
+      {{ dispayText }} ▼
+    </picker>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { computed, ref, type PropType } from 'vue';
+
+export interface SimpleDropDownPickerItem {
+  id: number,
+  name: string,
+}
+
+const props = defineProps({	
+  columns: {
+    type: Object as PropType<SimpleDropDownPickerItem[]|null>,
+    default: null,
+  },
+  modelValue: {
+    type: Number,
+    default: null,
+  },
+  defaultText: {
+    type: String,
+    default: '请选择',
+  },
+})
+
+const emit = defineEmits([
+  'update:modelValue', 
+])
+
+const show = ref(false);
+const dispayText = computed(() => {
+  if (props.columns) 
+    return props.columns.find(item => item.id == props.modelValue)?.name || props.defaultText;
+  return props.defaultText;
+});
+const selectedIndex = computed(() => {
+  let index = -1;
+  if (props.columns) 
+    index = props.columns.findIndex(item => item.id == props.modelValue);
+  return index >= 0 ? index : 0;
+});
+
+function bindPickerChange(e:{ detail: { value: number }}) {
+  show.value = false;
+  emit('update:modelValue', props.columns?.[e.detail.value].id || null);
+}
+</script>
+
+<style lang="scss">
+.simple-dropdown-box {
+  position: relative;
+  padding: 16rpx 18rpx;
+  border-radius: 30rpx;
+  background: #FFFFFF;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+</style>

+ 8 - 2
src/common/components/parts/ImageSwiper.vue

@@ -11,11 +11,17 @@
     }"
   >
     <swiper-item v-for="(item, key) in images" :key="key">
-      <view class="item">
+      <view 
+        class="item"
+        :style="{
+          backgroundImage: `url(${item})`,
+          backgroundSize: 'cover',
+        }"
+      >
         <image
           :src="item"
           class="w-100 radius-base"
-          mode="aspectFill"
+          mode="aspectFit"
           @click="onPreviewImage(key)"
         />
       </view>

+ 35 - 0
src/common/composeabe/TabControl.ts

@@ -0,0 +1,35 @@
+import { computed, ref, watch } from "vue";
+
+export interface TabControlItem {
+  text: string,
+  [key: string]: any,
+}
+
+export function useTabControl(options: {
+  tabs?: TabControlItem[],
+  onTabChange?: (tab: number, tabId: number) => void,
+}) {
+
+  const tabCurrentIndex = ref(0)
+  const tabCurrentId = computed(() => {
+    return tabsArray.value.filter(t => t.visible !== false)[tabCurrentIndex.value].id
+  });
+  const tabsArray = ref<TabControlItem[]>(options.tabs ?? []);
+
+  watch(tabCurrentIndex, (v) => {
+    options.onTabChange?.(v, tabCurrentId.value) 
+    if (tabsArray.value[v].jump)
+      tabsArray.value[v].jump()
+  })
+
+  const tabs = computed(() => {
+    return tabsArray.value.filter(t => t.visible !== false)
+  })
+
+  return {
+    tabCurrentId,
+    tabCurrentIndex,
+    tabs,
+    tabsArray,
+  }
+}

+ 1 - 0
src/common/config/AppCofig.ts

@@ -13,6 +13,7 @@ export default {
     '/pages/user/register',
     '/pages/user/reset-password',
   ],
+  defaultImage: 'https://mncdn.wenlvti.net/app_static/minnan/EmptyImage.png',
 }
 
 /**

+ 2 - 2
src/common/scss/define/colors.scss

@@ -1,5 +1,5 @@
 $colors: (
-  primary: #da7f08,
+  primary: #00b66a,
   text: #333232,
   text-content: #3d2c08,
   text-content-second: #817b67, 
@@ -24,7 +24,7 @@ $colors: (
   brown: #5f1a00,
   black: #000,
   white: #fff,
-  base: #f6f2e7,
+  base: #f7f8f9,
   custom: #4A5061,
   link: #0273F1,
   light: #fbfaf5,

+ 8 - 0
src/common/style/commonParserStyle.ts

@@ -0,0 +1,8 @@
+export default {
+  p: 'line-height:1.76;font-size:30rpx;margin-bottom:37rpx;color:#111111;text-align:justify;',
+  div: 'line-height:1.76;font-size:30rpx;margin-bottom:37rpx;color:#111111;text-align:justify;',
+  img: 'display:block;max-width:100%;height:auto;margin-bottom:30rpx;',
+  h2:'margin-bottom:30rpx;font-size:32rpx;line-height:1.7;',
+  h1:'margin-bottom:30rpx;font-size:36rpx;line-height:1.7;',
+  h3:'margin-bottom:30rpx;font-size:30rpx;line-height:1.7;',
+} as Record<string, string>;

+ 42 - 6
src/pages.json

@@ -18,7 +18,7 @@
       "path": "pages/index",
       "style": {
         "navigationStyle": "custom",
-        "navigationBarTitleText" : "乡源·乡村文化资源挖掘平台",
+        "navigationBarTitleText" : "村文化资源挖掘平台",
         "enablePullDownRefresh" : false
       }
     },
@@ -39,35 +39,35 @@
     {
       "path": "pages/dig/details",
       "style": {
-        "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-详情",
+        "navigationBarTitleText": "村文化资源挖掘平台-详情",
         "enablePullDownRefresh": false
       }
     },
     {
       "path": "pages/dig/admin",
       "style": {
-        "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-管理员",
+        "navigationBarTitleText": "村文化资源挖掘平台-管理员",
         "enablePullDownRefresh": false
       }
     },
     {
       "path": "pages/dig/admin/volunteer",
       "style": {
-        "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-志愿者管理",
+        "navigationBarTitleText": "村文化资源挖掘平台-志愿者管理",
         "enablePullDownRefresh": false
       }
     },
     {
       "path": "pages/dig/forms/task",
       "style": {
-        "navigationBarTitleText": "乡源·乡村文化资源挖掘平台",
+        "navigationBarTitleText": "村文化资源挖掘平台",
         "enablePullDownRefresh": false
       }
     },
     {
       "path": "pages/dig/forms/common",
       "style": {
-        "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-提交信息",
+        "navigationBarTitleText": "村文化资源挖掘平台-提交信息",
         "enablePullDownRefresh": false
       }
     },
@@ -91,6 +91,42 @@
         "navigationBarTitleText": "预览文章",
         "enablePullDownRefresh": false
       }
+    },
+    
+    {
+      "path": "pages/article/details",
+      "style": {
+        "navigationBarTitleText": "新闻详情"
+      }
+    },
+    {
+      "path": "pages/article/list",
+      "style": {
+        "navigationBarTitleText": "文章列表页",
+        "enablePullDownRefresh": true
+      }
+    },
+    {
+      "path": "pages/article/web/ewebview",
+      "style": {
+        "navigationBarTitleText": "",
+        "navigationStyle": "custom",
+        "enablePullDownRefresh": false
+      }
+    },
+    {
+      "path": "pages/article/common/list",
+      "style": {
+        "navigationBarTitleText": "通用列表页",
+        "enablePullDownRefresh": true
+      }
+    },
+    {
+      "path": "pages/home/village/details",
+      "style": {
+        "navigationBarTitleText": "村社详情",
+        "enablePullDownRefresh": true
+      }
     }
   ],
   "globalStyle": {

+ 55 - 0
src/pages/article/common/CommonContent.ts

@@ -0,0 +1,55 @@
+import CommonContent, { GetContentListItem, GetContentListParams } from "@/api/CommonContent";
+import { useSimpleDataLoader, type ISimpleDataLoader } from "@/common/composeabe/SimpleDataLoader";
+import { navTo } from "@/components/utils/PageAction";
+
+export interface IHomePageMiniCommonListGoMoreAndGoDetail {
+  loader: ISimpleDataLoader<GetContentListItem[], any>;
+  goDetail: (id: number) => void;
+  goList: () => void; 
+}
+
+/**
+ * 专用于通用内容的首页小列表控制代码组合
+ * @param p 
+ * @returns 
+ */
+export function useHomePageMiniCommonListGoMoreAndGoDetail(p: {
+  title?: string,
+  mainBodyColumnId?: number|number[],
+  modelId?: number,
+  itemType?: string,
+  detailsPage: string,
+  count?: number,
+}) : IHomePageMiniCommonListGoMoreAndGoDetail {
+  function goDetail(id: number) {
+    navTo(p.detailsPage, {
+      mainBodyColumnId: p.mainBodyColumnId,
+      modelId: p.modelId,
+      id,
+    }) 
+  }
+  function goList() {
+    navTo('/pages/article/common/list', {
+      title: p.title,
+      mainBodyColumnId: typeof p.mainBodyColumnId == 'object' ? 
+        p.mainBodyColumnId.join(',') : 
+        p.mainBodyColumnId,
+      modelId: p.modelId,
+      itemType: p.itemType,
+      detailsPage: p.detailsPage,
+    }) 
+  }
+
+  const loader = useSimpleDataLoader(async () => 
+    (await CommonContent.getContentList(new GetContentListParams().setSelfValues({
+      mainBodyColumnId: p.mainBodyColumnId,
+      modelId: p.modelId,
+    }), 1, p.count ?? 4)).list
+  );
+
+  return {
+    loader,
+    goDetail,
+    goList,
+  }
+}

+ 354 - 0
src/pages/article/common/CommonListPage.vue

@@ -0,0 +1,354 @@
+<template>
+  <!-- 通用列表页 -->
+  <view 
+    :class="[
+      'common-list-page d-flex flex-column', 
+      hasBg ? 'bg-base p-3' : ''
+    ]"
+  >
+    <view v-if="tabs" class="top-tab bg-base">
+      <Tabs
+        :tabs="tabs" 
+        :width="700"
+        v-model:currentIndex="tabCurrentIndex"
+        :autoScroll="false"
+        @click="handleTabClick"
+      />
+    </view>
+    <!-- 搜索 -->
+    <view v-if="showSearch" class="d-flex flex-col">
+      <SearchBar
+        v-model="searchValue"
+        :placeholder="`输入关键词搜索${title}`" 
+        @search="doSearch"
+        @cancel="doSearch"
+      />
+    </view>
+    <!-- 下拉框 -->
+    <view 
+      v-if="dropDownNames.length > 0" 
+      class="d-flex flex-row justify-between align-center mt-2"
+      :class="[
+        dropDownVisibleCount >= 3 ? 'justify-around' : ('justify-between')
+      ]"
+    >
+      <template v-for="(drop, k) in dropDownNames" :key="k" >
+        <SimpleDropDownPicker 
+          v-if="!drop.activeTab || drop.activeTab.includes(tabCurrentIndex)"
+          :modelValue="dropDownValues[k]"
+          :columns="drop.options"
+          :style="{maxWidth: `${100/dropDownNames.length}%`}"
+          @update:modelValue="(v) => handleChangeDropDownValue(k, v)"
+        />
+      </template>
+      <view 
+        v-if="(showTotal && dropDownVisibleCount < 3)" 
+        class="d-flex flex-row align-center mt-3 size-s color-primary text-bold"
+      >
+        <text>总共有 {{ listLoader.total }} 个</text>
+      </view>
+    </view>
+    <view 
+      v-if="(dropDownVisibleCount >= 3 || dropDownVisibleCount == 0)" 
+      class="d-flex flex-row justify-center align-center mt-3 size-s color-primary text-bold"
+    >
+      <text>总共有 {{ listLoader.total }} 个</text>
+    </view>
+    
+    <!-- 列表 -->
+    <view class="position-relative d-flex flex-row flex-wrap justify-between align-stretch mt-3">
+      <view
+        v-for="(item, i) in listLoader.list.value"
+        :key="item.id"
+        :class="[
+          'position-relative d-flex flex-grow-1',
+          itemType.endsWith('-2') ? 'width-1-2' : 'w-100'
+        ]"
+      >
+        <Box2LineLargeImageUserShadow 
+          v-if="itemType.startsWith('image-large')"
+          class="w-100"
+          titleColor="title-text"
+          :classNames="getItemClass(i)"
+          :image="getImage(item)"
+          :titleBox="item.titleBox"
+          :title="item.title"
+          :desc="item.desc"
+          :tags="item.bottomTags"
+          :badge="item.badge"
+          @click="goDetails(item, item.id)"
+        />
+        <Box2LineImageRightShadow 
+          v-else-if="itemType.startsWith('article-common')"
+          class="w-100"
+          titleColor="title-text"
+          :titleBox="item.titleBox"
+          :classNames="getItemClass(i)"
+          :image="getImage(item)"
+          :title="item.title"
+          :desc="item.desc"
+          :tags="item.bottomTags"
+          :badge="item.badge"
+          :wideImage="true"
+          @click="goDetails(item, item.id)"
+        />
+        <Box2LineImageRightShadow 
+          v-else-if="itemType.startsWith('article-character')"
+          class="w-100"
+          :classNames="getItemClass(i)"
+          :image="getImage(item)"
+          titleColor="title-text"
+          :title="item.title"
+          :titleBox="item.titleBox"
+          :tags="item.bottomTags || item.keywords"
+          :desc="item.desc"
+          :badge="item.badge"
+          @click="goDetails(item, item.id)"
+        />
+
+      </view>
+      <view v-if="itemType.endsWith('-2') && listLoader.list.value.length % 2 != 0" class="width-1-2" />
+    </view>
+    <SimplePageListLoader :loader="listLoader" />
+  </view>
+</template>
+
+<script setup lang="ts">
+import { computed, nextTick, onMounted, ref, watch, type PropType } from 'vue';
+import { useSimplePageListLoader } from '@/common/composeabe/SimplePageListLoader';
+import { navTo } from '@/components/utils/PageAction';
+import SimplePageListLoader from '@/common/components/SimplePageListLoader.vue';
+import Box2LineLargeImageUserShadow from '@/common/components/parts/Box2LineLargeImageUserShadow.vue';
+import Box2LineImageRightShadow from '@/common/components/parts/Box2LineImageRightShadow.vue';
+import SimpleDropDownPicker, { type SimpleDropDownPickerItem } from '@/common/components/SimpleDropDownPicker.vue';
+import AppCofig from '@/common/config/AppCofig';
+import Tabs from '@/components/nav/Tabs.vue';
+import SearchBar from '@/components/form/SearchBar.vue';
+
+function getImage(item: any) {
+  return item.thumbnail || item.image || AppCofig.defaultImage
+}
+function getItemClass(index: number) {
+  return props.itemType.endsWith('-2') ? (index % 2 != 0 ? 'ml-1' : 'mr-1') : ''
+}
+
+export interface DropDownNames {
+  options: SimpleDropDownPickerItem[],
+  defaultSelectedValue: number|string,
+  activeTab?: number[],
+}
+export interface CommonListItem extends Record<string, any>  {
+  id: number,
+  image: string,
+  title: string,
+}
+
+const props = defineProps({
+  /** 
+   * 标题
+   */
+  title: {
+    type: String,
+    default: '',
+  },
+  /**
+   * 分组标签
+   */
+  tabs: {
+    type: Array as PropType<{ 
+      id: number, 
+      text: string,
+      jump?: () => void,
+    }[]>,
+    default: null,
+  },
+  tabsScrollable: {
+    type: Boolean,
+    default: false, 
+  },
+  /**
+   * 是否显示搜索框
+   */
+  showSearch: {
+    type: Boolean,
+    default: true,
+  },
+  /**
+   * 显示总数
+   */
+  showTotal: {
+    type: Boolean,
+    default: false,
+  },
+  /**
+   * 下拉框选项控制
+   */
+  dropDownNames: {
+    type: Object as PropType<DropDownNames[]>,
+    default: () => ([]),
+  },
+  /**
+   * 列表项类型
+   */
+  itemType: {
+    type: String as PropType<'image-large-2'|'image-large'|'article-common'|'article-character'>,
+    default: 'article-common',
+  },
+  /**
+   * 分页大小
+   */
+  pageSize: {
+    type: Number,
+    default: 8,
+  },
+  /**
+   * 加载数据函数
+   * @param page 页码,从1开始
+   * @param pageSize 分页大小
+   * @param searchText 搜索文本
+   * @param dropDownValues 下拉框值
+   */
+  load: {
+    type: Function as PropType<(
+      page: number, 
+      pageSize: number,
+      searchText: string,
+      dropDownValues: number[],
+      tabSelect: number,
+    ) => Promise<{ list: CommonListItem[], total: number }>>,
+    required: true,
+  },
+  /**
+   * 点击详情跳转页面路径
+   */
+  detailsPage: {
+    type: [String,Object],
+    default: '/pages/article/details'
+  },
+  /**
+   * 详情跳转页面参数
+   */
+  detailsParams: {
+    type: Object as PropType<Record<string, any>>,
+    default: () => ({})
+  },
+  hasBg: {
+    type: Boolean,
+    default: true,
+  },
+  startTabIndex: {
+    type: Number,
+    default: undefined, 
+  },
+  loadMounted: {
+    type: Boolean,
+    default: true,
+  },
+})
+
+const emit = defineEmits([ 'goCustomDetails' ])
+
+const dropDownVisibleCount = computed(() => {
+  let c = 0;
+  for (const element of props.dropDownNames) {
+    if (!element.activeTab || element.activeTab.includes(tabCurrentIndex.value))
+      c++;
+  }
+  return c;
+})
+const dropDownValues = ref<any>([]);
+const searchValue = ref('');
+const listLoader = useSimplePageListLoader(props.pageSize, async (page, pageSize) => {
+  return await props.load(
+    page, pageSize, 
+    searchValue.value,
+    dropDownValues.value,
+    props.tabs?.[tabCurrentIndex.value]?.id ?? tabCurrentIndex.value,
+  )
+});
+const tabCurrentIndex = ref(0)
+
+function handleChangeDropDownValue(index: number, value: number) {
+  dropDownValues.value[index] = value;
+  listLoader.loadData(undefined, true);
+}
+function handleTabClick(e: any) {
+  nextTick(() => {
+    if (props.tabs?.[tabCurrentIndex.value]?.jump) {
+      props.tabs[tabCurrentIndex.value].jump?.();
+      return;
+    }
+    listLoader.loadData(undefined, true);
+  })
+}
+function doSearch() {
+  listLoader.loadData(undefined, true);
+}
+function goDetails(item: any, id: number) {
+  if (props.detailsPage == 'custom') {
+    emit('goCustomDetails', item, id)
+    return;
+  }
+  if (typeof props.detailsPage === 'object' && typeof props.detailsPage[0] === 'string') {
+    navTo(props.detailsPage[tabCurrentIndex.value], { 
+      ...props.detailsParams, 
+      id 
+    })
+    return; 
+  }
+  if (typeof props.detailsPage == 'object' && typeof props.detailsPage[0] === 'object') {
+    const item = props.detailsPage[tabCurrentIndex.value];
+    navTo(item.page, { 
+      ...item.params, 
+      id 
+    })
+    return; 
+  }
+  navTo(props.detailsPage as string, { 
+    ...props.detailsParams,
+    id
+  })
+}
+
+function loadDropDownValues() {
+  dropDownValues.value = [];
+  for (const element of props.dropDownNames) {
+    dropDownValues.value.push(element.defaultSelectedValue);
+  }
+}
+
+watch(tabCurrentIndex, () => {
+  listLoader.loadData(undefined, true);
+});
+watch(() => props.startTabIndex, () => {
+  if (props.startTabIndex) {
+    tabCurrentIndex.value = props.startTabIndex;
+  }
+});
+watch(() => props.dropDownNames.length, () => {
+  loadDropDownValues();
+  listLoader.loadData(undefined, true);
+});
+
+defineExpose({
+  load: () => {
+    listLoader.loadData(undefined, true);
+  },
+})
+
+onMounted(() => {
+  if (props.startTabIndex)
+    tabCurrentIndex.value = props.startTabIndex;
+  if (props.title)
+    uni.setNavigationBarTitle({ title: props.title, })
+  loadDropDownValues();
+  if (props.loadMounted)
+    listLoader.loadData(undefined, true);
+});
+</script>
+
+<style lang="scss">
+.common-list-page {
+  min-height: 100vh; 
+}
+</style>

+ 214 - 0
src/pages/article/common/DetailTabPage.vue

@@ -0,0 +1,214 @@
+<template>
+  <!-- TAB分页的详情页 -->
+  <view class="d-flex flex-col bg-base">
+    <SimplePageContentLoader :loader="loader">
+      <template v-if="loader.content.value">
+        <view class="d-flex flex-col">
+
+          <!-- 轮播大图 -->
+          <ImageSwiper 
+            v-if="showHead" 
+            :images="loader.content.value.images"
+          />
+
+          <!-- 标题区域 -->
+          <view class="d-flex flex-col mt-3 p-3">
+            <slot name="title" :content="loader.content.value">
+              <view class="d-flex flex-col">
+                <view class="d-flex flex-row align-center">
+                  <text :class="'size-lll font-songti font-bold color-text-content flex-shrink-1 mr-2' + (loader.content.value.titleBox ? ' border-all-text' : '')">
+                    {{ loader.content.value.title }}
+                  </text>
+                  <slot name="titleEnd" :content="loader.content.value" />
+                </view>
+                <text class="size-base color-text-content-second mt-2">{{ loader.content.value.desc }}</text>
+                <text v-if="loader.content.value.from" class="size-s color-text-content-second">来源:{{ loader.content.value.from }}</text>
+              </view>
+            </slot>
+            <slot name="titleExtra" :content="loader.content.value" />
+          </view>
+
+          <!-- 内容切换标签 -->
+          <view class="ml-2 mr-2">
+            <Tabs
+              :tabs="tabs" 
+              v-model:currentIndex="tabCurrentIndex"
+              :autoScroll="true"
+              :autoItemWidth="false"
+              :defaultIndicatorWidth="130"
+              class="top-tab"
+            />
+          </view>
+
+          <view class="d-flex flex-col radius-l bg-light p-25 mt-3" style="min-height:70vh">
+            <!-- 简介 -->
+            <template v-if="tabCurrentId == 0">
+              <Parse
+                v-if="loader.content.value.intro"
+                :content="loader.content.value.intro"
+                :tagStyle="commonParserStyle"
+              />
+              <Parse
+                v-if="loader.content.value.content"
+                :content="loader.content.value.content"
+                :tagStyle="commonParserStyle"
+              />
+              <text v-if="emptyContent">暂无简介</text>
+            </template>
+            <!-- 图片 -->
+            <template v-else-if="tabCurrentId == 1">
+              <slot name="imagesPrefix" />
+              <ImageGrid
+                :images="loader.content.value.images"
+                :rowCount="2"
+                :preview="true"
+                imageHeight="200rpx"
+              />
+            </template>
+            <!-- 视频 -->
+            <template v-else-if="tabCurrentId == 2">
+              <video
+                v-if="loader.content.value.video"
+                class="w-100 video"
+                autoplay
+                :poster="loader.content.value.image"
+                :src="loader.content.value.video"
+                controls
+              />
+            </template>
+            <!-- 音频 -->
+            <template v-else-if="tabCurrentId == 3">
+              <video 
+                v-if="loader.content.value.audio"
+                class="w-100 video"
+                autoplay
+                :poster="loader.content.value.image"
+                :src="loader.content.value.audio"
+                controls
+              />
+            </template>
+            <!-- 其他tab -->
+            <slot v-else name="extraTabs" :tabCurrentId="tabCurrentId" :content="loader.content.value" />
+          </view>
+          <ContentNote />
+        </view>
+        <LikeFooter :content="loader.content.value" />
+      </template>
+    </SimplePageContentLoader>
+  </view>
+</template>
+<script setup lang="ts">
+import type { GetContentDetailItem } from "@/api/CommonContent";
+import { useSimplePageContentLoader } from "@/common/composeabe/SimplePageContentLoader";
+import { useLoadQuerys } from "@/common/composeabe/LoadQuerys";
+import { useTabControl, type TabControlItem } from "@/common/composeabe/TabControl";
+import SimplePageContentLoader from "@/common/components/SimplePageContentLoader.vue";
+import ImageGrid from "@/pages/parts/ImageGrid.vue";
+import ImageSwiper from "@/pages/parts/ImageSwiper.vue";
+import ContentNote from "@/pages/parts/ContentNote.vue";
+import commonParserStyle from "@/common/style/commonParserStyle";
+import { computed, type PropType, type Ref } from "vue";
+import Parse from "@/components/display/parse/Parse.vue";
+import Tabs from "@/components/nav/Tabs.vue";
+import LikeFooter from "@/pages/parts/LikeFooter.vue";
+
+const props = defineProps({
+  load: {
+    type: Function as PropType<(id: number, tabsArray: Ref<TabControlItem[]>) => Promise<GetContentDetailItem>>,
+    default: null,
+  },
+  extraTabs: {
+    type: Array as PropType<TabControlItem[]>,
+    default: () => [],
+  },
+  showHead: {
+    type: Boolean,
+    default: true,
+  },
+})
+
+const emit = defineEmits([
+  "tabChange"
+])
+
+const emptyContent = computed(() => {
+  return !(loader.content.value?.intro as string || '').trim() && !(loader.content.value?.content || '').trim();
+})
+
+const loader = useSimplePageContentLoader<
+  GetContentDetailItem, 
+  { id: number }
+>(async (params) => {
+  if (!params)
+    throw new Error("!params");
+  const d = await props.load(params.id, tabsArray);
+  tabsArray.value[1].visible = Boolean(d.images && d.images.length > 1);
+  tabsArray.value[2].visible = Boolean(d.video);
+  tabsArray.value[3].visible = Boolean(d.audio);
+
+  if (d.title)
+    uni.setNavigationBarTitle({ title: d.title });
+  setTimeout(() => {
+    if (emptyContent.value) {
+      if (d.video)
+        tabCurrentIndex.value = 1;
+      else if (d.video)
+        tabCurrentIndex.value = 2;
+    }
+  }, 200);
+  return d;
+});
+
+const { 
+  tabCurrentId,
+  tabCurrentIndex,
+  tabsArray,
+  tabs,
+} = useTabControl({
+  tabs: [
+    {
+      id: 0,
+      text: '简介',
+      visible: true,
+    },
+    {
+      id: 1,
+      text: '图片',
+      visible: true,
+    },
+    {
+      id: 2,
+      text: '视频',
+      visible: true,
+    },
+    {
+      id: 3,
+      text: '音频',
+      visible: true,
+    },
+    ...props.extraTabs,
+  ],
+  onTabChange(a, b) {
+    emit("tabChange", a, b);
+  },
+})
+
+useLoadQuerys({ id : 0 }, (p) => loader.loadData(p));
+
+defineExpose({
+  getPageShareData() {
+    const content = loader.content.value;
+    if (!content)
+      return {};
+    const res = {
+      title: content.title,
+      imageUrl: content.image,
+    };
+    return res;
+  } 
+})
+</script>
+
+<style lang="scss">
+
+</style>

+ 134 - 0
src/pages/article/common/IntroBlock.vue

@@ -0,0 +1,134 @@
+<template>
+  <view :class="[
+    'intro-block',
+    small ? 'small' : '',
+  ]">
+    <SubTitle v-if="title" :title="title" />
+    <view class="desc no-indent">
+      <view v-if="address" class="navigation">
+        <view class="address">
+          <text class="iconfont icon-navigation"></text>
+          <text>{{ address }}</text>
+        </view>
+        <view class="link" @click="emit('navTo')">
+          去这里 <text class="iconfont icon-go"></text>
+        </view>
+      </view>
+      <view 
+        v-for="(it, k) in descItems" 
+        :key="k"
+        :class="['entry',Boolean(it.value)?'':'hidden']"
+      >
+        <view class="label">{{ it.label }}</view>
+        <view class="value">{{ it.value }}</view>
+      </view>
+      <slot name="lastDesc" />
+    </view>
+    <slot />
+  </view>
+</template>
+
+<script setup lang="ts">
+import SubTitle from '@/components/display/title/SubTitle.vue';
+import type { PropType } from 'vue';
+
+const props = defineProps({	
+  title: {
+    type: String,
+    default: ''
+  },
+  address: {
+    type: String,
+    default: ''
+  },
+  descItems: {
+    type: Array as PropType<Array<{ label: string, value: any }>>,
+    default: () => []
+  },
+  small: {
+    type: Boolean,
+    default: false
+  }
+})
+
+const emit = defineEmits([	
+  "navTo"	
+])
+</script>
+
+<style lang="scss">
+.intro-block {
+  margin-bottom: 38rpx;
+
+  &.small {
+    margin-bottom: 0rpx;
+
+    .desc{
+      line-height: inherit;
+      padding-bottom: 10rpx;
+    }
+  }
+
+  .entry {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+    margin-bottom: 10rpx;
+
+    &.hidden {
+      display: none;
+    }
+
+    .label {
+      color: #666666;
+      font-weight: 400;
+      font-size: 30rpx;
+      flex-shrink: 0;
+    }
+    .value {
+      font-size: 30rpx;
+      color: #312520;
+      font-weight: 400;
+      text-align: right;
+      max-width: 500rpx;
+      flex-shrink: 1;
+    }
+  }
+  .sub-title{
+    margin-left: 20rpx;
+    margin-top: 10rpx;
+    font-size: 35rpx;
+    font-weight: 600;
+  }
+  .desc{
+    padding: 30rpx 0;
+  }
+  .navigation{
+    display: flex;
+    align-items: center;
+    margin-bottom: 28rpx;
+    .address{
+      flex:1;
+      height: auto;
+      background: #F9F6EB;
+      border-radius: 28rpx;
+      font-weight: 400;
+      font-size: 24rpx;
+      color: #000000;
+      line-height: 48rpx;
+      padding-left: 30rpx;
+      display: flex;
+      align-items: center;
+      text.iconfont{
+        display: inline-block;
+        font-size: 36rpx;
+        margin-right: 8rpx;
+      }
+    }
+    .link{
+      margin-left: 20rpx;
+      color:#FF8719;
+    }
+  }
+}
+</style>

+ 50 - 0
src/pages/article/common/list.vue

@@ -0,0 +1,50 @@
+<template>
+  <CommonListPage 
+    :title="querys.title || undefined"
+    :load="loadData"
+    :itemType="querys.itemType as any || undefined"
+    :detailsPage="querys.detailsPage || undefined"
+    :detailsParams="{
+      mainBodyColumnId: querys.mainBodyColumnId || undefined,
+      modelId: querys.modelId || undefined,
+    }"
+  />
+</template>
+
+<script setup lang="ts">
+import { useLoadQuerys, stringDotNumbersToNumbers } from '@/common/composeabe/LoadQuerys';
+import CommonListPage from './CommonListPage.vue';
+import CommonContent, { GetContentListParams } from '@/api/CommonContent';
+
+const { querys } = useLoadQuerys({
+  mainBodyColumnId: '',
+  modelId: 0,
+  itemType: '',
+  detailsPage: '',
+  title: '',
+  region: '',
+});
+
+async function loadData(
+  page: number, 
+  pageSize: number,
+  searchText: string,
+  dropDownValues: number[]
+) {
+  const mainBodyColumnId = stringDotNumbersToNumbers(querys.value.mainBodyColumnId);
+
+  const res = await CommonContent.getContentList(new GetContentListParams().setSelfValues({
+    mainBodyColumnId: mainBodyColumnId || undefined,
+    modelId: querys.value.modelId || undefined,
+    keywords: searchText,
+    region: querys.value.region || undefined,
+  }), page, pageSize);
+
+  for (const element of res.list || []) {
+    if (!element.desc && element.from)
+      element.desc = `来源:${element.from}`;
+  }
+
+  return res;
+}
+</script>

+ 141 - 0
src/pages/article/details.vue

@@ -0,0 +1,141 @@
+<template>
+  <view class="d-flex flex-column bg-base pb-45">
+    <SimplePageContentLoader :loader="loader">
+      <template v-if="loader.content.value">
+        <view class="d-flex flex-col">
+          <swiper 
+            v-if="loader.content.value.images.length > 0"
+            circular 
+            :indicator-dots="true"
+            :autoplay="true"
+            :interval="3000"
+            :duration="1000"
+            class="height-500"
+          >
+            <swiper-item v-for="(item, key) in loader.content.value.images" :key="key">
+              <view class="item">
+                <image 
+                  :src="item" 
+                  class="w-100 height-500 radius-base"
+                  mode="aspectFill" 
+                  @click="onPreviewImage(key)"
+                />
+              </view>
+            </swiper-item>
+          </swiper>
+          <image 
+            v-else-if="loader.content.value.image"
+            class="w-100 radius-base"
+            :src="loader.content.value.image"
+            mode="widthFix"
+          />
+          <view class="d-flex flex-col p-3">
+            <view class="size-ll color-title-text">{{ loader.content.value.title }}</view>
+            <view class="d-flex flex-row mt-2">
+              <text v-if="loader.content.value.from" class="size-s color-text-content-second mr-2 ">来源:{{ loader.content.value.from }}</text>
+              <text class="size-s color-text-content-second">{{ DataDateUtils.formatDate(loader.content.value.publishAt, 'YYYY-MM-dd') }}</text>
+            </view>
+          </view>
+          <view class="p-3 radius-ll bg-light mt-3">
+            <Parse
+              v-if="loader.content.value.content"
+              :content="loader.content.value.content"
+              :tagStyle="commonParserStyle"
+            />
+            <text v-if="emptyContent">暂无简介</text>
+          </view>
+          
+          <!-- 推荐 -->
+          <view v-if="recommendListLoader.content.value?.length" class="d-flex flex-col p-3">
+            <text class="size-base text-bold mb-3">相关推荐</text>
+            <Box2LineImageRightShadow
+              class="w-100"
+              titleColor="title-text"
+              v-for="item in recommendListLoader.content.value"
+              :key="item.id"
+              :image="item.thumbnail || item.image || AppCofig.defaultImage"
+              :title="item.title"
+              :desc="item.desc"
+              :badge="item.badge"
+              :wideImage="true"
+              @click="goDetails(item.id)"
+            />
+          </view>
+        </view>
+      </template>
+    </SimplePageContentLoader>
+  </view>
+</template>
+
+<script setup lang="ts">
+import type { GetContentDetailItem } from "@/api/CommonContent";
+import { onShareTimeline, onShareAppMessage } from "@dcloudio/uni-app";
+import { DataDateUtils } from "@imengyu/js-request-transform";
+import { useSimplePageContentLoader } from "@/common/composeabe/SimplePageContentLoader";
+import { useSwiperImagePreview } from "@/common/composeabe/SwiperImagePreview";
+import { useLoadQuerys } from "@/common/composeabe/LoadQuerys";
+import commonParserStyle from "@/common/style/commonParserStyle";
+import SimplePageContentLoader from "@/common/components/SimplePageContentLoader.vue";
+import Parse from "@/components/display/parse/Parse.vue";
+import { computed } from "vue";
+import { useSimpleDataLoader } from "@/common/composeabe/SimpleDataLoader";
+import { navTo } from "@/components/utils/PageAction";
+import CommonContent, { GetContentListParams } from "@/api/CommonContent";
+import Box2LineImageRightShadow from "@/common/components/parts/Box2LineImageRightShadow.vue";
+import AppCofig from "@/common/config/AppCofig";
+
+const loader = useSimplePageContentLoader<
+  GetContentDetailItem, 
+  { id: number }
+>(async (params) => {
+  if (!params)
+    throw new Error("!params");
+  const res = await CommonContent.getContentDetail(params.id);
+  //console.log(res);
+  uni.setNavigationBarTitle({ title: res.title });
+  return res;
+});
+
+const { onPreviewImage } = useSwiperImagePreview(() => loader.content.value?.images || [])
+
+const emptyContent = computed(() => (loader.content.value?.content || '').trim() === '')
+
+const recommendListLoader = useSimpleDataLoader(async () => {
+  if (!querys.value.modelId)
+    return []
+  return (await CommonContent.getContentList(new GetContentListParams()
+    .setModelId(querys.value.modelId)
+    .setMainBodyColumnId(querys.value.mainBodyColumnId)
+  , 1, 10)).list.filter((p) => p.id !== querys.value.id);
+});
+
+
+function goDetails(id: number) {
+  navTo('/pages/article/details', { 
+    id, 
+    mainBodyColumnId: querys.value.mainBodyColumnId, 
+    modelId: querys.value.modelId 
+  });
+}
+
+const { querys } = useLoadQuerys({ 
+  id: 0,
+  mainBodyColumnId: 0,
+  modelId: 0,
+}, (t) => loader.loadData(t));
+
+function getPageShareData() {
+  if (!loader.content.value)
+    return { title: '文章详情', imageUrl: '' }
+  return {
+    title: loader.content.value.title,
+    imageUrl: loader.content.value.images[0],
+  }
+}
+onShareTimeline(() => {
+  return getPageShareData(); 
+})
+onShareAppMessage(() => {
+  return getPageShareData();
+})
+</script>

+ 36 - 0
src/pages/article/list.vue

@@ -0,0 +1,36 @@
+<template>
+  <CommonListPage
+    title="闽南文化资讯"
+    :load="loadData"
+    itemType="article-common"
+    :detailsParams="{
+      modelId: CommonContent.modelId,
+    }"
+  />
+</template>
+
+<script setup lang="ts">
+import CommonContent, { GetContentListParams } from '@/api/CommonContent';
+import { DataDateUtils } from '@imengyu/js-request-transform';
+import CommonListPage from './common/CommonListPage.vue';
+
+async function loadData(
+  page: number, 
+  pageSize: number,
+  searchText: string,
+  dropDownValues: number[]
+) {
+  const res = await CommonContent.getContentList(new GetContentListParams()
+    .setMainBodyColumnId([228/* , 298, 299 */])
+    .setKeywords(searchText)
+  , page, pageSize);
+  return { list: res.list.map((item) => {
+    return {
+      id: item.id,
+      image: item.thumbnail || item.image,
+      title: item.title,
+      date: DataDateUtils.formatDate(item.publishAt, 'YYYY-MM-dd'),
+    }
+  }), total: res.total }
+}
+</script>

+ 19 - 0
src/pages/article/web/ewebview.vue

@@ -0,0 +1,19 @@
+<template>
+  <web-view 
+    class="w-100 h-100vh"
+    :src="finalUrl"
+  />
+</template>
+
+<script setup lang="ts">
+import { useLoadQuerys } from '@/common/composeabe/LoadQuerys';
+import { ref } from 'vue';
+
+const finalUrl = ref('')
+const { querys } = useLoadQuerys({
+  url: '',
+}, ({ url }) => {
+  finalUrl.value = decodeURIComponent(url)
+  console.log('web-view', finalUrl.value)
+});
+</script>

+ 1 - 1
src/pages/dig/forms/data/common.ts

@@ -57,7 +57,7 @@ export function villageCommonContent (ref: Ref<IDynamicFormRef>, options: {
             defaultValue: 1,
             additionalProps: {
               loadData: async () => [
-                { text: '文', value: 1 },
+                { text: '文', value: 1 },
                 { text: '图片', value: 2 },
                 { text: '视频', value: 3 },
                 { text: '相册', value: 4 },

+ 21 - 7
src/pages/home/index.vue

@@ -4,9 +4,11 @@
       <ImageSwiper 
         :height="300"
         :images="[
-          'https://mn.wenlvti.net/app_static/minnan/images/home/BackgroundBanner5.jpg',
-          'https://mn.wenlvti.net/app_static/minnan/images/home/BackgroundBanner4.jpg',
-          'https://mn.wenlvti.net/app_static/minnan/images/home/BackgroundBanner3.jpg',
+          'https://mncdn.wenlvti.net/app_static/xiangyuan/images/causel/1.webp',
+          'https://mncdn.wenlvti.net/app_static/xiangyuan/images/causel/2.jpg',
+          'https://mncdn.wenlvti.net/app_static/xiangyuan/images/causel/3.jpg',
+          'https://mncdn.wenlvti.net/app_static/xiangyuan/images/causel/4.jpg',
+          'https://mncdn.wenlvti.net/app_static/xiangyuan/images/causel/5.jpg',
         ]"
       />
     </FlexCol>
@@ -19,6 +21,7 @@
         :scale="10"
         :longitude="AppCofig.defaultLonLat[0]"
         :latitude="AppCofig.defaultLonLat[1]"
+        @markertap="goVillageDetails($event)"
       />
     </Box>
     <Box title="线上史馆展示" icon="/static/images/home/icon-ancient-gate.png">
@@ -27,10 +30,11 @@
           <Touchable 
             v-for="(item, i) in recommendLoader.content.value"
             :key="i"
+            :gap="20"
             flex="0 0 48%"
             align="center"
             direction="column"
-            :gap="20"
+            @click="goVillageDetails(item)"
           > 
             <Image 
               :src="item.thumbnail || item.image" 
@@ -93,6 +97,7 @@ import FlexCol from '@/components/layout/FlexCol.vue';
 import FlexRow from '@/components/layout/FlexRow.vue';
 import Height from '@/components/layout/space/Height.vue';
 import Width from '@/components/layout/space/Width.vue';
+import { navTo } from '@/components/utils/PageAction';
 import { RandomUtils } from '@imengyu/imengyu-utils';
 import { getCurrentInstance } from 'vue';
 
@@ -103,8 +108,9 @@ function testImage() {
 const instance = getCurrentInstance();
 const mapCtx = uni.createMapContext('prevMap', instance);
 const mapLoader = useSimpleDataLoader(async () => {
-  const res = (await VillageApi.getVallageList()).map(p => ({
-    id: p.id,
+  const res = (await VillageApi.getVallageList()).map((p, i) => ({
+    ...p,
+    id: p.id ?? i,
     title: p.villageName,
     longitude: Number(p.longitude),
     latitude: Number(p.latitude),
@@ -130,9 +136,17 @@ const mapLoader = useSimpleDataLoader(async () => {
   return res;
 });
 
+function goVillageDetails(e: any) {
+  const id = typeof e.markerId == 'number' ? e.markerId : e.id;
+  uni.setStorageSync('VillageTemp', JSON.stringify(mapLoader.content.value?.find(p => p.id == id)));
+  setTimeout(() => {
+    navTo('/pages/home/village/details', { id: id });
+  }, 200);
+}
+
 const recommendLoader = useSimpleDataLoader(async () => {
   const category = (await CommonContent.getCategoryList(151)).find(p => p.title == '省级');
-  return (await VillageApi.getVallageList(category?.id)).slice(0, 8);
+  return (await VillageApi.getVallageList(/* category?.id */));
 });
 
 const discoverLoader = useSimpleDataLoader(async () => {

+ 32 - 24
src/pages/home/store/index.vue

@@ -5,9 +5,8 @@
       <ImageSwiper 
         :height="300"
         :images="[
-          'https://mn.wenlvti.net/app_static/minnan/images/home/BackgroundBanner5.jpg',
-          'https://mn.wenlvti.net/app_static/minnan/images/home/BackgroundBanner4.jpg',
-          'https://mn.wenlvti.net/app_static/minnan/images/home/BackgroundBanner3.jpg',
+          'https://mncdn.wenlvti.net/app_static/xiangyuan/images/causel/B1.jpg',
+          'https://mncdn.wenlvti.net/app_static/xiangyuan/images/causel/B2.jpg',
         ]"
       />
     </FlexCol>
@@ -18,16 +17,16 @@
         GridItemPaddingHorizontal: 0,
       }">
         <Grid :borderGrid="false" :mainAxisCount="3">
-          <GridItem title="全部" icon="/static/images/icons/icon-all.png" touchable />
-          <GridItem title="拍摄标准" icon="/static/images/icons/icon-camera.png" touchable />
-          <GridItem title="命名规则" icon="/static/images/icons/icon-rules.png" touchable />
-          <GridItem title="文本规范" icon="/static/images/icons/icon-mark.png" touchable />
-          <GridItem title="隐私授权" icon="/static/images/icons/icon-pac.png" touchable />
-          <GridItem title="错误案例" icon="/static/images/icons/icon-route.png" touchable />
+          <GridItem title="全部" icon="/static/images/icons/icon-all.png" touchable @click="goList(undefined, '知识库 · 全部')" />
+          <GridItem title="优秀案例" icon="/static/images/icons/icon-camera.png" touchable @click="goList(359, '知识库 · 优秀案例')" />
+          <GridItem title="经验分享" icon="/static/images/icons/icon-rules.png" touchable @click="goList(358, '知识库 · 经验分享')" />
+          <GridItem title="常见问题" icon="/static/images/icons/icon-mark.png" touchable @click="goList(357, '知识库 · 常见问题')" />
+          <GridItem title="挖掘技巧" icon="/static/images/icons/icon-pac.png" touchable @click="goList(356, '知识库 · 挖掘技巧')" />
+          <GridItem title="认知基础" icon="/static/images/icons/icon-route.png" touchable @click="goList(355, '知识库 · 认知基础')" />
         </Grid>
       </ProvideVar>
     </Box>
-    <Box title="最新" icon="/static/images/home/icon-shining.png" showMore @moreClicked="$emit('goDiscover')">  
+    <Box title="最新" icon="/static/images/home/icon-shining.png" showMore @moreClicked="goList(undefined, '知识库 · 全部')">  
       <SimplePageContentLoader :loader="discoverLoader">
         <FlexCol :gap="20">
           <Touchable 
@@ -36,12 +35,13 @@
             justify="space-between"
             align="center"
             direction="row"
+            @click="goDetail(item)"
           > 
             <FlexCol flex="1" :gap="10">
               <Text :text="item.title" fontConfig="h5" />
               <Text :text="item.desc" fontConfig="subText" />
               <FlexRow :gap="10">
-                <Tag v-for="tag in item.tags" :key="tag" :text="tag" size="small" type="primary" scheme="light" />
+                <Tag v-for="tag in item.keywords" :key="tag" :text="tag" size="small" type="primary" scheme="light" />
               </FlexRow>
             </FlexCol>
             <Width :width="25" />
@@ -63,11 +63,11 @@
 </template>
 
 <script setup lang="ts">
+import CommonContent, { GetContentListItem, GetContentListParams } from '@/api/CommonContent';
 import Box from '@/common/components/parts/Box.vue';
 import ImageSwiper from '@/common/components/parts/ImageSwiper.vue';
 import SimplePageContentLoader from '@/common/components/SimplePageContentLoader.vue';
 import { useSimpleDataLoader } from '@/common/composeabe/SimpleDataLoader';
-import AppCofig from '@/common/config/AppCofig';
 import Image from '@/components/basic/Image.vue';
 import Text from '@/components/basic/Text.vue';
 import Loadmore from '@/components/display/loading/Loadmore.vue';
@@ -81,19 +81,27 @@ import GridItem from '@/components/layout/grid/GridItem.vue';
 import Height from '@/components/layout/space/Height.vue';
 import Width from '@/components/layout/space/Width.vue';
 import ProvideVar from '@/components/theme/ProvideVar.vue';
-import { RandomUtils } from '@imengyu/imengyu-utils';
-
-function testImage() {
-  return 'https://mncdn.wenlvti.net/app_static/minnan/images/home/ImageTest' + RandomUtils.genRandom(1, 5) +'.jpg';
-}
+import { navTo } from '@/components/utils/PageAction';
 
 const discoverLoader = useSimpleDataLoader(async () => {
-  return new Array(12).fill(0).map((_, i) => ({ 
-    title: '茶艺传承作坊' + i, 
-    desc: '多馆联展,沉浸式交互体验。',
-    tags: ['拍摄标准', '命名规则','拍摄标准', '命名规则'],
-    image: testImage(),
-    thumbnail: testImage(),
-  }));
+  return (await CommonContent.getContentList(new GetContentListParams()
+    .setModelId(16)
+    .setMainBodyColumnId(354)
+  , 1, 30)).list
 });
+
+function goDetail(item: GetContentListItem) {
+  navTo('/pages/article/details', {
+    modelId: item.modelId,
+    mainBodyColumnId: item.mainBodyColumnId,
+    id: item.id,
+  });
+}
+function goList(mainBodyColumnId: number|undefined, title: string) {
+  navTo('/pages/article/common/list', {
+    modelId: 16,
+    mainBodyColumnId,
+    title,
+  });
+}
 </script>

+ 241 - 0
src/pages/home/village/details.vue

@@ -0,0 +1,241 @@
+<template>
+  <SimplePageContentLoader :loader="contentLoader">
+    <view 
+      v-if="contentLoader.loadStatus.value == 'finished'"
+      class="d-flex flex-column bg-base"
+    >
+      <swiper 
+        circular
+        class="height-500"
+        :indicator-dots="false"
+        :autoplay="true"
+        :interval="2000"
+        :duration="1000"
+      >
+        <swiper-item v-for="(item, k) in data.images" :key="k">
+          <image 
+            class="w-100 height-500 radius-l-top" 
+            :src="item" 
+            mode="aspectFill"
+            @click="onPreviewImage(k)"
+          />
+        </swiper-item>
+      </swiper>
+
+      <view class="d-flex flex-col p-3 radius-l-top p-3 bg-light">
+
+        <view class="d-flex flex-col">
+          <SubTitle :title="data.villageName" />
+          <IntroBlock 
+            small
+            :descItems="[
+              {
+                label: '保护级别',
+                value: data.historyLevelText ,
+              },
+              {
+                label: '年份时间',
+                value: data.ageText,
+              },
+              {
+                label: '所属区域',
+                value: data.regionText ,
+              },
+            ]"
+          />
+          <view class="mt-3 color-text-content">
+            <Parse :content="data.overview" :tagStyle="commonParserStyle" />
+            <text v-if="!data.overview" >无内容,请添加内容! {{ data.overview }}</text>
+          </view>
+        </view>
+
+        <view class="d-flex flex-row flex-wrap mt-3">
+          <view 
+            v-for="(tag, key) in tagsData"
+            :key="key"
+            class="w-20 d-flex flex-column align-center"
+            @click="goList(tag)"
+          >
+            <image :src="tag.image" class="width-100 mt-2" mode="widthFix"></image>
+            <view class="text-align-center color-text-content size-ss">{{ tag.title }}</view>
+          </view>
+        </view>
+
+        <view class="d-flex flex-col mt-3 mb-2">
+          <SubTitle title="地理位置" />
+          <div class="d-flex flex-column radius-base bg-white mt-3">
+            <map id="map"
+              class="w-100 height-350"
+              :latitude="center[1]"
+              :longitude="center[0]"
+              :markers="markers"
+              :scale="15"
+            />
+            <view class="d-flex flex-row justify-between p-2 mt-1">
+              <view>
+                <text class="iconfont icon-navigation"></text>
+                <text class="address">{{ data.address }}</text>
+              </view>
+              <view class="d-flex flex-row align-center" @click="goAddress">
+                <text class="color-orange">去这里</text>
+                <text class="iconfont icon-arrow-right"></text>
+              </view>
+            </view>
+          </div>
+        </view>
+
+        <template 
+          v-for="(tag, index) in tagsDataRecommend"
+          :key="index"
+        >
+          <SubTitle :title="tag.title" showMore @clickMore="tag.goList()" />
+          <SimplePageContentLoader :loader="tag.loader" >
+            <view class="d-flex flex-col">
+              <Box2LineLargeImageUserShadow 
+                v-for="(item, i) in tag.loader.content.value"
+                :key="i"
+                :title="item.title"
+                :desc="item.desc"
+                :image="item.image"
+                :likes="item.likes"
+                :comment="item.comments"
+                @click="tag.goDetail(item.id)"
+              />
+            </view>
+          </SimplePageContentLoader>
+        </template>
+      </view>
+
+    </view>
+  </SimplePageContentLoader>
+</template>
+
+<script setup lang="ts">
+import { ref, toRefs, type Ref } from 'vue';
+import { useLoadQuerys } from '@/common/composeabe/LoadQuerys';
+import { useSwiperImagePreview } from '@/common/composeabe/SwiperImagePreview';
+import { navTo } from '@/components/utils/PageAction';
+import { onShareTimeline, onShareAppMessage } from '@dcloudio/uni-app';
+import { useSimpleDataLoader } from '@/common/composeabe/SimpleDataLoader';
+import { useHomePageMiniCommonListGoMoreAndGoDetail, type IHomePageMiniCommonListGoMoreAndGoDetail } from '@/pages/article/common/CommonContent';
+import IntroBlock from '@/pages/article/common/IntroBlock.vue';
+import Parse from '@/components/display/parse/Parse.vue';
+import SimplePageContentLoader from '@/common/components/SimplePageContentLoader.vue';
+import Box2LineLargeImageUserShadow from '@/common/components/parts/Box2LineLargeImageUserShadow.vue';
+import VillageApi from '@/api/inhert/VillageApi';
+import commonParserStyle from '@/common/style/commonParserStyle';
+import ImagesUrls from '@/common/config/ImagesUrls';
+import SubTitle from '@/components/display/title/SubTitle.vue';
+
+const EmptyImage = 'https://mncdn.wenlvti.net/app_static/minnan/EmptyImage.png';
+
+interface TagDataItem {
+  image: string, 
+  title: string,
+  modelId?: number,
+  mainBodyColumnId?: number,
+}
+interface TagDataRecommendItem extends TagDataItem, IHomePageMiniCommonListGoMoreAndGoDetail {
+}
+
+const center = ref([118.15723, 24.48147]);
+const markers = ref<any>([]);
+const tagsData = ref<TagDataItem[]>([]);
+const tagsDataRecommend = ref<TagDataRecommendItem[]>([]) as unknown as Ref<TagDataRecommendItem[]>;
+
+const { querys } = useLoadQuerys({ 
+  id: 0,
+}, () => contentLoader.loadData());
+
+const data = ref<Record<string, any>>({
+  images: [],
+  overview: '',
+  longitude: 0,
+  latitude: 0,
+  region: 0,
+  address: '',
+  villageName: '',
+})
+function goAddress() {
+  uni.openLocation({
+    latitude: data.value.latitude,
+    longitude: data.value.longitude,
+  })
+}
+const { onPreviewImage } = useSwiperImagePreview(() => data.value.images || [])
+
+function goList(tag: TagDataItem) {
+  navTo('/pages/article/common/list', {
+    title: tag.title,
+    modelId: tag.modelId,
+    mainBodyColumnId: tag.mainBodyColumnId,
+    region: data.value.region,
+  });
+}
+
+const contentLoader = useSimpleDataLoader(async () => {
+  data.value = {
+    ...data.value,
+    ...JSON.parse(uni.getStorageSync('VillageTemp') || '{}'),
+  };
+  console.log(data.value);
+
+  if (data.value.longitude && data.value.latitude) {
+    center.value = [Number(data.value.longitude), Number(data.value.latitude)];
+  } else {
+    center.value = [118.11593, 24.467580];
+  }
+  markers.value = [
+    {
+      id: 1,
+      latitude: center.value[1],
+      longitude: center.value[0],
+      iconPath: ImagesUrls.IconMarker,
+      width: 40,
+      height: 40,
+    }
+  ];
+
+  const menu = await VillageApi.getVillageMenuList(querys.value.id);
+
+  tagsData.value = menu.map((item, index) => {
+    return {
+      title: item.name,
+      image: item.logo || EmptyImage,
+      modelId: item.modelId as number,
+      mainBodyColumnId: item.mainBodyColumnId as number,
+    };
+  });
+
+  tagsDataRecommend.value = tagsData.value.slice(0, 2).map((t) => {
+    return {
+      ...t,
+      ...(toRefs(useHomePageMiniCommonListGoMoreAndGoDetail({
+        title: t.title,
+        mainBodyColumnId: t.mainBodyColumnId,
+        modelId: t.modelId,
+        itemType: 'article-common',
+        detailsPage: '/pages/article/details',
+      })))
+    }
+  }) as any;
+
+  tagsDataRecommend.value.forEach(e => {
+    e.loader.loadData();
+  });
+}, false);
+
+
+function getPageShareData() {
+  return {
+    title: data.value.villageName,
+    imageUrl: data.value.images[0],
+  }
+}
+onShareTimeline(() => {
+  return getPageShareData(); 
+})
+onShareAppMessage(() => {
+  return getPageShareData();
+})
+</script>