快乐的梦鱼 hai 4 días
pai
achega
3c48e7842b

+ 3 - 3
src/components/utils/PageAction.ts

@@ -40,7 +40,7 @@ function redirectTo(url: string, data: Record<string, unknown> = {}) {
   }
 
   uni.redirectTo({ 
-    url: url + '?' + dataString,
+    url: url + (url.includes('?') ? '&' : '?') + dataString,
     fail: (err) => {
       console.error('页面跳转失败:', err);
     },
@@ -53,14 +53,14 @@ function redirectTo(url: string, data: Record<string, unknown> = {}) {
  */
 function navTo(url: string, data: Record<string, unknown> = {}) {
   var dataString = '';
-
+  
   for (const key in data) {
     if (Object.prototype.hasOwnProperty.call(data, key))
       dataString += `&${key}=${data[key]}`;
   }
 
   uni.navigateTo({ 
-    url: url + '?' + dataString,
+    url: url + (url.includes('?') ? '&' : '?') + dataString,
     fail: (err) => {
       console.error('页面跳转失败:', err);
     },

+ 6 - 0
src/pages.json

@@ -171,6 +171,12 @@
       }
     },
     {
+      "path": "pages/article/data/details",
+      "style": {
+        "navigationBarTitleText": "动态通用详情页"
+      }
+    },
+    {
       "path": "pages/article/details",
       "style": {
         "navigationBarTitleText": "新闻详情",

+ 1 - 1
src/pages/article/common/CommonContent.ts

@@ -32,7 +32,7 @@ export function navCommonList(p: {
   mainBodyColumnId?: string|number|number[],
   modelId?: number,
   itemType?: string,
-  detailsPage? : string,
+  detailsPage? : string|[string, Record<string, any>],
 }) {
   navTo('/pages/article/common/list', {
     title: p.title,

+ 9 - 1
src/pages/article/common/CommonListPage.vue

@@ -3,7 +3,8 @@
   <view 
     :class="[
       'common-list-page d-flex flex-column', 
-      hasBg ? 'bg-base p-3' : ''
+      hasBg ? 'bg-base' : '',
+      hasPadding ? 'p-3' : ''
     ]"
   >
     <view v-if="showTab && tabs" class="top-tab bg-base">
@@ -262,6 +263,11 @@ export interface CommonListPageProps {
    */
   hasBg?: boolean
   /**
+   * 是否有内边距
+   * @default true
+   */
+  hasPadding?: boolean
+  /**
    * 起始标签索引
    * @default 0
    */
@@ -287,6 +293,7 @@ const props = withDefaults(defineProps<CommonListPageProps>(), {
   detailsPage: '/pages/article/details',
   detailsParams: () => ({}),
   hasBg: true,
+  hasPadding: true,
   startTabIndex: undefined,
   loadMounted: true,
 })
@@ -343,6 +350,7 @@ function goDetails(item: any, id: number) {
     if (handleByContent())
       return;
   }
+
   function handleByContent() {
     const page = props.detailsPageByContentCallback?.(item);
     if (page) {

+ 26 - 0
src/pages/article/common/DetailTabPage.ts

@@ -0,0 +1,26 @@
+import type { GetContentDetailItem } from "@/api/CommonContent";
+import type { TabControlItem } from "@/common/composeabe/TabControl";
+import type { Ref } from "vue";
+
+export const TAB_ID_INTRO = 0;
+export const TAB_ID_IMAGES = 1;
+export const TAB_ID_VIDEO = 2;
+export const TAB_ID_AUDIO = 3;
+
+export interface DetailTabPageTabsArray {
+  tabsArray: Ref<TabControlItem[]>,
+  getTabById(id: number): TabControlItem | undefined;
+}
+
+export interface DetailTabPageProps {
+  load?: (
+    id: number, 
+    tabsArray: DetailTabPageTabsArray
+  ) => Promise<GetContentDetailItem>,
+  extraTabs?: TabControlItem[],
+  showHead?: boolean,
+  hasInternalTabs?: boolean,
+  overrideInternalTabsName?: undefined|{
+    [key: number]: string,
+  },
+}

+ 27 - 38
src/pages/article/common/DetailTabPage.vue

@@ -41,7 +41,7 @@
 
           <view class="d-flex flex-col radius-l bg-light p-25 mt-3" style="min-height:70vh">
             <!-- 简介 -->
-            <template v-if="tabCurrentId == TAB_ID_INTRO">
+            <template v-if="hasInternalTabs && tabCurrentId == TAB_ID_INTRO">
               <Parse
                 v-if="loader.content.value.intro"
                 :content="(loader.content.value.intro as string)"
@@ -54,7 +54,7 @@
               <text v-if="loader.content.value.from" class="size-s color-text-content-second mr-2 ">以上内容摘自 {{ loader.content.value.from }}</text>
             </template>
             <!-- 图片 -->
-            <template v-else-if="tabCurrentId == TAB_ID_IMAGES">
+            <template v-else-if="hasInternalTabs && tabCurrentId == TAB_ID_IMAGES">
               <slot name="imagesPrefix" />
               <ImageGrid
                 :images="loader.content.value.images"
@@ -64,7 +64,7 @@
               />
             </template>
             <!-- 视频 -->
-            <template v-else-if="tabCurrentId == TAB_ID_VIDEO">
+            <template v-else-if="hasInternalTabs && tabCurrentId == TAB_ID_VIDEO">
               <video
                 v-if="loader.content.value.video"
                 class="w-100 video"
@@ -75,7 +75,7 @@
               />
             </template>
             <!-- 音频 -->
-            <template v-else-if="tabCurrentId == TAB_ID_AUDIO">
+            <template v-else-if="hasInternalTabs && tabCurrentId == TAB_ID_AUDIO">
               <video 
                 v-if="loader.content.value.audio"
                 class="w-100 video"
@@ -100,63 +100,48 @@
   </view>
 </template>
 <script setup lang="ts">
-import type { GetContentDetailItem } from "@/api/CommonContent";
+import { computed } from "vue";
 import { useSimplePageContentLoader } from "@/common/composeabe/SimplePageContentLoader";
 import { useLoadQuerys } from "@/common/composeabe/LoadQuerys";
-import { useTabControl, type TabControlItem } from "@/common/composeabe/TabControl";
+import { useTabControl } from "@/common/composeabe/TabControl";
 import { requireNotNull } from "@imengyu/imengyu-utils";
 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 { 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";
 import ArticleCorrect from "@/pages/parts/ArticleCorrect.vue";
+import type { GetContentDetailItem } from "@/api/CommonContent";
+import { TAB_ID_AUDIO, TAB_ID_IMAGES, TAB_ID_INTRO, TAB_ID_VIDEO, type DetailTabPageProps, type DetailTabPageTabsArray } from "./DetailTabPage";
 
-export interface DetailTabPageTabsArray {
-  tabsArray: Ref<TabControlItem[]>,
-  getTabById(id: number): TabControlItem | undefined;
-}
-
-const props = defineProps({
-  load: {
-    type: Function as PropType<(
-      id: number, 
-      tabsArray: DetailTabPageTabsArray
-    ) => Promise<GetContentDetailItem>>,
-    default: null,
-  },
-  extraTabs: {
-    type: Array as PropType<TabControlItem[]>,
-    default: () => [],
-  },
-  showHead: {
-    type: Boolean,
-    default: true,
-  },
+const props = withDefaults(defineProps<DetailTabPageProps>(), {
+  extraTabs: () => [],
+  showHead: true,
+  hasInternalTabs: true,
 })
 
+const hasInternalTabs = computed(() => props.hasInternalTabs !== false)
+
 const emit = defineEmits([
-  "tabChange"
+  "tabChange",
+  "loaded"
 ])
 
 const emptyContent = computed(() => {
   return !(loader.content.value?.intro as string || '').trim() && !(loader.content.value?.content || '').trim();
 })
 
-const TAB_ID_INTRO = 0;
-const TAB_ID_IMAGES = 1;
-const TAB_ID_VIDEO = 2;
-const TAB_ID_AUDIO = 3;
-
 const loader = useSimplePageContentLoader<
   GetContentDetailItem, 
   { id: number }
 >(async (params) => {
   if (!params)
     throw new Error("!params");
+  if (!props.load)
+    throw new Error("!props.load");
+
   const d = await props.load(params.id, tabsArrayObject);
   requireNotNull(tabsArrayObject.getTabById(TAB_ID_IMAGES)).visible = Boolean(d.images && d.images.length > 1);
   requireNotNull(tabsArrayObject.getTabById(TAB_ID_VIDEO)).visible = Boolean(d.video);
@@ -172,6 +157,7 @@ const loader = useSimplePageContentLoader<
         tabCurrentIndex.value = 2;
     }
   }, 200);
+  emit("loaded", d);
   return d;
 });
 
@@ -184,22 +170,22 @@ const {
   tabs: [
     {
       id: TAB_ID_INTRO,
-      text: '简介',
+      text: props.overrideInternalTabsName?.[TAB_ID_INTRO] || '简介',
       visible: true,
     },
     {
       id: TAB_ID_IMAGES,
-      text: '图片',
+      text: props.overrideInternalTabsName?.[TAB_ID_IMAGES] || '图片',
       visible: true,
     },
     {
       id: TAB_ID_VIDEO,
-      text: '视频',
+      text: props.overrideInternalTabsName?.[TAB_ID_VIDEO] || '视频',
       visible: true,
     },
     {
       id: TAB_ID_AUDIO,
-      text: '音频',
+      text: props.overrideInternalTabsName?.[TAB_ID_AUDIO] || '音频',
       visible: true,
     },
     ...props.extraTabs,
@@ -219,6 +205,9 @@ const tabsArrayObject : DetailTabPageTabsArray = {
 useLoadQuerys({ id : 0 }, (p) => loader.loadData(p));
 
 defineExpose({
+  load(params: { id: number }) {
+    loader.loadData(params, true);
+  },
   getPageShareData() {
     const content = loader.content.value;
     if (!content)

+ 12 - 11
src/pages/article/common/DetailsCommon.vue

@@ -38,6 +38,7 @@
         visible: true,
       }
     ]"
+    v-bind="$attrs"
   >
     <template #extraTabs="{ content, tabCurrentId }">
       <template v-if="tabCurrentId==TAB_ID_ICH_SITES">
@@ -219,7 +220,7 @@ import { useLoadQuerys } from "@/common/composeabe/LoadQuerys";
 import { useTabId } from "@/common/composeabe/TabControl";
 import { navTo } from "@/components/utils/PageAction";
 import { StringUtils } from "@imengyu/imengyu-utils";
-import DetailTabPage, { type DetailTabPageTabsArray } from "@/pages/article/common/DetailTabPage.vue";
+import DetailTabPage from "@/pages/article/common/DetailTabPage.vue";
 import ProjectsContent from "@/api/inheritor/ProjectsContent";
 import CommonListPage from "@/pages/article/common/CommonListPage.vue";
 import IntroBlock from "@/pages/article/common/IntroBlock.vue";
@@ -229,17 +230,17 @@ import SeminarContent from "@/api/inheritor/SeminarContent";
 import ImagesUrls from "@/common/config/ImagesUrls";
 import Tag from "@/components/display/Tag.vue";
 import Parse from "@/components/display/parse/Parse.vue";
+import type { DetailTabPageProps, DetailTabPageTabsArray } from "./DetailTabPage";
 
-defineProps({	
-  commonRefName : {
-    type: String,
-    default: '',
-  },
-  commonRefTarget : {
-    type: String,
-    default: '',
-  },
-})
+export interface DetailsCommonProps extends DetailTabPageProps {
+  commonRefName?: string;
+  commonRefTarget?: string;
+}
+
+const props = withDefaults(defineProps<DetailsCommonProps>(), {	
+  commonRefName: '',
+  commonRefTarget: '',
+});
 
 const { nextId } = useTabId({ idStart: 4 });
 const TAB_ID_ICH_SITES = nextId();

+ 3 - 11
src/pages/article/data/CommonCategoryBlocks.vue

@@ -168,7 +168,6 @@ import { CommonContentApi, GetContentListItem, GetContentListParams } from '@/ap
 import { navCommonDetail, navCommonList, resolveCommonContentGetPageDetailUrlAuto, resolveCommonContentSolveProps, useHomeCommonCategoryBlock, type HomeCommonCategoryBlockProps, type IHomeCommonCategoryBlock } from '../common/CommonContent';
 import { useSimpleDataLoader } from '@/common/composeabe/SimpleDataLoader';
 import { navTo } from '@/components/utils/PageAction';
-import type { IHomeCommonCategoryListTabNestCategoryItemDefine } from './CommonCategoryDefine';
 import HomeTitle from '@/pages/parts/HomeTitle.vue';
 import SimplePageContentLoader from '@/common/components/SimplePageContentLoader.vue';
 import Box2LineImageRightShadow from '@/pages/parts/Box2LineImageRightShadow.vue';
@@ -199,12 +198,8 @@ const categoryDatas = computed(() => props.categoryDefine.map(item => {
       ...item,
       detailsPage: () => {},
       morePage: () => {
-        if (item.morePage) {
-          if (Array.isArray(item.morePage))
-            navTo(item.morePage[0], item.morePage[1] || {});
-          else
-            navTo(item.morePage, {});
-        }
+        if (item.morePage)
+          navTo(item.morePage, {});
       },
       data: null,
     };
@@ -230,10 +225,7 @@ const categoryDatas = computed(() => props.categoryDefine.map(item => {
       morePage: () => {
         const content = item.content as CommonContentApi;
         if (item.morePage) {
-          if (Array.isArray(item.morePage))
-            navTo(item.morePage[0], item.morePage[1] || {});
-          else
-            navTo(item.morePage, {});
+          navTo(item.morePage, {});
         } else {
           navCommonList({
             title: item.title,

+ 4 - 1
src/pages/article/data/CommonCategoryDefine.ts

@@ -1,3 +1,4 @@
+import type { IHomeCommonCategoryDetailDefine } from "./defines/Details";
 import type { IHomeCommonCategoryHomeDefine } from "./defines/Home";
 import type { IHomeCommonCategoryListDefine } from "./defines/List";
 
@@ -23,7 +24,9 @@ export interface IHomeCommonCategoryDefine {
     /**
      * 页面内容定义
      */
-    content: IHomeCommonCategoryListDefine|IHomeCommonCategoryHomeDefine,
+    content: IHomeCommonCategoryListDefine
+      |IHomeCommonCategoryHomeDefine
+      |IHomeCommonCategoryDetailDefine,
   }[],
 }
 

+ 306 - 0
src/pages/article/data/CommonCategoryDetail.vue

@@ -0,0 +1,306 @@
+<template>
+  <!--详情页可配置主控-->
+  <FlexCol>
+    <FlexCol v-if="errorMessage" :padding="30" :gap="30" center height="100%">
+      <Result status="error" :description="errorMessage" />
+      <Button type="primary" @click="loadPageConfig">重新加载</Button>
+    </FlexCol>
+    <LoadingPage v-else-if="!loadState" />
+    <DetailTabPage
+      v-else
+      ref="pageRef"
+      v-bind="$attrs"
+      :hasInternalTabs="false"
+      :load="load"
+      :extraTabs="tabRenderDefinesArray"
+      @loaded="onLoaded"
+    >
+      <template #extraTabs="{ content, tabCurrentId }">
+        <template v-if="tabRenderDefines[tabCurrentId].type === 'intro'">
+          <!-- 简介 -->
+          <Parse
+            v-if="content.intro"
+            :content="(content.intro as string)"
+          />
+          <Parse
+            v-if="content.content"
+            :content="content.content"
+          />
+          <text v-if="!(content.intro || content.content)">暂无简介</text>
+          <text v-if="content.from" class="size-s color-text-content-second mr-2 ">以上内容摘自 {{ content.from }}</text>
+        </template>
+        <template v-else-if="tabRenderDefines[tabCurrentId].type === 'images'">
+          <!-- 图片 -->
+          <ImageGrid
+            :images="content.images"
+            :rowCount="2"
+            :preview="true"
+            imageHeight="200rpx"
+          />
+        </template>
+        <template v-else-if="tabRenderDefines[tabCurrentId].type === 'video'">
+          <!-- 视频 -->
+          <video
+            v-if="content.video"
+            class="w-100 video"
+            autoplay
+            :poster="content.image"
+            :src="content.video"
+            controls
+          />
+        </template>
+        <template v-else-if="tabRenderDefines[tabCurrentId].type === 'audio'">
+          <!-- 视频 -->
+          <video
+            v-if="content.audio"
+            class="w-100 video"
+            autoplay
+            :poster="content.image"
+            :src="content.audio"
+            controls
+          />
+        </template>
+        <template v-else-if="tabRenderDefines[tabCurrentId].type === 'list'">
+          <!-- 列表 -->
+          <CommonCategoryListBlock
+            v-if="currentCommonCategoryDefine"
+            :currentCommonCategoryDefine="{ 
+              title: '',
+              name: 'default',
+              content: tabRenderDefines[tabCurrentId].define
+            }"
+            :currentCommonCategoryContentDefine="tabRenderDefines[tabCurrentId].define"
+            :pageQuerys="pageQuerys"
+            :parentData="content"
+            :hasPadding="false"
+            @error="errorMessage = $event"
+          />
+        </template>
+        <template v-else-if="tabRenderDefines[tabCurrentId].type === 'rich'">
+          <!-- 富文本 -->
+          <view class="d-flex flex-col mt-3 mb-2">
+            <Parse :content="(content[tabRenderDefines[tabCurrentId].key] as string)" />
+          </view>
+        </template>
+        <template v-else-if="tabRenderDefines[tabCurrentId].type === 'nestCategory'">
+          <!-- 嵌套分类 -->
+          <CommonCategoryBlocks :categoryDefine="tabRenderDefines[tabCurrentId].categoryDefine" />
+        </template>
+        <template v-else>
+          <CommonCategoryDetailContentBlocks
+            :define="tabRenderDefines[tabCurrentId]"
+            :content="content"
+          />
+        </template>
+      </template>
+      <template #titleEnd="{ content }">
+        <Tag
+          v-if="content.levelText"
+          :text="StringUtils.cutString(content.levelText as string, 4)"
+          size="small" scheme="light" type="primary"
+          class="flex-shrink-0"
+        />
+      </template>
+      <template #titleExtra="{ content }">
+        <view class="d-flex flex-col">
+          <IntroBlock small :descItems="descItems" />
+          <CommonCategoryDetailIntroBlocks
+            :introBlocks="currentCommonCategoryContentDefine?.props.introBlocks"
+            :content="content"
+          />
+        </view>
+      </template>
+    </DetailTabPage>
+  </FlexCol>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, ref, watch } from 'vue';
+import LoadingPage from '@/components/display/loading/LoadingPage.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import Result from '@/components/feedback/Result.vue';
+import Button from '@/components/basic/Button.vue';
+import { CommonCategoryListTabNestCategoryDataToContent, type IHomeCommonCategoryDefine, type IHomeCommonCategoryListTabNestCategoryItemDefine } from './CommonCategoryDefine';
+import { injectCommonCategory } from './CommonCategoryGlobalLoader';
+import { doLoadDynamicCategoryDataMergeTypeGetColumns } from './CommonCategoryDynamicData';
+import { formatError, StringUtils, waitTimeOut } from '@imengyu/imengyu-utils';
+import type { IHomeCommonCategoryDetailDefine, IHomeCommonCategoryDetailTabItemDefine } from './defines/Details';
+import type { DetailTabPageProps, DetailTabPageTabsArray } from '../common/DetailTabPage';
+import type { CategoryDefine } from './CommonCategoryBlocks';
+import DetailTabPage from '../common/DetailTabPage.vue';
+import IntroBlock from '../common/IntroBlock.vue';
+import CommonCategoryDetailIntroBlocks from './CommonCategoryDetailIntroBlocks.vue';
+import Tag from '@/components/display/Tag.vue';
+import CommonCategoryBlocks from './CommonCategoryBlocks.vue';
+import Parse from '@/components/display/parse/Parse.vue';
+import CommonContent from '@/api/CommonContent';
+import CommonCategoryDetailContentBlocks from './CommonCategoryDetailContentBlocks.vue';
+import ImageGrid from '@/pages/parts/ImageGrid.vue';
+import CommonCategoryListBlock from './CommonCategoryListBlock.vue';
+
+export interface CommonCategoryDetailProps extends DetailTabPageProps {
+  /**
+   * 简介块描述项
+   */
+  introBlockDescs?: {
+    label: string;
+    key: string;
+  }[];
+  /**
+   * 简介下方块
+   */
+  introBlocks?: CommonCategoryDetailIntroBlocksDesc[];
+}
+export interface CommonCategoryDetailIntroBlocksDesc {
+  label?: string;
+  type: string;
+  key: string;
+}
+export type RenderTabDefine = IHomeCommonCategoryDetailTabItemDefine & {
+  id: number;
+  categoryDefine?: CategoryDefine[];  
+};
+
+const pageRef = ref();
+const props = defineProps({
+  pageConfigName: {
+    type: String,
+  },
+  pageQuerys: {
+    type: Object as () => Record<string, string|number|number[]|undefined>,
+    default: () => ({}),
+  },
+})
+
+const loadState = ref(false);
+const errorMessage = ref('');
+const currentCommonCategoryDefine = ref<IHomeCommonCategoryDefine['page'][0]>();
+const currentCommonCategoryContentDefine = ref<IHomeCommonCategoryDetailDefine>();
+const commonCategory = injectCommonCategory();
+
+const tabDefines = computed(() => currentCommonCategoryContentDefine.value?.props.tabs || []);
+const tabRenderDefines = computed(() => {
+  const result = {} as Record<number, RenderTabDefine>;
+  try {
+    tabDefines.value.forEach((item, i) => {
+      const renderItem : RenderTabDefine = {
+        ...item,
+        id: i,
+      };
+      function loadNestCategoryData(items: IHomeCommonCategoryListTabNestCategoryItemDefine[]) {
+        return items
+          .filter((item) => item.visible !== false)
+          .map((item) => {
+            return {
+              ...item,
+              showTitle: item.showTitle !== false,
+              title: item.text,
+              content: CommonCategoryListTabNestCategoryDataToContent(
+                item.data, item
+              ),
+              type: item.type as CategoryDefine['type'],
+            }
+          });
+      }
+      switch (item.type) {
+        case 'nestCategory':
+          renderItem.categoryDefine = loadNestCategoryData(item.categorys);
+          break;
+      }
+      result[i] = renderItem;
+    });
+  } catch (error) {
+    errorMessage.value = formatError(error);
+  }
+  return result;
+});
+const tabRenderDefinesArray = computed(() => {
+  return Object.values(tabRenderDefines.value);
+});
+
+async function loadPageConfig() {
+  if (!props.pageConfigName) {
+    errorMessage.value = '配置有误';
+    return;
+  }
+  currentCommonCategoryDefine.value = commonCategory.value.page
+    .find((item) => item.name === props.pageConfigName);
+  if (!currentCommonCategoryDefine.value) {
+    errorMessage.value = '未找到指定的分类配置:' + props.pageConfigName;
+    return;
+  }
+  if (currentCommonCategoryDefine.value.content.type !== 'Details') {
+    errorMessage.value = '分类配置:' + props.pageConfigName + ' 不是详情类型';
+    return;
+  }
+  currentCommonCategoryContentDefine.value = 
+    currentCommonCategoryDefine.value.content as IHomeCommonCategoryDetailDefine;
+  uni.setNavigationBarTitle({
+    title: currentCommonCategoryDefine.value?.title || '',
+  })
+
+  await waitTimeOut(100);
+  
+  try {
+    //特殊处理
+    let hasNestCategory = false;
+    for (const [_, tab] of Object.entries(tabDefines.value)) {
+      if (tab.type === 'nestCategory') {
+        tab.categorys = await doLoadDynamicCategoryDataMergeTypeGetColumns(tab.categorys)
+        hasNestCategory = true;
+      }
+    }
+    if (hasNestCategory)
+      await waitTimeOut(100);
+    loadState.value = true;
+
+    await waitTimeOut(100);
+
+    pageRef.value?.load(props.pageQuerys);
+  } catch (error) {
+    console.error(error);
+    loadState.value = false;
+    errorMessage.value = formatError(error);
+  }
+}
+
+watch(() => props.pageConfigName, loadPageConfig);
+onMounted(loadPageConfig);
+
+const content = ref<any>();
+const descItems = computed(() => (
+  currentCommonCategoryContentDefine.value?.props.introBlockDescs || [])
+    .map((item) => ({
+      ...item,
+      value: content.value?.[item.key] || '',
+    }))
+);
+
+function onLoaded(d: any) {
+  content.value = d;
+}
+async function load(id: number, tabsArray: DetailTabPageTabsArray) {
+  const d = await CommonContent.getContentDetail(
+    id, 
+    undefined, 
+    props.pageQuerys.modelId && Number(props.pageQuerys.modelId) > 0 ? Number(props.pageQuerys.modelId) : undefined
+  );
+  for (const tab of tabRenderDefinesArray.value) {
+    const v = d[tab.key];
+    let check = true
+    if (!v)
+      check = false;
+    else if (Array.isArray(v))
+      check = (v as any[]).length > 0;
+
+    tabsArray.getTabById(tab.id)!.visible = tab.visible !== false && check;
+  }
+  return d;
+}
+
+defineExpose({
+  getPageShareData() {
+    return pageRef.value?.getPageShareData() || {};
+  } 
+})
+</script>

+ 60 - 0
src/pages/article/data/CommonCategoryDetailContentBlocks.vue

@@ -0,0 +1,60 @@
+<template>
+  <template v-if="define.type === 'map'">
+    <!-- 地理位置单元 -->
+    <view class="d-flex flex-col mt-3 mb-2">
+    <HomeTitle title="地理位置" />
+      <map id="map"
+        class="w-100 height-350 mt-3"
+        :latitude="content.latitude"
+        :longitude="content.longitude"
+        :markers="[
+          {
+            id: 1,
+            latitude: content.latitude,
+            longitude: content.longitude,
+            iconPath: ImagesUrls.IconMarker,
+            width: 40,
+            height: 40,
+          }
+        ]"
+        :scale="15"
+      />
+      <view class="d-flex flex-row justify-between bg-light radius-base p-2 mt-2">
+        <view>
+          <text class="iconfont icon-navigation"></text>
+          <text class="address">{{ content.address }}</text>
+        </view>
+        <view class="d-flex flex-row align-center flex-shrink-0" @click="handleNavTo(content)">
+          <text class="color-orange">去这里</text>
+          <text class="iconfont icon-arrow-right"></text>
+        </view>
+      </view>
+    </view>
+  </template>
+</template>
+
+<script setup lang="ts">
+import { type PropType } from 'vue';
+import type { RenderTabDefine } from './CommonCategoryDetail.vue';
+import ImagesUrls from '@/common/config/ImagesUrls';
+
+defineProps({
+  define: {
+    type: Object as PropType<RenderTabDefine>,
+    default: () => [],
+  },
+  content: {
+    type: null,
+    default: () => ({}),
+  },
+})
+
+function handleNavTo(content: any) {
+  if (!content?.latitude || !content?.longitude) 
+    return;
+  uni.openLocation({
+    latitude: Number(content.latitude),  
+    longitude: Number(content.longitude),  
+  }) 
+}
+</script>

+ 50 - 0
src/pages/article/data/CommonCategoryDetailIntroBlocks.vue

@@ -0,0 +1,50 @@
+<template>
+  <!--详情页,标题下方可配置单元-->
+  <template v-for="(item, k) in introBlocks" :key="k">
+    <!-- 同级别非遗项目显示 -->
+    <view v-if="item.type == 'OtherLevel' && content.otherLevel && content.otherLevel.length > 0" class="mt-2">
+      <view 
+        v-for="(item, k) in content.otherLevel"
+        :key="k"
+        class="d-flex flex-row align-center justify-between p-3 radius-base bg-light"
+        @click="navTo('/pages/inhert/intangible/details', {
+          id: item.id,
+        })"
+      >
+        <view class="d-flex flex-row align-center">
+          <Tag
+            :text="StringUtils.cutString(item.levelText as string, 3)"
+            size="small" scheme="light" type="primary"
+            class="flex-shrink-0"
+          />
+          <view class="d-flex flex-col ml-2">
+            <view class="d-flex flex-row align-center">
+              <text>{{ item.title }}</text>
+              <text v-if="item.regionText" class="ml-2">({{ item.regionText }})</text>
+            </view>
+            <text v-if="item.unit" class="size-s color-second">{{ item.unit }}</text>
+          </view>
+        </view>
+        <text class="iconfont icon-arrow-right"></text>
+      </view>
+    </view>
+  </template>
+</template>
+
+<script setup lang="ts">
+import { type PropType } from 'vue';
+import type { CommonCategoryDetailIntroBlocksDesc } from './CommonCategoryDetail.vue';
+import { navTo } from '@/components/utils/PageAction';
+import { StringUtils } from '@imengyu/imengyu-utils';
+
+defineProps({
+  introBlocks: {
+    type: Array as PropType<CommonCategoryDetailIntroBlocksDesc[]>,
+    default: () => [],
+  },
+  content: {
+    type: null,
+    default: () => ({}),
+  },
+})
+</script>

+ 19 - 1
src/pages/article/data/CommonCategoryDynamicData.ts

@@ -88,10 +88,18 @@ export interface IHomeCommonCategoryDynamicDataRequest {
   querys?: Record<string, any>,
   params?: Record<string, any>,
 }
+/**
+ * 默认列表动态数据接口定义 - 从父级指定键内容
+ */
+export interface IHomeCommonCategoryDynamicDataParentKey {
+  type: 'parentKey',
+  key: string,
+}
 
 export type IHomeCommonCategoryDynamicData = IHomeCommonCategoryDynamicDataCommonContent 
   | IHomeCommonCategoryDynamicDataSerializedApi 
-  | IHomeCommonCategoryDynamicDataRequest;
+  | IHomeCommonCategoryDynamicDataRequest
+  | IHomeCommonCategoryDynamicDataParentKey;
 
 /**
  * 序列化接口映射表
@@ -141,6 +149,7 @@ export async function doLoadDynamicListData(
   keywords: string,
   dropdownDefines: IHomeCommonCategoryListTabListDropdownDefine[],
   dropDownValues: (number|string|boolean)[],
+  parentData?: any,
 ) {
   switch (item.type) {
     default:
@@ -155,6 +164,15 @@ export async function doLoadDynamicListData(
           ...CommonCategorDynamicDropDownValuesToParams(dropDownValues, dropdownDefines || []),
         }) 
       , page, pageSize);
+    case 'parentKey': {
+      if (!parentData)
+        throw new Error(`此处不允许加载父级数据`);
+      const arr = parentData?.[item.key] || [];
+      return {
+        list: arr,
+        total: arr.length,
+      };
+    }
     case 'serializedApi': {
       const params = new GetContentListParams();
       if (item.params?.mainBodyColumnId)

+ 2 - 2
src/pages/article/data/CommonCategoryGlobalLoader.ts

@@ -24,10 +24,10 @@ export function useCommonCategoryGlobalLoader() {
   async function loadCommonCategory() {
     uni.showLoading({ title: '加载中' });
     try {
-      /* if (uni.getSystemInfoSync().platform === 'devtools') {
+      if (uni.getSystemInfoSync().platform === 'devtools') {
         commonCategoryData.value = DefaultCofig as IHomeCommonCategoryDefine;
         return;
-      } */
+      }
       const category = (await CommonCategoryApi.getConfig()) as any as IHomeCommonCategoryDefine;
       if (category)
         commonCategoryData.value = category;

+ 14 - 209
src/pages/article/data/CommonCategoryList.vue

@@ -4,59 +4,36 @@
       <Result status="error" :description="errorMessage" />
       <Button type="primary" @click="loadPageConfig">重新加载</Button>
     </FlexCol>
-    <LoadingPage v-else-if="!loadState" />
-    <CommonListPage
-      v-else-if="currentCommonCategoryDefine && loadState"
-      :startTabIndex="pageStartTab"
-      v-bind="currentCommonCategoryDefine.content.props as any || undefined"
-      :title="currentCommonCategoryDefine.title || undefined"
-      :load="loadData"
-      :tabs="tabs"
-      :dropDownNames="dropdownNames"
-      :showListTabIds="showListTabIds"
-      :detailsPage="detailsPage"
-    >
-      <template #list="{ tabId }">
-        <CommonCategoryBlocks
-          v-if="tabRenderDefines[tabId]?.type === 'nestCategory'" 
-          :categoryDefine="tabRenderDefines[tabId].categoryDefine"
-        />
-        <CommonCategoryBlocks
-          v-else-if="tabRenderDefines[tabId]?.type === 'list' && tabRenderDefines[tabId].preInsertCategorys?.length" 
-          :categoryDefine="tabRenderDefines[tabId].categoryDefine"
-        />
-      </template>
-    </CommonListPage>
-
+    <CommonCategoryListBlock
+      v-else-if="currentCommonCategoryDefine && currentCommonCategoryContentDefine"
+      :currentCommonCategoryDefine="currentCommonCategoryDefine"
+      :currentCommonCategoryContentDefine="currentCommonCategoryContentDefine"
+      :pageStartTab="pageStartTab"
+      :pageQuerys="pageQuerys"
+      @error="errorMessage = $event"
+    />
+    <LoadingPage v-else />
     <Footer text="我也是有底线的~" />
   </FlexCol>
 </template>
 
 <script setup lang="ts">
-import { computed, onMounted, ref, watch } from 'vue';
+import { onMounted, ref, watch } from 'vue';
 import { injectCommonCategory } from './CommonCategoryGlobalLoader';
-import { navTo } from '@/components/utils/PageAction';
-import { doLoadDynamicCategoryDataMergeTypeGetColumns, doLoadDynamicDropdownData, doLoadDynamicListData } from './CommonCategoryDynamicData';
-import { CommonCategoryListTabNestCategoryDataToContent, type IHomeCommonCategoryDefine, type IHomeCommonCategoryListDefine, type IHomeCommonCategoryListTabDefine, type IHomeCommonCategoryListTabNestCategoryItemDefine } from './CommonCategoryDefine';
-import { resolveCommonContentSolveProps } from '../common/CommonContent';
-import { waitTimeOut } from '@imengyu/imengyu-utils';
-import { formatError } from '@/common/composeabe/ErrorDisplay';
-import type { SimpleDropDownPickerItem } from '@/common/components/SimpleDropDownPicker.vue';
-import type { CommonListPageProps, DropDownNames } from '../common/CommonListPage.vue';
-import type { CategoryDefine } from './CommonCategoryBlocks';
-import CommonListPage from '../common/CommonListPage.vue';
+import { type IHomeCommonCategoryDefine, type IHomeCommonCategoryListDefine } from './CommonCategoryDefine';
 import Result from '@/components/feedback/Result.vue';
 import CommonCategoryBlocks from './CommonCategoryBlocks.vue';
 import Footer from '@/components/display/Footer.vue';
 import FlexCol from '@/components/layout/FlexCol.vue';
 import LoadingPage from '@/components/display/loading/LoadingPage.vue';
 import Button from '@/components/basic/Button.vue';
+import { waitTimeOut } from '@imengyu/imengyu-utils';
+import CommonCategoryListBlock from './CommonCategoryListBlock.vue';
 
 /**
  * 动态通用内容 - 通用列表页
  */
 
-const loadState = ref(false);
 const errorMessage = ref('');
 const currentCommonCategoryDefine = ref<IHomeCommonCategoryDefine['page'][0]>();
 const currentCommonCategoryContentDefine = ref<IHomeCommonCategoryListDefine>();
@@ -98,181 +75,9 @@ async function loadPageConfig() {
     title: currentCommonCategoryDefine.value?.title || '',
   })
 
-  await waitTimeOut(100);
-  
-  try {
-    //特殊处理
-    let hasNestCategory = false;
-    for (const [_, tab] of Object.entries(tabDefines.value)) {
-      if (tab.type === 'nestCategory') {
-        tab.categorys = await doLoadDynamicCategoryDataMergeTypeGetColumns(tab.categorys)
-        hasNestCategory = true;
-      } else if (tab.type === 'list') {
-        tab.preInsertCategorys = await doLoadDynamicCategoryDataMergeTypeGetColumns(tab.preInsertCategorys || [])
-        hasNestCategory = true;
-      }
-    }
-
-    if (hasNestCategory)
-      await waitTimeOut(100);
-
-    //加载下拉列表
-    const result = [] as DropDownNames[];
-    for (const [key, tab] of Object.entries(tabRenderDefines.value)) {
-      if (tab.type !== 'list')
-        continue;
-      if (tab.dropdownDefines) {
-        for (const dropdown of tab.dropdownDefines) {    
-          const data : SimpleDropDownPickerItem[] = (dropdown.addAll ? [{
-            id: 0, 
-            name: dropdown.addAll
-          }] : []);
-          if (dropdown.data) {
-            data.push(...(await doLoadDynamicDropdownData(dropdown.data)).map((item) => ({
-              id: item[dropdown.data.idKey || 'id'],
-              name: item[dropdown.data.nameKey || 'title'],
-            })))
-          }
-          result.push({
-            options: data,
-            activeTab: [Number(key)],
-            defaultSelectedValue: dropdown.formQueryKey ? (
-              props.pageQuerys[dropdown.formQueryKey] as number || 0
-            ) : dropdown.defaultValue || 0,
-          })
-        }
-      }
-    }
-    loadState.value = true;
-    dropdownNames.value = result;
-  } catch (error) {
-    console.error(error);
-    loadState.value = false;
-    errorMessage.value = formatError(error);
-  }
+  await waitTimeOut(600);
 }
 
 watch(() => props.pageConfigName, loadPageConfig);
 onMounted(loadPageConfig);
-
-type RenderTabDefine = IHomeCommonCategoryListTabDefine & {
-  categoryDefine?: CategoryDefine[];  
-};
-
-const tabDefines = computed(() => currentCommonCategoryContentDefine.value?.props.tabs || []);
-const tabRenderDefines = computed(() => {
-  const result = {} as Record<number, RenderTabDefine>;
-  try {
-    tabDefines.value.forEach((item, i) => {
-      const renderItem : RenderTabDefine = {
-        ...item,
-      };
-      function loadNestCategoryData(items: IHomeCommonCategoryListTabNestCategoryItemDefine[]) {
-        return items
-          .filter((item) => item.visible !== false)
-          .map((item) => {
-            return {
-              ...item,
-              showTitle: item.showTitle !== false,
-              title: item.text,
-              content: CommonCategoryListTabNestCategoryDataToContent(
-                item.data, item
-              ),
-              type: item.type as CategoryDefine['type'],
-            }
-          });
-      }
-      switch (item.type) {
-        default:
-        case 'list':
-          renderItem.categoryDefine = loadNestCategoryData(item.preInsertCategorys || []);
-          break;
-        case 'jump':
-          break;
-        case 'nestCategory':
-          renderItem.categoryDefine = loadNestCategoryData(item.categorys);
-          break;
-      }
-      result[i] = renderItem;
-    });
-  } catch (error) {
-    errorMessage.value = formatError(error);
-  }
-  return result;
-});
-const showListTabIds = computed(() => {
-  const define = tabDefines.value;
-  if (define.length === 0)
-    return undefined;
-  return define
-    .map((item, i) => ({ type: item.type, id: i }))
-    .filter((item) => item.type === 'list')
-    .map((item) => item.id);
-})
-const tabs = computed<CommonListPageProps['tabs']>(() => {
-  const define = tabDefines.value;
-  return define.map((item, i) => {
-    switch (item.type) {
-      default:
-      case 'list':
-        return {
-          id: i,
-          text: item.text,
-          width: item.width,
-        };
-      case 'jump':
-        return {
-          id: i,
-          text: item.text,
-          onlyJump: true,
-          jump: () => navTo(item.url, item.params),
-          width: item.width,
-        };
-      case 'nestCategory':
-        return {
-          id: i,
-          text: item.text,
-          width: item.width,
-        };
-    }
-  });
-});
-const dropdownNames = ref<DropDownNames[]>([]);
-const detailsPage = computed(() => {
-  if (tabDefines.value.find((item) => item.detailsPage)) {
-    const result = {} as Record<number, string>;
-    tabDefines.value.forEach((item, i) => {
-      result[i] = item.detailsPage || '';
-    });
-    return result;
-  }
-  return currentCommonCategoryContentDefine.value?.props.detailsPage || undefined;
-});
-
-async function loadData(
-  page: number, 
-  pageSize: number,
-  searchText: string,
-  dropDownValues: number[],
-  tabSelect: number
-) {
-  const tab = tabRenderDefines.value[tabSelect];
-  if (!tab) 
-    throw new Error(`配置有误 tab:${tabSelect}`);
-  if (tab.type !== 'list')
-    return { list: [], total: 0 };
-  if (!tab.data) 
-    throw new Error(`配置有误 tab:${tabSelect} 没有配置列表数据`);
-  const res = await doLoadDynamicListData(
-    tab.data, 
-    page, 
-    pageSize, 
-    searchText, 
-    tab.dropdownDefines || [],
-    dropDownValues,
-  );
-  if (res && tab.dataSolve)
-    res.list = resolveCommonContentSolveProps(res.list, tab.dataSolve);
-  return res;
-}
 </script>

+ 233 - 0
src/pages/article/data/CommonCategoryListBlock.vue

@@ -0,0 +1,233 @@
+<template>
+  <CommonListPage
+    v-if="currentCommonCategoryDefine"
+    :startTabIndex="pageStartTab"
+    v-bind="currentCommonCategoryDefine.content.props as any || undefined"
+    :title="currentCommonCategoryDefine.title || undefined"
+    :load="loadData"
+    :tabs="tabs"
+    :dropDownNames="dropdownNames"
+    :showListTabIds="showListTabIds"
+    :detailsPage="detailsPage"
+    :hasPadding="hasPadding"
+  >
+    <template #list="{ tabId }">
+      <CommonCategoryBlocks
+        v-if="tabRenderDefines[tabId]?.type === 'nestCategory'" 
+        :categoryDefine="tabRenderDefines[tabId].categoryDefine"
+      />
+      <CommonCategoryBlocks
+        v-else-if="tabRenderDefines[tabId]?.type === 'list' && tabRenderDefines[tabId].preInsertCategorys?.length" 
+        :categoryDefine="tabRenderDefines[tabId].categoryDefine"
+      />
+    </template>
+  </CommonListPage>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, ref, watch } from 'vue';
+import { navTo } from '@/components/utils/PageAction';
+import { doLoadDynamicCategoryDataMergeTypeGetColumns, doLoadDynamicDropdownData, doLoadDynamicListData } from './CommonCategoryDynamicData';
+import { CommonCategoryListTabNestCategoryDataToContent, type IHomeCommonCategoryDefine, type IHomeCommonCategoryListDefine, type IHomeCommonCategoryListTabDefine, type IHomeCommonCategoryListTabNestCategoryItemDefine } from './CommonCategoryDefine';
+import { resolveCommonContentSolveProps } from '../common/CommonContent';
+import { waitTimeOut } from '@imengyu/imengyu-utils';
+import type { SimpleDropDownPickerItem } from '@/common/components/SimpleDropDownPicker.vue';
+import type { CommonListPageProps, DropDownNames } from '../common/CommonListPage.vue';
+import type { CategoryDefine } from './CommonCategoryBlocks';
+import CommonListPage from '../common/CommonListPage.vue';
+import CommonCategoryBlocks from './CommonCategoryBlocks.vue';
+
+/**
+ * 动态通用内容 - 通用列表页
+ */
+
+const props = withDefaults(defineProps<{
+  currentCommonCategoryDefine: IHomeCommonCategoryDefine['page'][0],
+  currentCommonCategoryContentDefine: IHomeCommonCategoryListDefine,
+  pageStartTab?: number,
+  pageQuerys?: Record<string, any>,
+  parentData?: any,
+  hasPadding?: boolean,
+}>(), {
+  pageStartTab: 0,
+  pageQuerys: () => ({}),
+});
+const emit = defineEmits<{
+  (e: 'error', error: any): void;
+}>();
+
+async function loadPageConfig() {
+  await waitTimeOut(100);
+  try {
+    //特殊处理
+    let hasNestCategory = false;
+    for (const [_, tab] of Object.entries(tabDefines.value)) {
+      if (tab.type === 'nestCategory') {
+        tab.categorys = await doLoadDynamicCategoryDataMergeTypeGetColumns(tab.categorys)
+        hasNestCategory = true;
+      } else if (tab.type === 'list') {
+        tab.preInsertCategorys = await doLoadDynamicCategoryDataMergeTypeGetColumns(tab.preInsertCategorys || [])
+        hasNestCategory = true;
+      }
+    }
+
+    if (hasNestCategory)
+      await waitTimeOut(100);
+
+    //加载下拉列表
+    const result = [] as DropDownNames[];
+    for (const [key, tab] of Object.entries(tabRenderDefines.value)) {
+      if (tab.type !== 'list')
+        continue;
+      if (tab.dropdownDefines) {
+        for (const dropdown of tab.dropdownDefines) {    
+          const data : SimpleDropDownPickerItem[] = (dropdown.addAll ? [{
+            id: 0, 
+            name: dropdown.addAll
+          }] : []);
+          if (dropdown.data) {
+            data.push(...(await doLoadDynamicDropdownData(dropdown.data)).map((item) => ({
+              id: item[dropdown.data.idKey || 'id'],
+              name: item[dropdown.data.nameKey || 'title'],
+            })))
+          }
+          result.push({
+            options: data,
+            activeTab: [Number(key)],
+            defaultSelectedValue: dropdown.formQueryKey ? (
+              props.pageQuerys[dropdown.formQueryKey] as number || 0
+            ) : dropdown.defaultValue || 0,
+          })
+        }
+      }
+    }
+    dropdownNames.value = result;
+  } catch (error) {
+    emit('error', error);
+  }
+}
+
+watch(() => props.currentCommonCategoryDefine, loadPageConfig, { immediate: true });
+onMounted(loadPageConfig);
+
+type RenderTabDefine = IHomeCommonCategoryListTabDefine & {
+  categoryDefine?: CategoryDefine[];  
+};
+
+const tabDefines = computed(() => props.currentCommonCategoryContentDefine?.props.tabs || []);
+const tabRenderDefines = computed(() => {
+  const result = {} as Record<number, RenderTabDefine>;
+  try {
+    tabDefines.value.forEach((item, i) => {
+      const renderItem : RenderTabDefine = {
+        ...item,
+      };
+      function loadNestCategoryData(items: IHomeCommonCategoryListTabNestCategoryItemDefine[]) {
+        return items
+          .filter((item) => item.visible !== false)
+          .map((item) => {
+            return {
+              ...item,
+              showTitle: item.showTitle !== false,
+              title: item.text,
+              content: CommonCategoryListTabNestCategoryDataToContent(
+                item.data, item
+              ),
+              type: item.type as CategoryDefine['type'],
+            }
+          });
+      }
+      switch (item.type) {
+        case 'list':
+          renderItem.categoryDefine = loadNestCategoryData(item.preInsertCategorys || []);
+          break;
+        case 'jump':
+          break;
+        case 'nestCategory':
+          renderItem.categoryDefine = loadNestCategoryData(item.categorys);
+          break;
+      }
+      result[i] = renderItem;
+    });
+  } catch (error) {
+    emit('error', error);
+  }
+  return result;
+});
+const showListTabIds = computed(() => {
+  const define = tabDefines.value;
+  if (define.length === 0)
+    return undefined;
+  return define
+    .map((item, i) => ({ type: item.type, id: i }))
+    .filter((item) => item.type === 'list')
+    .map((item) => item.id);
+})
+const tabs = computed<CommonListPageProps['tabs']>(() => {
+  const define = tabDefines.value;
+  return define.map((item, i) => {
+    switch (item.type) {
+      default:
+      case 'list':
+        return {
+          id: i,
+          text: item.text,
+          width: item.width,
+        };
+      case 'jump':
+        return {
+          id: i,
+          text: item.text,
+          onlyJump: true,
+          jump: () => navTo(item.url, item.params),
+          width: item.width,
+        };
+      case 'nestCategory':
+        return {
+          id: i,
+          text: item.text,
+          width: item.width,
+        };
+    }
+  });
+});
+const dropdownNames = ref<DropDownNames[]>([]);
+const detailsPage = computed(() => {
+  if (tabDefines.value.find((item) => item.detailsPage)) {
+    const result = {} as Record<number, string|[string, Record<string, any>]>;
+    tabDefines.value.forEach((item, i) => {
+      result[i] = item.detailsPage || '';
+    });
+    return result;
+  }
+  return props.currentCommonCategoryContentDefine?.props.detailsPage || undefined;
+});
+
+async function loadData(
+  page: number, 
+  pageSize: number,
+  searchText: string,
+  dropDownValues: number[],
+  tabSelect: number
+) {
+  const tab = tabRenderDefines.value[tabSelect];
+  if (!tab) 
+    throw new Error(`配置有误 tab:${tabSelect}`);
+  if (tab.type !== 'list')
+    return { list: [], total: 0 };
+  if (!tab.data) 
+    throw new Error(`配置有误 tab:${tabSelect} 没有配置列表数据`);
+  const res = await doLoadDynamicListData(
+    tab.data, 
+    page, 
+    pageSize, 
+    searchText, 
+    tab.dropdownDefines || [],
+    dropDownValues,
+    props.parentData,
+  );
+  if (res && tab.dataSolve)
+    res.list = resolveCommonContentSolveProps(res.list, tab.dataSolve);
+  return res;
+}
+</script>

+ 1 - 0
src/pages/article/data/CommonCategoryPathDefine.ts

@@ -1 +1,2 @@
 export const CommonCategoryListPath = '/pages/article/data/list';
+export const CommonCategoryDetailPath = '/pages/article/data/detail';

+ 237 - 82
src/pages/article/data/DefaultCategory.json

@@ -12,69 +12,75 @@
           "homeButtons": [
             {
               "title": "常识一点通",
-              "icon": "https://mncdn.wenlvti.net/app_static/minnan/images/home/IconMap.png",
+              "icon": "https://mncdn.wenlvti.net/app_static/minnan/images/home/Button1.png",
               "size": 50,
               "link": [
                 "/pages/article/data/list",
                 {
                   "pageConfigName": "explore"
                 }
-              ]
+              ],
+              "style": "large-bg"
             },
             {
               "title": "闽南新鲜事",
-              "icon": "https://mncdn.wenlvti.net/app_static/minnan/images/home/IconDoc.png",
+              "icon": "https://mncdn.wenlvti.net/app_static/minnan/images/home/Button2.png",
               "size": 50,
               "link": [
                 "/pages/article/data/list",
                 {
                   "pageConfigName": "news"
                 }
-              ]
+              ],
+              "style": "large-bg"
             },
             {
               "title": "遗产报你知",
-              "icon": "https://mncdn.wenlvti.net/app_static/minnan/images/home/IconIch.png",
+              "icon": "https://mncdn.wenlvti.net/app_static/minnan/images/home/Button3.png",
               "size": 50,
               "link": [
                 "/pages/article/data/list",
                 {
                   "pageConfigName": "inhert"
                 }
-              ]
+              ],
+              "style": "large-bg"
             },
             {
               "title": "文化新视角",
-              "icon": "https://mncdn.wenlvti.net/app_static/minnan/images/home/IconReserch.png",
+              "icon": "https://mncdn.wenlvti.net/app_static/minnan/images/home/Button4.png",
               "size": 50,
               "link": [
                 "/pages/article/data/list",
                 {
                   "pageConfigName": "research"
                 }
-              ]
+              ],
+              "style": "large-bg"
             },
             {
               "title": "世界走透透",
-              "icon": "https://mncdn.wenlvti.net/app_static/minnan/images/home/IconArtifact.png",
+              "icon": "https://mncdn.wenlvti.net/app_static/minnan/images/home/Button5.png",
               "size": 50,
               "link": [
                 "/pages/article/data/list",
                 {
                   "pageConfigName": "communicate"
                 }
-              ]
+              ],
+              "style": "large-bg"
             },
             {
               "title": "来厦门䢐迌",
-              "icon": "https://mncdn.wenlvti.net/app_static/minnan/images/home/IconDiscover.png",
+              "icon": "https://mncdn.wenlvti.net/app_static/minnan/images/home/Button6.png",
               "size": 50,
               "link": [
                 "/pages/article/data/list",
                 {
                   "pageConfigName": "travel"
                 }
-              ]
+              ],
+              "style": "large-bg"
             }
           ],
           "categorys": [
@@ -105,7 +111,8 @@
                   }
                 ]
               },
-              "type": "StatsBlock"
+              "type": "StatsBlock",
+              "visible": false
             },
             {
               "text": "文化地图",
@@ -249,12 +256,7 @@
                     "inheritor"
                   ],
                   "detailsPage": "/pages/introduction/character/details",
-                  "morePage": [
-                    "/pages/article/data/list",
-                    {
-                      "pageConfigName": "character"
-                    }
-                  ],
+                  "morePage": "/pages/article/data/list?pageConfigName=character",
                   "type": "",
                   "textLevel": "h3"
                 },
@@ -296,6 +298,19 @@
                   "visible": false
                 },
                 {
+                  "text": "闽南语在线课程",
+                  "type": "horizontal-large",
+                  "data": {
+                    "type": "commonContent",
+                    "params": {
+                      "mainBodyColumnId": 257,
+                      "modelId": 5
+                    }
+                  },
+                  "visible": true,
+                  "detailsPage": "/pages/video/details"
+                },
+                {
                   "text": "政策法规",
                   "data": {
                     "type": "serializedApi",
@@ -305,12 +320,7 @@
                     "date"
                   ],
                   "type": "",
-                  "morePage": [
-                    "/pages/article/data/list",
-                    {
-                      "pageConfigName": "laws"
-                    }
-                  ],
+                  "morePage": "/pages/article/data/list?pageConfigName=laws",
                   "visible": false
                 }
               ]
@@ -343,7 +353,7 @@
                     }
                   },
                   "morePage": "/pages/travel/fashion/list",
