Procházet zdrojové kódy

🎨 按要求调整栏目

快乐的梦鱼 před 1 týdnem
rodič
revize
498fe4ef4c

+ 30 - 92
package-lock.json

@@ -27,7 +27,7 @@
         "@imengyu/imengyu-utils": "^0.0.26",
         "@imengyu/js-request-transform": "^0.3.7",
         "async-validator": "^4.2.5",
-        "htmlparser2": "^10.1.0",
+        "parse5": "^8.0.0",
         "pinia": "^3.0.1",
         "tslib": "^2.8.1",
         "vue": "3.5.27",
@@ -7144,37 +7144,11 @@
         "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
       }
     },
-    "node_modules/dom-serializer": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-2.0.0.tgz",
-      "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
-      "license": "MIT",
-      "dependencies": {
-        "domelementtype": "^2.3.0",
-        "domhandler": "^5.0.2",
-        "entities": "^4.2.0"
-      },
-      "funding": {
-        "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
-      }
-    },
     "node_modules/dom-walk": {
       "version": "0.1.2",
       "resolved": "https://registry.npmmirror.com/dom-walk/-/dom-walk-0.1.2.tgz",
       "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
     },
-    "node_modules/domelementtype": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmmirror.com/domelementtype/-/domelementtype-2.3.0.tgz",
-      "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/fb55"
-        }
-      ],
-      "license": "BSD-2-Clause"
-    },
     "node_modules/domexception": {
       "version": "2.0.1",
       "resolved": "https://registry.npmmirror.com/domexception/-/domexception-2.0.1.tgz",
@@ -7201,35 +7175,6 @@
         "node": ">=8"
       }
     },
-    "node_modules/domhandler": {
-      "version": "5.0.3",
-      "resolved": "https://registry.npmmirror.com/domhandler/-/domhandler-5.0.3.tgz",
-      "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
-      "license": "BSD-2-Clause",
-      "dependencies": {
-        "domelementtype": "^2.3.0"
-      },
-      "engines": {
-        "node": ">= 4"
-      },
-      "funding": {
-        "url": "https://github.com/fb55/domhandler?sponsor=1"
-      }
-    },
-    "node_modules/domutils": {
-      "version": "3.2.2",
-      "resolved": "https://registry.npmmirror.com/domutils/-/domutils-3.2.2.tgz",
-      "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
-      "license": "BSD-2-Clause",
-      "dependencies": {
-        "dom-serializer": "^2.0.0",
-        "domelementtype": "^2.3.0",
-        "domhandler": "^5.0.3"
-      },
-      "funding": {
-        "url": "https://github.com/fb55/domutils?sponsor=1"
-      }
-    },
     "node_modules/dunder-proto": {
       "version": "1.0.1",
       "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -8134,37 +8079,6 @@
       "license": "MIT",
       "peer": true
     },
-    "node_modules/htmlparser2": {
-      "version": "10.1.0",
-      "resolved": "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-10.1.0.tgz",
-      "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
-      "funding": [
-        "https://github.com/fb55/htmlparser2?sponsor=1",
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/fb55"
-        }
-      ],
-      "license": "MIT",
-      "dependencies": {
-        "domelementtype": "^2.3.0",
-        "domhandler": "^5.0.3",
-        "domutils": "^3.2.2",
-        "entities": "^7.0.1"
-      }
-    },
-    "node_modules/htmlparser2/node_modules/entities": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz",
-      "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
-      "license": "BSD-2-Clause",
-      "engines": {
-        "node": ">=0.12"
-      },
-      "funding": {
-        "url": "https://github.com/fb55/entities?sponsor=1"
-      }
-    },
     "node_modules/http-errors": {
       "version": "2.0.0",
       "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz",
@@ -9382,6 +9296,14 @@
         }
       }
     },
+    "node_modules/jsdom/node_modules/parse5": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmmirror.com/parse5/-/parse5-6.0.1.tgz",
+      "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true
+    },
     "node_modules/jsdom/node_modules/ws": {
       "version": "7.5.10",
       "resolved": "https://registry.npmmirror.com/ws/-/ws-7.5.10.tgz",
@@ -10213,12 +10135,28 @@
       "peer": true
     },
     "node_modules/parse5": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmmirror.com/parse5/-/parse5-6.0.1.tgz",
-      "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
-      "dev": true,
+      "version": "8.0.0",
+      "resolved": "https://registry.npmmirror.com/parse5/-/parse5-8.0.0.tgz",
+      "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
       "license": "MIT",
-      "peer": true
+      "dependencies": {
+        "entities": "^6.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/inikulin/parse5?sponsor=1"
+      }
+    },
+    "node_modules/parse5/node_modules/entities": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz",
+      "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
     },
     "node_modules/parseurl": {
       "version": "1.3.3",

+ 1 - 1
package.json

@@ -54,7 +54,7 @@
     "@imengyu/imengyu-utils": "^0.0.26",
     "@imengyu/js-request-transform": "^0.3.7",
     "async-validator": "^4.2.5",
-    "htmlparser2": "^10.1.0",
+    "parse5": "^8.0.0",
     "pinia": "^3.0.1",
     "tslib": "^2.8.1",
     "vue": "3.5.27",

+ 9 - 2
src/api/CommonContent.ts

@@ -428,10 +428,17 @@ export class CommonContentApi extends AppServerRequestModule<DataModel> {
    * @param querys 额外参数
    * @returns 
    */
-  async getModelColumList<T extends DataModel = GetModelColumContentList>(model_id: number, page: number, pageSize: number = 10, querys?: QueryParams) {
+  async getModelColumList<T extends DataModel = GetModelColumContentList>(
+    modelId: number, 
+    mainBodyColumnId?: number,
+    page: number = 1, 
+    pageSize: number = 50, 
+    querys?: QueryParams
+  ) {
     const res = await this.get('/content/main_body_column/getColumnList', `${this.debugName} 模型的主体栏目列表`, {
       main_body_id: this.mainBodyId,
-      model_id: model_id ?? this.modelId,
+      model_id: modelId ?? this.modelId,
+      main_body_column_id: mainBodyColumnId,
       page,
       pageSize,
       ...querys

+ 23 - 24
src/components/display/parse/Parse.vue

@@ -6,10 +6,9 @@
 
 <script setup lang="ts">
 import ParseNodeRender from './ParseNodeRender.vue'
-import { parseDocument } from 'htmlparser2'
+import { parse, type DefaultTreeAdapterTypes } from 'parse5'
 import { computed, provide, toRef } from 'vue';
 import type { ParseNode } from './Parse'
-import type { Element } from 'domhandler';
 
 export interface ParseProps {
   /**
@@ -32,60 +31,60 @@ const props = withDefaults(defineProps<ParseProps>(), {
 
 provide('tagStyle', toRef(props, 'tagStyle'));
 
+const toObj = (attrs: DefaultTreeAdapterTypes.Element['attrs']) => {
+  const obj: Record<string, string> = {};
+  for (const attr of attrs) {
+    obj[attr.name] = attr.value;
+  }
+  return obj;
+}
 // 解析HTML为节点树
 const parseHtml = (html: string): ParseNode[] => {
   const nodes: ParseNode[] = [];
-  const doc = parseDocument(html);
+  const doc = parse(html);
   
-  const traverse = (element: Element): ParseNode => {
-    if (element.type !== 'tag') {
-      return { tag: 'text' };
-    }
-
+  const traverse = (element: DefaultTreeAdapterTypes.Element): ParseNode => {
     const node: ParseNode = {
-      tag: element.name,
-      attrs: element.attribs || {},
+      tag: element.tagName,
+      attrs: toObj(element.attrs),
       children: []
     };
     
     // 解析子节点
-    if (element.children) {
-      for (const child of element.children) {
-        if (child.type === 'tag') {
-          node.children?.push(traverse(child));
-        } else if (child.type === 'text' && child.data?.trim()) {
+    if (element.childNodes) {
+      for (const child of element.childNodes) {
+        if (child.nodeName === '#text') {
           node.children?.push({
             tag: 'text',
             attrs: {
-              content: child.data
+              content: (child as DefaultTreeAdapterTypes.TextNode).value
             }
           });
+        } else if (child.nodeName !== '#comment' && child.nodeName !== '#documentType') {
+          node.children?.push(traverse(child as DefaultTreeAdapterTypes.Element));
         }
       }
     }
     
     return node;
   };
-  for (const child of doc.children) {
-    if (child.type === 'tag') {
-      nodes.push(traverse(child));
-    } else if (child.type === 'text' && child.data?.trim()) {
+  for (const child of doc.childNodes) {
+    if (child.nodeName === '#text') {
       nodes.push({
         tag: 'text',
         attrs: {
-          content: child.data
+          content: (child as DefaultTreeAdapterTypes.TextNode).value
         }
       });
+    } else if (child.nodeName !== '#documentType') {
+      nodes.push(traverse(child as DefaultTreeAdapterTypes.Element));
     }
   }
-
-  console.log(nodes);
   return nodes;
 };
 
 // 计算属性,获取解析后的节点树
 const nodes = computed(() => parseHtml(props.content));
-
 </script>
 
 <style scoped>

+ 0 - 182
src/components/display/parse/ParseNodeRenderWrapper.vue

@@ -1,182 +0,0 @@
-<template>
-  <!-- 图片 -->
-  <image 
-    v-if="node.tag === 'img'" 
-    :id="node.attrs?.id" 
-    :class="'_img ' + (node.attrs?.class || '')" 
-    :style="node.attrs?.style || {}" 
-    :src="node.attrs?.src || ''" 
-    mode="widthFix" 
-  />
-  
-  <!-- 换行 -->
-  <text v-else-if="node.tag === 'br'">\n</text>
-  
-  <!-- 链接 -->
-  <view 
-    v-else-if="node.tag === 'a'" 
-    :id="node.attrs?.id" 
-    :class="(node.attrs?.href ? '_a ' : '') + (node.attrs?.class || '')" 
-    hover-class="_hover" 
-    :style="'display:inline;' + (node.attrs?.style || '')" 
-    @tap.stop="linkTap" 
-  >
-    <ParseNodeRender 
-      v-for="(child, index) in node.children" 
-      :key="index" 
-      :node="child" 
-    />
-  </view>
-  
-  <!-- 视频 -->
-  <video 
-    v-else-if="node.tag === 'video'" 
-    :id="node.attrs?.id" 
-    :class="node.attrs?.class || ''" 
-    :style="node.attrs?.style || {}" 
-    :autoplay="Boolean(node.attrs?.autoplay || false)" 
-    :controls="Boolean(node.attrs?.controls || true)" 
-    :loop="Boolean(node.attrs?.loop || false)" 
-    :muted="Boolean(node.attrs?.muted || false)" 
-    :object-fit="node.attrs?.['object-fit'] || 'contain'" 
-    :poster="node.attrs?.poster as string || ''" 
-    :src="node.attrs?.src as string || ''" 
-  />
-  
-  <!-- 音频 -->
-  <audio 
-    v-else-if="node.tag === 'audio'" 
-    :id="node.attrs?.id" 
-    :class="node.attrs?.class || ''" 
-    :style="node.attrs?.style || {}" 
-    :author="node.attrs?.author || ''" 
-    :controls="Boolean(node.attrs?.controls || true)" 
-    :loop="Boolean(node.attrs?.loop || false)" 
-    :name="node.attrs?.name || ''" 
-    :poster="node.attrs?.poster || ''" 
-    :src="node.attrs?.src as string || ''" 
-  />
-
-  <!-- 文本 -->
-  <text 
-    v-else-if="node.tag === 'text' || node.tag === 'span'"
-    :style="style"
-    :class="node.attrs?.class || ''" 
-  >
-    {{ node.attrs?.content }}
-  </text>
-  
-  <!-- 其他标签 -->
-  <view
-    v-else-if="node.tag !== 'text'"
-    :id="node.attrs?.id"
-    :data-tag="node.tag"
-    :class="node.attrs?.class || ''"
-    :style="style"
-  >
-    <ParseNodeRender
-      v-for="(child, index) in node.children"
-      :key="index"
-      :node="child"
-    />
-  </view>
-
-</template>
-
-<script setup lang="ts">
-import { computed, inject, ref, type Ref } from 'vue';
-import type { ParseNode } from './Parse';
-
-const props = withDefaults(defineProps<{
-  node: ParseNode;
-}>(), {
-});
-
-const tagStyle = inject<Ref<Record<string, string>>>('tagStyle', ref({}));
-const style = computed(() => 
-  [(props.node.attrs?.style || ''), (tagStyle.value[props.node.tag] || '')].join(';'),
-);
-
-// 链接点击事件
-const linkTap = (e: any) => {
-  const href = props.node.attrs?.href as string;
-  if (href) {
-    if (href[0] === '#') {
-      // 跳转锚点
-      // 实现锚点跳转逻辑
-    } else if (href.includes('://')) {
-      // 外部链接
-      uni.showModal({
-        title: '打开链接',
-        content: href,
-        success: (res) => {
-          if (res.confirm) {
-            // #ifdef H5
-            window.open(href);
-            // #endif
-            // #ifdef MP
-            uni.setClipboardData({
-              data: href,
-              success: () => {
-                uni.showToast({
-                  title: '链接已复制',
-                  duration: 2000
-                });
-              }
-            });
-            // #endif
-            // #ifdef APP-PLUS
-            plus.runtime.openWeb(href);
-            // #endif
-          }
-        }
-      });
-    } else {
-      // 跳转页面
-      uni.navigateTo({
-        url: href,
-        fail: () => {
-          uni.switchTab({
-            url: href,
-            fail: () => {}
-          });
-        }
-      });
-    }
-  }
-};
-
-defineOptions({
-  options: {
-    inheritAttrs: false,
-    virtualHost: true,
-  }
-})
-</script>
-
-<style scoped>
-/* a 标签默认效果 */
-._a {
-  padding: 1.5px 0;
-  color: #366092;
-  word-break: break-all;
-}
-
-/* a 标签点击态效果 */
-._hover {
-  text-decoration: underline;
-  opacity: 0.7;
-}
-
-/* 图片默认效果 */
-._img {
-  max-width: 100%;
-  -webkit-touch-callout: none;
-}
-
-/* 视频默认效果 */
-._video {
-  width: 300px;
-  height: 225px;
-}
-</style>

+ 0 - 7
src/pages.json

@@ -96,13 +96,6 @@
       }
     },
     {
-      "path": "pages/inhert/artifact/list",
-      "style": {
-        "navigationBarTitleText": "相关文物古迹",
-        "enablePullDownRefresh": true
-      }
-    },
-    {
       "path": "pages/inhert/artifact/details",
       "style": {
         "navigationBarTitleText": "文物详情"

+ 1 - 1
src/pages/article/data/CommonCategoryBlocks.vue

@@ -2,7 +2,7 @@
   <!--通用内容首页小分块组件-->
   <FlexCol width="100%">
     <!-- 分类 -->
-    <template v-for="category in categoryDatas" :key="category.title">
+    <template v-for="(category,i) in categoryDatas" :key="i">
       <HomeTitle 
         v-if="category.showTitle"
         :title="category.title"

+ 4 - 63
src/pages/article/data/CommonCategoryDynamicData.ts

@@ -26,6 +26,10 @@ import TeamsContent from "@/api/research/TeamsContent";
 import type { IHomeCommonCategoryListTabListDropdownDefine } from "./CommonCategoryDefine";
 import DiscussContent from "@/api/research/DiscussContent";
 import ResultContent from "@/api/research/ResultContent";
+import { CommonCategorDynamicDropDownValuesToParams } from "./data-defines/Dropdown";
+
+export * from './data-defines/Category';
+export * from './data-defines/Dropdown';
 
 //默认动态数据接口定义
 
@@ -55,36 +59,6 @@ export type IHomeCommonCategoryDynamicData = IHomeCommonCategoryDynamicDataCommo
   | IHomeCommonCategoryDynamicDataSerializedApi 
   | IHomeCommonCategoryDynamicDataRequest;
 
-// 下拉列表动态数据接口定义
-
-export interface IHomeCommonCategoryDropdownDynamicDataCommonContent {
-  type: 'commonContent',
-  url?: string,
-  typeId: number, // 分类类型id
-  otherParams?: Record<string, any>,
-  idKey?: string,
-  nameKey?: string,
-}
-export interface IHomeCommonCategoryDropdownStaticData {
-  type: 'static',
-  data: Record<string, any>[],
-  idKey?: string,
-  nameKey?: string,
-}
-export interface IHomeCommonCategoryDropdownDynamicDataRequest {
-  type: 'request',
-  method: "OPTIONS" | "GET" | "HEAD" | "POST" | "PUT" | "DELETE",
-  url: string,
-  querys?: Record<string, any>,
-  params?: Record<string, any>,
-  idKey?: string,
-  nameKey?: string,
-}
-export type IHomeCommonCategoryDropdownDynamicData = 
-  IHomeCommonCategoryDropdownDynamicDataCommonContent 
-  | IHomeCommonCategoryDropdownDynamicDataRequest
-  | IHomeCommonCategoryDropdownStaticData;
-
 /**
  * 动态数据序列化接口
  */
@@ -117,40 +91,7 @@ export function CommonCategoryDynamicDataSerializedApi(item: IHomeCommonCategory
       throw new Error(`未实现的序列化接口 ${item.name}`);
   }
 }
-function CommonCategorDynamicDropDownValuesToParams(dropDownValues: (number|string|boolean)[], dropdownDefines: IHomeCommonCategoryListTabListDropdownDefine[]) {
-  const params: Record<string, any> = {};
-  dropdownDefines.forEach((define, index) => {
-    const v = dropDownValues[index]
-    if (typeof v === 'number' && v > 0)
-      params[define.key] = v;
-    else if (typeof v === 'string')
-      params[define.key] = v;
-  });
-  return params;
-}
-
 //加载接口
-
-export async function doLoadDynamicDropdownData(item: IHomeCommonCategoryDropdownDynamicData) {
-  switch (item.type) {
-    case 'commonContent':
-      return (await CommonContent.getCategoryList(item.typeId)) as unknown as Record<string, any>[];
-    case 'static':
-      return item.data;
-    case 'request':
-      return (await CommonContent.request(
-        item.url, 
-        { ...item.querys }, 
-        {
-          method: item.method, 
-          data: item.params,
-        },
-        '',
-        undefined,
-      )).data as unknown as Record<string, any>[];
-  }
-  throw new Error(`未实现的动态数据接口`);
-}
 export async function doLoadDynamicListData(
   item: IHomeCommonCategoryDynamicData,
   page: number,

+ 8 - 1
src/pages/article/data/CommonCategoryList.vue

@@ -27,7 +27,7 @@
 import { computed, onMounted, ref, watch } from 'vue';
 import { injectCommonCategory } from './CommonCategoryGlobalLoader';
 import { navTo } from '@/components/utils/PageAction';
-import { doLoadDynamicDropdownData, doLoadDynamicListData } from './CommonCategoryDynamicData';
+import { doLoadDynamicCategoryDataMergeTypeGetColumns, doLoadDynamicDropdownData, doLoadDynamicListData } from './CommonCategoryDynamicData';
 import { CommonCategoryListTabNestCategoryDataToContent, type IHomeCommonCategoryDefine, type IHomeCommonCategoryListDefine, type IHomeCommonCategoryListTabDefine } from './CommonCategoryDefine';
 import { resolveCommonContentSolveProps } from '../common/CommonContent';
 import { waitTimeOut } from '@imengyu/imengyu-utils';
@@ -86,6 +86,12 @@ async function loadPageConfig() {
   })
 
   await waitTimeOut(200);
+  
+  //特殊处理
+  for (const [_, tab] of Object.entries(tabRenderDefines.value)) {
+    if (tab.type === 'nestCategory')
+      await doLoadDynamicCategoryDataMergeTypeGetColumns(tab.categorys)
+  }
 
   //加载下拉列表
   const result = [] as DropDownNames[];
@@ -116,6 +122,7 @@ async function loadPageConfig() {
   }
   loadState.value = true;
   dropdownNames.value = result;
+
 }
 
 watch(() => props.pageConfigName, loadPageConfig);

+ 69 - 5
src/pages/article/data/DefaultCategory.json

@@ -26,7 +26,7 @@
               "title": "遗产报你知",
               "icon": "https://mncdn.wenlvti.net/app_static/minnan/images/home/IconIch.png",
               "size": 50,
-              "link": ["/pages/article/data/inhert", { "pageConfigName": "inhert" }]
+              "link": ["/pages/article/data/list", { "pageConfigName": "inhert" }]
             },
             {
               "title": "文化新视角",
@@ -438,6 +438,7 @@
                 },
                 {
                   "text": "闽南语在线课程",
+                  "type": "horizontal-large",
                   "data": {
                     "type": "commonContent",
                     "params": {
@@ -445,8 +446,17 @@
                       "modelId": 5
                     }
                   },
-                  "detailsPage": "/pages/video/details",
-                  "type": "horizontal-large"
+                  "dataSolve": [ "common" ],
+                  "detailsPage": "/pages/video/details"
+                },
+                {
+                  "type": "speicalMergeItem:getColumns:horizontal-large",
+                  "params": {
+                    "mainBodyColumnId": 366,
+                    "modelId": 18
+                  },
+                  "dataSolve": [ "common" ],
+                  "detailsPage": "byContent"
                 }
               ]
             }
@@ -708,6 +718,61 @@
       }
     },
     {
+      "name": "artifact",
+      "title": "相关文物古迹",
+      "content": {
+        "type": "CommonList",
+        "props": {
+          "showTab": false,
+          "tabs": [
+            {
+              "text": "物质文化遗产",
+              "type": "list",
+              "data": {
+                "type": "serializedApi",
+                "name": "UnmoveableContent"
+              },
+              "dataSolve": [ "ich" ],
+              "detailsPage": "/pages/inhert/artifact/details",
+              "itemType": "image-large-2",
+              "dropdownDefines": [
+                {
+                  "key": "crType",
+                  "text": "分类",
+                  "defaultValue": 0,
+                  "addAll": "全部分类",
+                  "data": {
+                    "type": "commonContent",
+                    "typeId": 3
+                  }
+                },
+                {
+                  "key": "level",
+                  "text": "级别",
+                  "defaultValue": 0,
+                  "addAll": "全部级别",
+                  "data": {
+                    "type": "commonContent",
+                    "typeId": 2
+                  }
+                },
+                {
+                  "key": "region",
+                  "text": "地区",
+                  "defaultValue": 0,
+                  "addAll": "全部地区",
+                  "data": {
+                    "type": "commonContent",
+                    "typeId": 1
+                  }
+                }
+              ]
+            }
+          ]
+        }
+      }
+    },
+    {
       "name": "inhert",
       "title": "遗产报你知",
       "content": {
@@ -785,7 +850,7 @@
                 "name": "UnmoveableContent"
               },
               "dataSolve": [ "ich" ],
-              "morePage": "/pages/inhert/artifact/list",
+              "morePage": ["/pages/article/data/list", { "pageConfigName": "artifact", "tab": 0 }],
               "detailsPage": "/pages/inhert/artifact/details",
               "itemType": "image-large-2",
               "dropdownDefines": [
@@ -832,7 +897,6 @@
                 }
               },
               "dataSolve": [ "ich" ],