-                  "detailsPage": "/pages/inhert/songs/details",
+                  "detailsPage": "/pages/article/data/details?pageConfigName=songs-details",
                   "type": "large-image"
                 },
                 {
@@ -359,7 +369,7 @@
                   },
                   "morePage": "/pages/travel/fashion/list",
                   "detailsPage": "/pages/video/details",
-                  "type": "large-grid2"
+                  "type": "large-image"
                 },
                 {
                   "text": "精品剧目",
@@ -377,7 +387,7 @@
                   "type": ""
                 },
                 {
-                  "text": "非遗作品秀",
+                  "text": "非遗秀",
                   "data": {
                     "type": "commonContent",
                     "params": {
@@ -543,7 +553,8 @@
                   },
                   "dataSolve": [
                     "common"
-                  ]
+                  ],
+                  "visible": false
                 },
                 {
                   "text": "闽南语在线课程",
@@ -555,7 +566,7 @@
                       "modelId": 5
                     }
                   },
-                  "visible": true,
+                  "visible": false,
                   "detailsPage": "/pages/video/details"
                 },
                 {
@@ -588,26 +599,34 @@
               "type": "nestCategory",
               "categorys": [
                 {
-                  "text": "闽南歌曲",
+                  "text": "景区、景点",
                   "data": {
                     "type": "commonContent",
                     "params": {
-                      "mainBodyColumnId": [
-                        315
-                      ],
-                      "modelId": 16
+                      "mainBodyColumnId": 273,
+                      "modelId": 17
                     }
                   },
-                  "visible": false,
-                  "morePage": "/pages/travel/fashion/list",
-                  "detailsPage": "/pages/video/details",
-                  "type": "large-grid2"
+                  "morePage": "/pages/travel/scenic-spot/list",
+                  "detailsPage": "/pages/inhert/intangible/details",
+                  "type": ""
                 },
                 {
-                  "text": "闽南节庆日历",
-                  "data": null,
-                  "morePage": "/pages/travel/calendar/index",
-                  "type": "CalendarBlock"
+                  "text": "文化旅游路线",
+                  "data": {
+                    "type": "commonContent",
+                    "params": {
+                      "mainBodyColumnId": [
+                        274,
+                        275,
+                        276,
+                        277
+                      ],
+                      "modelId": 17
+                    }
+                  },
+                  "detailsPage": "byContent",
+                  "type": "horizontal-large"
                 },
                 {
                   "text": "闽南美食",
@@ -622,17 +641,16 @@
                   "type": "large-grid2"
                 },
                 {
-                  "text": "景区、景点",
+                  "text": "美食动态",
+                  "type": "",
                   "data": {
                     "type": "commonContent",
                     "params": {
-                      "mainBodyColumnId": 273,
-                      "modelId": 17
+                      "modelId": 3,
+                      "mainBodyColumnId": 386
                     }
                   },
-                  "morePage": "/pages/travel/scenic-spot/list",
-                  "detailsPage": "/pages/inhert/intangible/details",
-                  "type": ""
+                  "textLevel": "h2"
                 },
                 {
                   "text": "市民文化汇",
@@ -650,21 +668,26 @@
                   "type": ""
                 },
                 {
-                  "text": "文化旅游路线",
+                  "text": "闽南歌曲",
                   "data": {
                     "type": "commonContent",
                     "params": {
                       "mainBodyColumnId": [
-                        274,
-                        275,
-                        276,
-                        277
+                        315
                       ],
-                      "modelId": 17
+                      "modelId": 16
                     }
                   },
-                  "detailsPage": "byContent",
-                  "type": "horizontal-large"
+                  "visible": false,
+                  "morePage": "/pages/travel/fashion/list",
+                  "detailsPage": "/pages/video/details",
+                  "type": "large-grid2"
+                },
+                {
+                  "text": "闽南节庆日历",
+                  "data": null,
+                  "morePage": "/pages/travel/calendar/index",
+                  "type": "CalendarBlock"
                 }
               ]
             }