-              "morePage": "/pages/inhert/artifact/list",
               "detailsPage": "/pages/inhert/artifact/details",
               "itemType": "image-large-2",
               "dropdownDefines": []

+ 81 - 0
src/pages/article/data/data-defines/Category.ts

@@ -0,0 +1,81 @@
+import CommonContent from "@/api/CommonContent";
+import type { IHomeCommonCategoryListTabNestCategoryItemDefine } from "../CommonCategoryDefine";
+import type { IHomeCommonCategoryDynamicDataCommonContent } from "../CommonCategoryDynamicData";
+
+export interface IHomeCommonCategoryCategoryDynamicDataMergeTypeGetColumns {
+  type: 'speicalMergeItem:getColumns',
+  params: {
+    modelId: number,
+    mainBodyColumnId: number,
+  },
+}
+export interface IHomeCommonCategoryCategoryDynamicDataMergeTypeGetColumn {
+  type: 'speicalMergeItem:getColumn',
+  params: {
+    modelId: number,
+    mainBodyColumnId: number,
+  },
+}
+
+export type IHomeCommonCategoryCategoryDynamicDataMerg = 
+  | IHomeCommonCategoryCategoryDynamicDataMergeTypeGetColumns 
+  | IHomeCommonCategoryCategoryDynamicDataMergeTypeGetColumn;
+
+export async function doLoadDynamicCategoryDataMergeTypeGetColumns(
+  categorys: IHomeCommonCategoryListTabNestCategoryItemDefine[],
+) { 
+  for (let i = categorys.length - 1; i >= 0; i--) {
+    const category = categorys[i];
+    if (category.type?.startsWith('speicalMergeItem:')) {
+      const d = category.type.split(':');
+      const op = d[1];
+      const type = d[2];
+      switch (op) {
+        case 'getColumns': {
+          const data = category as unknown as IHomeCommonCategoryCategoryDynamicDataMergeTypeGetColumns;
+          const result = await CommonContent.getModelColumList(
+            data.params.modelId,
+            data.params.mainBodyColumnId
+          )
+          const newArray = result.map((item) => ({
+            ...category,
+            params: undefined,
+            text: item.name as string,
+            type,
+            data: {
+              type: 'commonContent',
+              params: {
+                modelId: item.modelId,
+                mainBodyColumnId: item.id,
+              },
+            } as IHomeCommonCategoryDynamicDataCommonContent,
+          }));
+          categorys.splice(i, 1, ...newArray);
+          break;
+        }
+        case 'getColumn': {
+          const data = category as unknown as IHomeCommonCategoryCategoryDynamicDataMergeTypeGetColumn;
+          const result = await CommonContent.getModelColumList(
+            data.params.modelId,
+          )
+          categorys.splice(i, 1, ...result
+            .filter((item) => item.id === data.params.mainBodyColumnId)
+            .map((item) => ({
+              ...category,
+              text: item.name as string,
+              type,
+              data: {
+                type: 'commonContent',
+                params: {
+                  modelId: item.modelId,
+                  mainBodyColumnId: item.id,
+                },
+              } as IHomeCommonCategoryDynamicDataCommonContent,
+            })));
+          break;
+        }
+      }
+    }
+  }
+  return categorys;
+}

+ 65 - 0
src/pages/article/data/data-defines/Dropdown.ts

@@ -0,0 +1,65 @@
+// 下拉列表动态数据接口定义
+
+import CommonContent from "@/api/CommonContent";
+import type { IHomeCommonCategoryListTabListDropdownDefine } from "../CommonCategoryDefine";
+
+export function CommonCategorDynamicDropDownValuesToParams(dropDownValues: (number|string|boolean)[], dropdownDefines: IHomeCommonCategoryListTabListDropdownDefine[]) {
+  const params: Record<string, any> = {};
+  dropdownDefines.forEach((define, index) => {
+    const v = dropDownValues[index]
+    if (typeof v === 'number' && v > 0)
+      params[define.key] = v;
+    else if (typeof v === 'string')
+      params[define.key] = v;
+  });
+  return params;
+}
+
+export interface IHomeCommonCategoryDropdownDynamicDataCommonContent {
+  type: 'commonContent',
+  url?: string,
+  typeId: number, // 分类类型id
+  otherParams?: Record<string, any>,
+  idKey?: string,
+  nameKey?: string,
+}
+export interface IHomeCommonCategoryDropdownStaticData {
+  type: 'static',
+  data: Record<string, any>[],
+  idKey?: string,
+  nameKey?: string,
+}
+export interface IHomeCommonCategoryDropdownDynamicDataRequest {
+  type: 'request',
+  method: "OPTIONS" | "GET" | "HEAD" | "POST" | "PUT" | "DELETE",
+  url: string,
+  querys?: Record<string, any>,
+  params?: Record<string, any>,
+  idKey?: string,
+  nameKey?: string,
+}
+export type IHomeCommonCategoryDropdownDynamicData = 
+  IHomeCommonCategoryDropdownDynamicDataCommonContent 
+  | IHomeCommonCategoryDropdownDynamicDataRequest
+  | IHomeCommonCategoryDropdownStaticData;
+
+  export async function doLoadDynamicDropdownData(item: IHomeCommonCategoryDropdownDynamicData) {
+    switch (item.type) {
+      case 'commonContent':
+        return (await CommonContent.getCategoryList(item.typeId)) as unknown as Record<string, any>[];
+      case 'static':
+        return item.data;
+      case 'request':
+        return (await CommonContent.request(
+          item.url, 
+          { ...item.querys }, 
+          {
+            method: item.method, 
+            data: item.params,
+          },
+          '',
+          undefined,
+        )).data as unknown as Record<string, any>[];
+    }
+    throw new Error(`未实现的动态数据接口`);
+  }