@@ -935,6 +958,23 @@
               "type": "nestCategory",
               "categorys": [
                 {
+                  "text": "数据统计",
+                  "showTitle": false,
+                  "blockProps": {
+                    "statsNameConfig": [
+                      {
+                        "name": "projects",
+                        "title": "非物质文化遗产代表性项目"
+                      },
+                      {
+                        "name": "inheritors",
+                        "title": "非物质文化遗产代表性传承人"
+                      }
+                    ]
+                  },
+                  "type": "StatsBlock"
+                },
+                {
                   "text": "非遗项目",
                   "data": {
                     "type": "serializedApi",
@@ -943,13 +983,7 @@
                   "dataSolve": [
                     "ich"
                   ],
-                  "morePage": [
-                    "/pages/article/data/list",
-                    {
-                      "pageConfigName": "intangible",
-                      "tab": 0
-                    }
-                  ],
+                  "morePage": "/pages/article/data/list?pageConfigName=intangible&tab=0",
                   "detailsPage": "/pages/inhert/intangible/details",
                   "type": "horizontal-large"
                 },
@@ -987,13 +1021,7 @@
                   "dataSolve": [
                     "ich"
                   ],
-                  "morePage": [
-                    "/pages/article/data/list",
-                    {
-                      "pageConfigName": "seminar",
-                      "tab": 0
-                    }
-                  ],
+                  "morePage": "/pages/article/data/list?pageConfigName=seminar&tab=0",
                   "detailsPage": "/pages/inhert/seminar/details",
                   "type": ""
                 },
@@ -1012,7 +1040,8 @@
                   "detailsPage": "byContent",
                   "type": "large-grid2"
                 }
-              ]
+              ],
+              "width": 250
             },
             {
               "text": "物质文化遗产",
@@ -1024,13 +1053,7 @@
               "dataSolve": [
                 "ich"
               ],
-              "morePage": [
-                "/pages/article/data/list",
-                {
-                  "pageConfigName": "artifact",
-                  "tab": 0
-                }
-              ],
+              "morePage": "/pages/article/data/list?pageConfigName=artifact&tab=0",
               "detailsPage": "/pages/inhert/artifact/details",
               "itemType": "image-large-2",
               "dropdownDefines": [
@@ -1064,7 +1087,23 @@
                     "typeId": 1
                   }
                 }
-              ]
+              ],
+              "preInsertCategorys": [
+                {
+                  "text": "数据统计",
+                  "showTitle": false,
+                  "blockProps": {
+                    "statsNameConfig": [
+                      {
+                        "name": "minnanCr",
+                        "title": "闽南文化重要相关文物古迹"
+                      }
+                    ]
+                  },
+                  "type": "StatsBlock"
+                }
+              ],
+              "width": 250
             },
             {
               "text": "重点保护区域",
@@ -1081,7 +1120,23 @@
               ],
               "detailsPage": "/pages/inhert/artifact/details",
               "itemType": "image-large-2",
-              "dropdownDefines": []
+              "dropdownDefines": [],
+              "preInsertCategorys": [
+                {
+                  "text": "数据统计",
+                  "showTitle": false,
+                  "blockProps": {
+                    "statsNameConfig": [
+                      {
+                        "name": "historyData",
+                        "title": "重要相关历史风貌区"
+                      }
+                    ]
+                  },
+                  "type": "StatsBlock"
+                }
+              ],
+              "width": 250
             },
             {
               "text": "世界文化遗产",
@@ -1096,7 +1151,8 @@
               "dropdownDefines": [],
               "dataSolve": [
                 "ich"
-              ]
+              ],
+              "width": 250
             }
           ]
         }