+ 0 - 1
src/pages/blocks/MapBlock.vue

@@ -82,7 +82,6 @@ const mapLoader = useSimpleDataLoader(async () => {
     }),
     padding: [20, 20, 20, 20],
   });
-  console.log(res);
   return res;
 }, true, undefined, true);
 

+ 2 - 1
src/pages/blocks/StatsBlock.vue

@@ -145,7 +145,8 @@ const statsLoader = useSimpleDataLoader(async () => {
         title: item.title,
         value: item.total,
         titleSuffix: '处',
-        onClick: () => navTo('/pages/inhert/artifact/list', {
+        onClick: () => navTo('/pages/article/data/list', {
+          pageConfigName: 'artifact',
           level: item.level
         }),
       }

+ 1 - 1
src/pages/inhert/language/list.vue

@@ -101,7 +101,7 @@ const listLoader = useSimplePageListLoader(8, async (page, pageSize) => {
   const res = await CommonContent.getContentList(new GetContentListParams()
     .setModelId(5)
     .setKeywords(searchValue.value)
-    .setMainBodyColumnId(tabs.content.value?.[tab.value].id ?? 0)
+    .setMainBodyColumnId(tabs.content.value?.[tab.value]?.id ?? 0)
   , page, pageSize);
   return { list: res.list.map((item) => {
     return {