@@ -1155,6 +1211,105 @@
           "itemType": "article-common"
         }
       }
+    },
+    {
+      "name": "intangible-details",
+      "title": "非遗详情页",
+      "content": {
+        "type": "Details",
+        "props": {
+          "commonRefName": "作品",
+          "commonRefTarget": "product",
+          "introBlockDescs": [
+            {
+              "label": "项目级别",
+              "key": "levelText"
+            },
+            {
+              "label": "项目类别",
+              "key": "ichTypeText"
+            },
+            {
+              "label": "批次时间",
+              "key": "batchText"
+            },
+            {
+              "label": "所属区域",
+              "key": "regionText"
+            },
+            {
+              "label": "保护单位",
+              "key": "unit"
+            },
+            {
+              "label": "地址",
+              "key": "address"
+            },
+            {
+              "label": "其他级别保护单位",
+              "key": "otherLevel"
+            },
+            {
+              "label": "字号名称",
+              "key": "fontName"
+            },
+            {
+              "label": "认定类型",
+              "key": "brandType"
+            }
+          ],
+          "introBlocks": [
+            {
+              "type": "OtherLevelList",
+              "props": {}
+            }
+          ]
+        }
+      }
+    },
+    {
+      "name": "songs-details",
+      "title": "双歌赛详情页",
+      "content": {
+        "type": "Details",
+        "props": {
+          "introBlockDescs": [],
+          "introBlocks": [],
+          "tabs": [
+            {
+              "text": "简介",
+              "type": "intro"
+            },
+            {
+              "text": "图片",
+              "type": "images"
+            },
+            {
+              "text": "视频",
+              "type": "list",
+              "define": {
+                "props": {
+                  "showTab": false,
+                  "showSearch": false,
+                  "tabs": [
+                    {
+                      "text": "Root",
+                      "type": "list",
+                      "data": {
+                        "type": "parentKey",
+                        "key": "associationMeList"
+                      },
+                      "itemType": "image-large-2",
+                      "detailsPage": "/pages/video/details",
+                      "dataSolve": []
+                    }
+                  ]
+                }
+              }
+            }
+          ]
+        }
+      }
     }
   ]
 }

+ 98 - 38
src/pages/article/data/defines/Details.ts

@@ -3,50 +3,110 @@
  * 详情页定义
  */
 
-import type { IHomeCommonCategoryListTabNestCategoryItemDefine } from "./List";
+import type { CommonCategoryDetailProps } from "../CommonCategoryDetail.vue";
+import type { IHomeCommonCategoryListDefine, IHomeCommonCategoryListTabNestCategoryItemDefine } from "./List";
 
 /**
  * 页面模板:首页定义
  */
-export interface IHomeCommonCategoryHomeDefine {
+export interface IHomeCommonCategoryDetailDefine {
   type: 'Details',
-  props: {
+  props: Omit<CommonCategoryDetailProps, 'load'|'extraTabs'> & {
     /**
-     * 首页标题
+     * 列表选项卡定义
      */
-    title: string,
-    /**
-     * 首页副标题
-     */
-    subTitle: string,
-    /**
-     * 首页banner图
-     */
-    homeBanner: string,
-    /**
-     * 首页按钮
-     */
-    homeButtons: {
-      /**
-       * 按钮标题
-       */
-      title: string,
-      /**
-       * 按钮图标
-       */
-      icon: string,
-      /**
-       * 按钮跳转链接
-       */
-      link: [string, object],
-      /**
-       * 按钮大小
-       */
-      size: number
-    }[],
-    /**
-     * 首页分类项
-     */
-    categorys: IHomeCommonCategoryListTabNestCategoryItemDefine[],
+    tabs?: IHomeCommonCategoryDetailTabItemDefine[],
   },
 }
+
+
+/**
+ * TAB定义 - 类型:嵌套子分类 - 子分类项定义
+ */
+export type IHomeCommonCategoryDetailTabItemDefine = 
+    (IHomeCommonCategoryDetailTabItemInternalDefine
+      | IHomeCommonCategoryDetailTabItemListDefine 
+      | IHomeCommonCategoryDetailTabItemNestCategoryDefine
+      | IHomeCommonCategoryDetailTabItemRichDefine
+      | IHomeCommonCategoryDetailTabItemMapDefine
+    ) & IHomeCommonCategoryDetailTabItemBaseDefine;
+
+/**
+ * TAB定义 - 类型:嵌套子分类 - 通用定义
+ */
+export interface IHomeCommonCategoryDetailTabItemBaseDefine {
+  /**
+   * 是否可见
+   * @default true
+   */
+  visible?: boolean,
+  /**
+   * 标签页文本
+   */
+  text: string,
+  /**
+   * 标签页宽度
+   * @default 180
+   */
+  width?: number,
+
+  /**
+   * 判断数据键
+   */
+  key: string,
+  type: string,
+}
+
+/**
+ * TAB定义 - 类型:嵌套子分类 - 通用定义
+ */
+export interface IHomeCommonCategoryDetailTabItemInternalDefine {
+  /**
+   * 标签页内容显示类型
+   * * 'intro' - 简介
+   * * 'images' - 相册图片
+   * * 'video' - 单视频
+   * * 'audio' - 单音频
+   */
+  type: 'intro'|'images'|'video'|'audio',
+}
+/**
+ * TAB定义 - 类型:嵌套子分类 - 列表类型
+ */
+export interface IHomeCommonCategoryDetailTabItemListDefine {
+  type: 'list',
+  /**
+   * 列表数据
+   */
+  define: IHomeCommonCategoryListDefine,
+}
+
+/**
+ * TAB定义 - 类型:嵌套子分类 - 富文本类型
+ */
+export interface IHomeCommonCategoryDetailTabItemRichDefine {
+  type: 'rich',
+  /**
+   * 数据键
+   */
+  key: string,
+}
+
+
+/**
+ * TAB定义 - 类型:嵌套子分类 - 地图位置显示类型
+ */
+export interface IHomeCommonCategoryDetailTabItemMapDefine {
+  type: 'map',
+}
+
+/**
+ * TAB定义 - 类型:嵌套子分类 - 嵌套子分类类型
+ */
+export interface IHomeCommonCategoryDetailTabItemNestCategoryDefine {
+  type: 'nestCategory',
+  /**
+   * 首页分类项
+   */
+  categorys: IHomeCommonCategoryListTabNestCategoryItemDefine[],
+}

+ 3 - 1
src/pages/article/data/defines/List.ts

@@ -162,7 +162,7 @@ export interface IHomeCommonCategoryListTabNestCategoryItemDefine {
   /**
    * 更多页面定义
    */
-  morePage?: string|[string, Record<string, any>],
+  morePage?: string,
   /**
    * 详情页面定义
    */
@@ -202,6 +202,8 @@ export function CommonCategoryListTabNestCategoryDataToContent(
   switch (data.type) {
     case 'serializedApi':
       return CommonCategoryDynamicDataSerializedApi(data);
+    case 'parentKey':
+      throw new Error(`未实现的动态数据接口 ${data.type}`);
     case 'request':
       throw new Error(`未实现的动态数据接口 ${data.type}`);
     case 'commonContent':

+ 29 - 0
src/pages/article/data/details.vue

@@ -0,0 +1,29 @@
+<template>
+  <CommonCategoryDetail
+    :pageConfigName="querys.pageConfigName"
+    :pageQuerys="rawQuerys"
+  />
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app';
+import { useLoadQuerys } from '@/common/composeabe/LoadQuerys';
+import CommonCategoryDetail from './CommonCategoryDetail.vue';
+
+const pageRef = ref();
+
+/**
+ * 动态通用内容 - 通用详情页
+ */
+const { querys, rawQuerys } = useLoadQuerys({
+  pageConfigName: '',
+});
+
+onShareTimeline(() => {
+  return pageRef.value?.getPageShareData() || {}; 
+})
+onShareAppMessage(() => {
+  return pageRef.value?.getPageShareData() || {};
+})
+</script>

+ 1 - 1
src/pages/article/data/editor/editors/CommonListPropsEditor.vue

@@ -14,7 +14,7 @@
         <ItemTypeEditor v-model="props.props.itemType" />
       </a-form-item>
       <a-form-item label="详情页">
-        <LinkPathEditor v-model="(props.props.detailsPage as string)" :noParams="true" />
+        <LinkPathEditor v-model="(props.props.detailsPage as string)" />
       </a-form-item>
     </a-form>
 

+ 1 - 1
src/pages/article/data/editor/subpart/NestCategoryEditor.vue

@@ -65,7 +65,7 @@
               <ItemTypeEditor v-model="cat.itemType" />
             </a-form-item>
             <a-form-item label="列表页详情页">
-              <LinkPathEditor v-model:value="cat.detailsPage" :noParams="true" />
+              <LinkPathEditor v-model:value="cat.detailsPage" />
             </a-form-item>
             <a-popconfirm title="确定删除该子分类吗?" @confirm="remove(i)">
               <a-button type="link" danger size="small">删除子分类</a-button>

+ 9 - 1
src/pages/inhert/intangible/details.vue

@@ -1,9 +1,17 @@
 <template>
-  <DetailsCommon ref="pageRef" commonRefName="作品" commonRefTarget="product" />
+  <DetailsCommon 
+    ref="pageRef"
+    commonRefName="作品"
+    commonRefTarget="product" 
+    :overrideInternalTabsName="{
+      [TAB_ID_VIDEO]: '资料影像',
+    }"
+  />
 </template>
 
 <script setup lang="ts">
 import { ref } from 'vue';
+import { TAB_ID_VIDEO } from '@/pages/article/common/DetailTabPage';
 import DetailsCommon from '@/pages/article/common/DetailsCommon.vue';
 import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app';