Selaa lähdekoodia

⚙️ 动态核心调整,支持打开公众号位置

快乐的梦鱼 1 kuukausi sitten
vanhempi
commit
0419de5119

+ 31 - 0
src/api/fusion/VillageContent.ts

@@ -0,0 +1,31 @@
+import type { NewDataModel } from '@imengyu/js-request-transform';
+import type { QueryParams } from '@imengyu/imengyu-utils';
+import { CommonContentApi, GetContentListParams } from '../CommonContent';
+import VillageApi from '../inhert/VillageApi';
+
+export class VillageContentApi extends CommonContentApi {
+
+  constructor() {
+    super(undefined, 18, "闽南节庆日历", 272);
+  }
+
+  async getContentList(params: GetContentListParams, page: number, pageSize?: number, modelClassCreator?: NewDataModel, querys?: QueryParams) {
+    const res = await this.getCategoryList(151);
+    const it2 = res.find(p => p.title == '省级');
+    if (it2) it2.title = '传统村落';
+    const list = page == 1 ? await VillageApi.getVallageList(it2?.id) : [];
+    const searchText = params.keywords;
+    list.filter((p) => !searchText || p.title.includes(searchText)).forEach((p) => {
+      p.desc = p.desc;
+      p.badge = p.district;
+      p.bottomTags = [
+        p.levelText, 
+        p.batchText,
+        p.historyLevelText,
+      ];
+    })
+    return { list: list as any, total: list.length }
+  }
+}
+
+export default new VillageContentApi();

+ 0 - 5
src/api/system/ConfigurationApi.ts

@@ -21,11 +21,6 @@ export const CommonConfigurationConfig = {
 export interface IConfigurationItem {
   baseServerUrl: string,
   articleMark: string,
-  articleBorrow: {
-    url: string,
-    text: string,
-    icon: string,
-  },
 }
 
 export class ConfigurationApi extends UpdateServerRequestModule<DataModel> {

+ 1 - 6
src/api/system/DefaultConfiguration.json

@@ -1,9 +1,4 @@
 {
   "baseServerUrl": "https://mnwh.wenlvti.net/api",
-  "articleMark": "以上内容摘自:",
-  "articleBorrow": {
-    "url": "https://mn.wenlvti.net/xmlib/opac/m/search?q={0}&curlibcode=&searchWay=title&hasholding=1",
-    "icon": "https://mn.wenlvti.net/app_static/minnan/images/inhert/IconBorrow.png",
-    "text": "点击查询借阅"
-  }
+  "articleMark": "以上内容摘自:"
 }

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

@@ -40,6 +40,16 @@ export function navCommonList(p: {
   }) 
 }
 
+
+export function navigateToAutoContent(dataItem: GetContentListItem, params: any) {
+  navTo(resolveCommonContentGetPageDetailUrlAuto(dataItem), {
+    id: dataItem.id,
+    mainBodyColumnId: dataItem.mainBodyColumnId,
+    modelId: dataItem.modelId,
+    ...params,
+  });
+}
+
 export function resolveCommonContentFormData(item: GetContentListItem[]) {
   item.forEach(it => {
     it.bottomTags = it.keywords?.length ? it.keywords as string[] : [ it.mainBodyColumnName ];

+ 14 - 1
src/pages/article/common/CommonListPage.ts

@@ -1,3 +1,5 @@
+import { doCallDynamicFunction } from "../data/CommonCategoryDynamicEvax";
+
 export type CommonListPageItemType = 'image-large-2'|'image-large'|'article-common'
   |'article-common-2'|'article-character'|'article-character-2'
   |'simple-text'|'simple-text-2'|'image-small'|'image-small-2';
@@ -13,4 +15,15 @@ export const CommonListPageItemTypeOptions = [
   { value: 'article-character-2', label: '人物文章2列 (article-character-2)' },
   { value: 'simple-text', label: '简单文本 (simple-text)' },
   { value: 'simple-text-2', label: '简单文本2列 (simple-text-2)' },
-];
+];
+
+export function doDynamicNavDetails(dynamic: string, content: any, parentData: any) {
+  return doCallDynamicFunction(dynamic, {
+    sourceData: {
+      main: content,
+      customData: {
+        parentData,
+      },
+    },
+  });
+}

+ 25 - 17
src/pages/article/common/CommonListPage.vue

@@ -164,8 +164,8 @@ import SimpleDropDownPicker, { type SimpleDropDownPickerItem } from '@/common/co
 import Tabs from '@/components/nav/Tabs.vue';
 import SearchBar from '@/components/form/SearchBar.vue';
 import Empty from '@/components/feedback/Empty.vue';
-import { resolveCommonContentGetPageDetailUrlAuto } from './CommonContent';
-import type { CommonListPageItemType } from './CommonListPage';
+import { navigateToAutoContent, resolveCommonContentGetPageDetailUrlAuto } from './CommonContent';
+import { doDynamicNavDetails, type CommonListPageItemType } from './CommonListPage';
 import Grid4Item from '@/pages/parts/Grid4Item.vue';
 import Box2LineRightShadow from '@/pages/parts/Box2LineRightShadow.vue';
 
@@ -384,27 +384,38 @@ function goDetails(item: any, id: number) {
     return;
   }
   if (props.detailsPage == 'byContent') {
-    if (handleByContent())
-      return;
+    handleByContent();
+    return;
+  }
+  if (typeof props.detailsPage === 'string' && props.detailsPage.startsWith('dynamic:')) {
+    doDynamicNavDetails(props.detailsPage.substring(8), item, {});
+    return;
   }
 
   function handleByContent() {
-    const page = props.detailsPageByContentCallback?.(item);
-    if (page) {
-      navTo(page, { 
+    if (props.detailsPageByContentCallback) {
+      const page = props.detailsPageByContentCallback?.(item);
+      if (page) {
+        navTo(page, { 
+          ...props.detailsParams, 
+          id 
+        })
+      }
+    } else {
+      navigateToAutoContent(item, { 
         ...props.detailsParams, 
         id 
       })
-      return true; 
     }
-    return false;
   }
 
   const page = typeof props.detailsPage === 'object' ? props.detailsPage[tabCurrentId.value] : undefined;
   if (page) {
     if (typeof page === 'string') {
-      if (page == 'byContent' && handleByContent())
+      if (page == 'byContent') {
+        handleByContent();
         return;
+      }
       navTo(page, { 
         ...props.detailsParams, 
         id 
@@ -416,8 +427,10 @@ function goDetails(item: any, id: number) {
         page: string,
         params: Record<string, any>,
       };
-      if (item.page == 'byContent' && handleByContent())
+      if (item.page == 'byContent') {
+        handleByContent();
         return;
+      }
       navTo(item.page, { 
         ...item.params, 
         ...props.detailsParams, 
@@ -426,12 +439,7 @@ function goDetails(item: any, id: number) {
       return; 
     }
   } else if (typeof props.detailsPage === 'object') {
-    if (handleByContent())
-      return;
-    navTo('/pages/article/details', { 
-      ...props.detailsParams,
-      id
-    })
+    handleByContent();
     return;
   }
   navTo(props.detailsPage as string, { 

+ 2 - 1
src/pages/article/data/CommonCategoryBlocks.ts

@@ -7,7 +7,7 @@ export interface CategoryDefine extends Omit<IHomeCommonCategoryListTabNestCateg
   content: IHomeCommonCategoryDynamicData|IHomeCommonCategoryDynamicDataDetailContent|null;
   type?: 'article'|'large-image2'|'horizontal-large'|'large-image'|'large-grid2'|'small-grid'|'small-grid2'|'simple-article'|'simple-text'
     |'default'
-    |'CalendarBlock'|'StatsBlock'|'MapBlock'|'AudioBlock'
+    |'CalendarBlock'|'StatsBlock'|'MapBlock'|'AudioBlock'|'RichBlock'
     |undefined;
 }
 
@@ -29,4 +29,5 @@ export const CommonCategoryBlockType : {
   { type: 'StatsBlock', label: '预制块:统计块' },
   { type: 'MapBlock', label: '预制块:地图块' },
   { type: 'AudioBlock', label: '预制块:音频块' },
+  { type: 'RichBlock', label: '预制块:富文本块' },
 ];

+ 16 - 4
src/pages/article/data/CommonCategoryBlocks.vue

@@ -24,6 +24,9 @@
       <template v-else-if="category.type === 'AudioBlock'">
         <AudioBlock v-bind="category.blockProps" />
       </template>
+      <template v-else-if="category.type === 'RichBlock'">
+        <RichBlock v-bind="category.blockProps" />
+      </template>
       <!--通用列表-->
       <SimplePageContentLoader v-else-if="category.data" :loader="category.data" >
         <FlexCol>
@@ -192,7 +195,7 @@
 <script setup lang="ts">;
 import { computed, onMounted, watch, type PropType } from 'vue';
 import CommonContent, { GetContentListItem, GetContentListParams } from '@/api/CommonContent';
-import { navCommonDetail, navCommonList, resolveCommonContentGetPageDetailUrlAuto, resolveCommonContentSolveProps } from '../common/CommonContent';
+import { navCommonDetail, navCommonList, navigateToAutoContent, resolveCommonContentGetPageDetailUrlAuto, resolveCommonContentSolveProps } from '../common/CommonContent';
 import { useSimpleDataLoader } from '@/common/composeabe/SimpleDataLoader';
 import { navTo } from '@/components/utils/PageAction';
 import HomeTitle from '@/pages/parts/HomeTitle.vue';
@@ -207,11 +210,14 @@ import StatsBlock from '@/pages/blocks/StatsBlock.vue';
 import MapCategoryBlock from '@/pages/blocks/MapBlock.vue';
 import Image from '@/components/basic/Image.vue';
 import AppCofig from '@/common/config/AppCofig';
+import Grid4Item from '@/pages/parts/Grid4Item.vue';
 import type { CategoryDefine } from './CommonCategoryBlocks';
 import { CommonCategoryDynamicDataSerializedApi, doGetDynamicListDataParams, type IHomeCommonCategoryDynamicDataDetailContent } from './CommonCategoryDynamicData';
 import AudioBlock from '@/pages/blocks/AudioBlock.vue';
 import { toast } from '@/components/utils/DialogAction';
-import Grid4Item from '@/pages/parts/Grid4Item.vue';
+import { doDynamicNavDetails } from '../common/CommonListPage';
+import { doCallDynamicFunction } from './CommonCategoryDynamicEvax';
+import RichBlock from '@/pages/blocks/RichBlock.vue';
 
 const props = defineProps({
   /**
@@ -236,7 +242,9 @@ const categoryDatas = computed(() => props.categoryDefine.map(item => {
       ...item,
       detailsPage: () => {},
       morePage: () => {
-        if (item.morePage)
+        if (item.morePage?.startsWith('dynamic:'))
+          doDynamicNavDetails(item.morePage.substring(8), {}, props.parentData);
+        else if (item.morePage)
           navTo(item.morePage, {});
       },
       data: null,
@@ -268,7 +276,11 @@ const categoryDatas = computed(() => props.categoryDefine.map(item => {
         if (item.detailsPage === 'disabled')
           return;
         else if (item.detailsPage === 'byContent')
-          navTo(resolveCommonContentGetPageDetailUrlAuto(dataItem), params);
+          navigateToAutoContent(dataItem, params);
+        else if (typeof item.detailsPage === 'string' && item.detailsPage.startsWith('dynamic:')) {
+          doDynamicNavDetails(item.detailsPage.substring(8), dataItem, props.parentData);
+          return;
+        }
         else
           navTo(item.detailsPage, params);
       } else {

+ 3 - 2
src/pages/article/data/CommonCategoryDefine.ts

@@ -1,4 +1,4 @@
-import type { IHomeCommonCategoryDetailDefine } from "./defines/Details";
+import type { IHomeCommonCategoryDetailDefine, IHomeCommonArticleDetailDefine } from "./defines/Details";
 import type { IHomeCommonCategoryHomeDefine } from "./defines/Home";
 import type { IHomeCommonCategoryListDefine } from "./defines/List";
 
@@ -27,7 +27,8 @@ export interface IHomeCommonCategoryDefine {
      */
     content: IHomeCommonCategoryListDefine
       |IHomeCommonCategoryHomeDefine
-      |IHomeCommonCategoryDetailDefine,
+      |IHomeCommonCategoryDetailDefine
+      |IHomeCommonArticleDetailDefine,
   }[],
 }
 

+ 12 - 1
src/pages/article/data/CommonCategoryDetail.vue

@@ -136,7 +136,7 @@ import { computed, onMounted, ref, watch } from 'vue';
 import { navTo } from '@/components/utils/PageAction';
 import { injectAppConfiguration } from '@/api/system/useAppConfiguration';
 import { injectCommonCategory } from './CommonCategoryGlobalLoader';
-import { doLoadDynamicCategoryDataMergeTypeGetColumns, doSerializeInternalVar } from './CommonCategoryDynamicData';
+import { doLoadDynamicCategoryDataMergeTypeGetColumns, doLoadDynamicDetailData, doSerializeInternalVar, type IHomeCommonCategoryDynamicData } from './CommonCategoryDynamicData';
 import { formatError, StringUtils, waitTimeOut } from '@imengyu/imengyu-utils';
 import { getIsDevtoolsPlatform } from '@/common/utils/MpVersions';
 import type { IHomeCommonCategoryDefine, IHomeCommonCategoryListTabListDataSolve, IHomeCommonCategoryListTabNestCategoryItemDefine } from './CommonCategoryDefine';
@@ -182,6 +182,10 @@ export interface CommonCategoryDetailProps extends DetailTabPageProps {
    */
   showDeadBox?: boolean;
   /**
+   * 自定义数据接口
+   */
+  data?: IHomeCommonCategoryDynamicData;
+  /**
    * 内容处理
    */
   dataSolve?: IHomeCommonCategoryListTabListDataSolve[];
@@ -449,6 +453,13 @@ async function loadTabVisible(tabs: IHomeCommonCategoryDetailTabItemDefine[], d:
 async function load(id: number) {
   if (isNaN(id) || id <= 0)
     throw new Error("请输入ID。如果正在测试,可在后台复制ID");
+  if (currentCommonCategoryContentDefine.value?.props.data) {
+    return await doLoadDynamicDetailData(
+      currentCommonCategoryContentDefine.value?.props.data,
+      id,
+      props.pageQuerys.modelId && Number(props.pageQuerys.modelId) > 0 ? Number(props.pageQuerys.modelId) : undefined
+    );
+  }
   let res = await CommonContent.getContentDetail(
     id, 
     undefined, 

+ 45 - 0
src/pages/article/data/CommonCategoryDynamicData.ts

@@ -28,6 +28,7 @@ import DiscussContent from "@/api/research/DiscussContent";
 import ResultContent from "@/api/research/ResultContent";
 import { CommonCategorDynamicDropDownValuesToParams } from "./data-defines/Dropdown";
 import NewsIndexContent from "@/api/news/NewsIndexContent";
+import VillageContent from "@/api/fusion/VillageContent";
 
 export * from './data-defines/Category';
 export * from './data-defines/Dropdown';
@@ -153,6 +154,7 @@ export const SerializedApiMap : Record<string, CommonContentApi> = {
   DiscussContent,
   ResultContent,
   NewsIndexContent,
+  VillageContent,
 };
 
 /**
@@ -233,6 +235,49 @@ export async function doLoadDynamicListData(
       )).data;
   }
 }
+export async function doLoadDynamicDetailData(
+  item: IHomeCommonCategoryDynamicData,
+  id?: number,
+  modelId?: number,
+  parentData?: any,
+) {
+  switch (item.type) {
+    default:
+      throw new Error(`未实现的动态数据接口`);
+    case 'commonContent':
+      return await CommonContent.getContentDetail(id || 0, undefined, modelId || undefined, item.otherParams);
+    case 'detailContent': {
+      return await CommonContent.getContentDetail(item.params.id, undefined, item.params.modelId || undefined, item.otherParams);
+    }
+    case 'parentKey': {
+      if (!parentData)
+        throw new Error(`此处不允许加载父级数据`);
+      return parentData?.[item.key];
+    }
+    case 'serializedApi': {
+      return (await CommonCategoryDynamicDataSerializedApi(item).getContentDetail(id || 0, undefined, modelId || undefined, item.otherParams));
+    }
+    case 'request':
+      return (await CommonContent.request(
+        item.url, 
+        { 
+          id, 
+          modelId,
+          ...item.querys, 
+        }, 
+        {
+          method: item.method, 
+          data: {
+            id,
+            modelId,
+            ...item.otherParams,
+          },
+        },
+        '',
+        undefined,
+      )).data;
+  }
+}
 export function doGetDynamicListDataParams(data: IHomeCommonCategoryDynamicData) {
   let modelId = 0;
   let mainBodyColumnId : string|number|number[]|undefined = undefined;

+ 199 - 7
src/pages/article/data/CommonCategoryDynamicEvax.ts

@@ -1,6 +1,8 @@
 import { DataObjectUtils } from "@imengyu/js-request-transform";
 import { doSerializeInternalVar } from "./CommonCategoryDynamicData";
 import { DateUtils } from "@imengyu/imengyu-utils";
+import { back, navTo, redirectTo } from "@/components/utils/PageAction";
+import { navigateToAutoContent } from "../common/CommonContent";
 
 export interface IDynamicCompareExpressionContext {
   sourceData: {
@@ -43,15 +45,15 @@ function parseMapExpression(mapExpression: string) {
  * @returns 
  */
 export function doEvaluateDynamicDataExpression(expressions: string, context: IDynamicCompareExpressionContext) {
-  const expressionArr = expressions.split('/');
+  if (!expressions)
+    return undefined;
+  const expressionArr = expressions.replace(/\n/g, '/').split('/');
   const tempData = new Map<string, any>();
   
   function evaluateExpression(expression: string, prevResult: any) : any {
     const arr = expression.split(','); 
-    if (arr.length < 2)
-      return undefined;
     const type = arr[0];
-    const key = arr[1];
+    const key = arr[1] || '';
 
     function accessDynamicKeyOrPrevResult(key: string, write: boolean, setValue?: any) {
       //基础取值
@@ -245,6 +247,8 @@ export function doEvaluateDynamicDataExpression(expressions: string, context: ID
  * 评估单条动态比较表达式(格式: 取值:符号:值)
  */
 function evaluateSingleCompareExpression(expression: string, context: IDynamicCompareExpressionContext): boolean {
+  if (!expression)
+    return false;
   const arr = expression.split(':');
   if (arr.length !== 3)
     return false;
@@ -282,7 +286,6 @@ function evaluateSingleCompareExpression(expression: string, context: IDynamicCo
     }
   }
 }
-
 /**
  * 评估动态比较表达式
  *
@@ -296,11 +299,200 @@ function evaluateSingleCompareExpression(expression: string, context: IDynamicCo
  */
 export function doEvaluateDynamicCompareExpression(expression: string, context: IDynamicCompareExpressionContext): boolean {
   const trimmed = expression.trim();
-  if (!trimmed) return false;
-
+  if (!trimmed) 
+    return false;
   const orParts = trimmed.split(/\s*\|\|\s*/).map((s) => s.trim()).filter(Boolean);
   return orParts.some((orPart) => {
     const andParts = orPart.split(/\s*&&\s*/).map((s) => s.trim()).filter(Boolean);
     return andParts.every((andPart) => evaluateSingleCompareExpression(andPart, context));
   });
+}
+
+/**
+ * 调用动态函数
+ * 
+ * 格式:
+ * 函数名:参数1:参数2:参数3
+ * 
+ * 前缀# 表示行号,用于跳转
+ * 例如:
+ * if:A,K#main.externalLink:jumpMp:default/
+ * #jumpMp#openOfficialAccountArticle:A,K#main.externalLink/
+ * #default#navigateToAutoContent:K#main
+ * 
+ * 多个函数用 / 分隔,会按顺序依次调用,并返回最后一个函数的返回值
+ * 
+ * @param functions 
+ * @param context 
+ */
+export async function doCallDynamicFunction(functions: string, context: IDynamicCompareExpressionContext) {
+  const functionArr = functions.replace(/\n/g, '/').split('/');
+  let result = null;
+  let currentPointer = 0;
+  let breakFlag = false;
+
+  //记录行号映射
+  //格式#name#expression
+  let lineMap = new Map<string, number>();
+  for (let i = 0; i < functionArr.length; i++) {
+    if (functionArr[i].startsWith('#')) {
+      const endIndex = functionArr[i].substring(1).indexOf('#');
+      lineMap.set(functionArr[i].substring(1, endIndex + 1), i);
+      functionArr[i] = functionArr[i].substring(endIndex + 2);
+    }
+  }
+  //执行函数
+  for (; currentPointer < functionArr.length; currentPointer++) {
+    result = await evaluateExpression(functionArr[currentPointer], result);
+    if (breakFlag)
+      break;
+  }
+  function functionJump(line: string) {
+    const lineNumber = !Number.isNaN(Number(line)) ? Number(line) : lineMap.get(line);
+    if (!lineNumber || lineNumber < currentPointer)
+      throw new Error('doCallDynamicFunction: bad line number: ' + line);
+    currentPointer = lineNumber - 1;
+  }
+  async function evaluateExpression(expression: string, prevResult: any) : Promise<any> {
+    const arr = expression.split(':');
+    const functionName = arr[0];
+    const params = arr.slice(1);
+    function assertParamCount(count: number) {
+      if (params.length !== count)
+        throw new Error('doCallDynamicFunction: invalid param count: ' + functionName);
+    }
+    switch (functionName) {
+      /**
+       * 条件判断
+       * 格式: if:判断:true的跳转行号:false的跳转行号
+       */
+      case 'if': {
+        assertParamCount(3);
+        let returnNewLineName = '';
+        const result = params[0].includes(':') ? 
+          doEvaluateDynamicCompareExpression(params[0], context) : 
+          doEvaluateDynamicDataExpression(params[0], context);
+        if (result)
+          returnNewLineName = doEvaluateDynamicDataExpression(params[1], context);
+        else
+          returnNewLineName = doEvaluateDynamicDataExpression(params[2], context);
+        functionJump(returnNewLineName);
+        break;
+      }
+      /**
+       * 分支
+       * 格式: switch:判断:值:跳转:值:跳转...:默认跳转
+       */
+      case 'switch': {
+        assertParamCount(2);
+        const values = [] as any[];
+        for (let i = 1; i < params.length; i += 2)
+          values.push(doEvaluateDynamicDataExpression(params[i], context));
+        const valueMatch = doEvaluateDynamicDataExpression(params[1], context);
+        const index = values.indexOf(valueMatch);
+        if (index !== -1)
+          functionJump(params[index * 2 + 2]);
+        else if (params.length > 2 && params.length % 2 === 0)
+          functionJump(params[params.length - 1]);
+        break;
+      }
+      /**
+       * 跳转
+       * 格式: jump:跳转行号
+       */
+      case 'jump': {
+        assertParamCount(1);
+        functionJump(doEvaluateDynamicDataExpression(params[0], context));
+        break;
+      }
+      /**
+       * 终止当前脚本
+       * 格式: break
+       */
+      case 'break':
+        breakFlag = true;
+        break;
+      /**
+       * 返回值
+       * 格式: return:值
+       */
+      case 'return':
+        return doEvaluateDynamicDataExpression(params[0], context);
+      case 'get':
+        assertParamCount(1);
+        return context.sourceData.main[doEvaluateDynamicDataExpression(params[0], context)];
+      case 'set':
+        assertParamCount(2);
+        context.sourceData.main[doEvaluateDynamicDataExpression(params[0], context)] = 
+          doEvaluateDynamicDataExpression(params[1], context);
+        break;
+      case 'navTo': 
+        assertParamCount(2);
+        navTo(doEvaluateDynamicDataExpression(params[0], context), doEvaluateDynamicDataExpression(params[1], context));
+        break;
+      case 'redirectTo': 
+        assertParamCount(2);
+        redirectTo(doEvaluateDynamicDataExpression(params[0], context), doEvaluateDynamicDataExpression(params[1], context));
+        break;
+      case 'back':
+        back();
+        break;
+      case 'openOfficialAccountChat':
+        assertParamCount(1);
+        return new Promise((resolve, reject) => {
+          (uni as any).openOfficialAccountChat({
+            username: doEvaluateDynamicDataExpression(params[0], context),
+            success: (res: any) => {
+              console.log('doCallDynamicFunction: openOfficialAccountChat success: ' + JSON.stringify(res));
+              resolve(res);
+            },
+            fail: (err: any) => {
+              console.error('doCallDynamicFunction: openOfficialAccountArticle failed: ' + err);
+              reject(err);
+            },
+          });
+        });
+      case 'openOfficialAccountArticle':
+        assertParamCount(1);
+        return new Promise((resolve, reject) => {
+          (uni as any).openOfficialAccountArticle({
+            url: doEvaluateDynamicDataExpression(params[0], context),
+            success: (res: any) => {
+              console.log('doCallDynamicFunction: openOfficialAccountArticle success: ' + JSON.stringify(res));
+              resolve(res);
+            },
+            fail: (err: any) => {
+              console.error('doCallDynamicFunction: openOfficialAccountArticle failed: ' + err);
+              reject(err);
+            },
+          });
+        });
+      case 'navigateToMiniProgram':
+        assertParamCount(2);
+        return new Promise((resolve, reject) => {
+          uni.navigateToMiniProgram({
+            appId: doEvaluateDynamicDataExpression(params[0], context),
+            path: doEvaluateDynamicDataExpression(params[1], context),
+            envVersion: doEvaluateDynamicDataExpression(params[2], context),
+            success: (res: any) => resolve(res),
+            fail: (err: any) => {
+              console.error('doCallDynamicFunction: navigateToMiniProgram failed: ' + err);
+              reject(err);
+            },
+          });
+        });
+      case 'navigateToAutoContent':
+        assertParamCount(1);
+        navigateToAutoContent(
+          doEvaluateDynamicDataExpression(params[0], context), 
+          doEvaluateDynamicDataExpression(params[1], context)
+        );
+        break;
+      default:
+        throw new Error('doCallDynamicFunction: unknown function: ' + functionName);
+    }
+    return prevResult;
+  }
+
+  return result;
 }

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

@@ -26,10 +26,10 @@ export function useCommonCategoryGlobalLoader() {
     uni.showLoading({ title: '加载中' });
     try {
       //本地开发时,使用默认配置
-      /* if (getIsDevtoolsPlatform()) {
+      if (getIsDevtoolsPlatform()) {
         commonCategoryData.value = DefaultCofig as IHomeCommonCategoryDefine;
         return;
-      } */
+      }
       //根据环境版本,使用正式配置或体验配置
       const category = (await CommonCategoryApi.getConfig(
         // #ifdef MP-WEIXIN

+ 24 - 9
src/pages/article/data/DefaultCategory.json

@@ -12,7 +12,7 @@
           "homeButtons": [
             {
               "title": "常识一点通",
-              "icon": "https://mn.wenlvti.net/uploads/20260205/ccc542ae733ba89a887541839241d533.png",
+              "icon": "https://mnwh.wenlvti.net/uploads/20260205/ccc542ae733ba89a887541839241d533.png",
               "size": 50,
               "link": "/pages/article/data/list?pageConfigName=explore",
               "style": "large-bg",
@@ -20,7 +20,7 @@
             },
             {
               "title": "闽南新鲜事",
-              "icon": "https://mn.wenlvti.net/uploads/20260205/c1bfe5a093217f00f775af0b6ff60398.png",
+              "icon": "https://mnwh.wenlvti.net/uploads/20260205/c1bfe5a093217f00f775af0b6ff60398.png",
               "size": 50,
               "link": "/pages/article/data/list?pageConfigName=news",
               "style": "large-bg",
@@ -36,7 +36,7 @@
             },
             {
               "title": "文化新视角",
-              "icon": "https://mn.wenlvti.net/uploads/20260206/b96b9353f7a458fee33c3b1028921607.png",
+              "icon": "https://mnwh.wenlvti.net/uploads/20260206/b96b9353f7a458fee33c3b1028921607.png",
               "size": 50,
               "link": "/pages/article/data/list?pageConfigName=research",
               "style": "large-bg",
@@ -44,7 +44,7 @@
             },
             {
               "title": "世界走透透",
-              "icon": "https://mn.wenlvti.net/uploads/20260205/28273ceae512f8f5f9a615acead54d4e.png",
+              "icon": "https://mnwh.wenlvti.net/uploads/20260205/28273ceae512f8f5f9a615acead54d4e.png",
               "size": 50,
               "link": "/pages/article/data/list?pageConfigName=communicate",
               "style": "large-bg",
@@ -52,7 +52,7 @@
             },
             {
               "title": "来厦门逛逛",
-              "icon": "https://mn.wenlvti.net/uploads/20260205/69c0b025d566811dc275fb76198a619c.png",
+              "icon": "https://mnwh.wenlvti.net/uploads/20260205/69c0b025d566811dc275fb76198a619c.png",
               "size": 50,
               "link": "/pages/article/data/list?pageConfigName=travel",
               "style": "large-bg",
@@ -131,7 +131,7 @@
                     "icon": "icon-place",
                     "data": {
                       "type": "serializedApi",
-                      "name": "ProjectsContent"
+                      "name": "VillageContent"
                     }
                   },
                   {
@@ -139,7 +139,7 @@
                     "icon": "icon-task-environment-3",
                     "data": {
                       "type": "serializedApi",
-                      "name": "ProjectsContent"
+                      "name": "ScenicSpotContent"
                     }
                   }
                 ]
@@ -161,7 +161,7 @@
                 }
               },
               "showMore": false,
-              "detailsPage": "/pages/article/details?pageConfigName=intangible-details",
+              "detailsPage": "dynamic:if:A,K#main.externalLink:jumpMp:default/#jumpMp#openOfficialAccountArticle:A,K#main.externalLink/break/#default#navigateToAutoContent:K#main",
               "dataSolve": [
                 "common"
               ],
@@ -1319,7 +1319,8 @@
               ]
             }
           ],
-          "itemType": "article-common"
+          "itemType": "article-common",
+          "detailsPage": "dynamic:if:A,K#main.externalLink:jumpMp:default/#jumpMp#openOfficialAccountArticle:A,K#main.externalLink/break/#default#navigateToAutoContent:K#main"
         }
       }
     },
@@ -2028,6 +2029,20 @@
       }
     },
     {
+      "name": "common-details",
+      "title": "通用详情",
+      "content": {
+        "type": "CommonDetails",
+        "props": {
+          "articleBorrow": {
+            "url": "https://mn.wenlvti.net/xmlib/opac/m/search?q={0}&curlibcode=&searchWay=title&hasholding=1",
+            "icon": "https://mn.wenlvti.net/app_static/minnan/images/inhert/IconBorrow.png",
+            "text": "点击查询借阅"
+          }
+        }
+      }
+    },
+    {
       "name": "best-drama",
       "title": "精品剧目",
       "content": {

+ 12 - 1
src/pages/article/data/defines/Details.ts

@@ -3,11 +3,12 @@
  * 详情页定义
  */
 
+import type { CommonArticleDetailProps } from "../../details.vue";
 import type { CommonCategoryDetailProps } from "../CommonCategoryDetail.vue";
 import type { IHomeCommonCategoryListDefine, IHomeCommonCategoryListTabNestCategoryItemDefine } from "./List";
 
 /**
- * 页面模板:页定义
+ * 页面模板:详情页定义
  */
 export interface IHomeCommonCategoryDetailDefine {
   type: 'Details',
@@ -29,6 +30,16 @@ export interface IHomeCommonCategoryDetailDefine {
 
 
 /**
+ * 页面模板:默认详情页定义
+ */
+export interface IHomeCommonArticleDetailDefine {
+  type: 'CommonDetails',
+  props: Omit<CommonArticleDetailProps, 'load'> & {
+    
+  },
+}
+
+/**
  * TAB定义 - 类型:嵌套子分类 - 子分类项定义
  */
 export type IHomeCommonCategoryDetailTabItemDefine = 

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

@@ -29,7 +29,7 @@
             <ArrowDownOutlined title="下移" @click.stop="emit('move-down', item)" />
             <CopyOutlined title="克隆页面" @click.stop="emit('duplicate', item)" />
             <a-popconfirm title="确认删除吗?" @confirm="emit('delete', item)">
-              <a-button type="text" danger size="small" @click.stop="">
+              <a-button type="text" danger size="small" :disabled="noDeletePageNames.includes(item.name)" @click.stop="">
                 <DeleteOutlined title="删除" />
               </a-button>
             </a-popconfirm>
@@ -49,6 +49,8 @@ defineProps<{
   selectedPage: (IHomeCommonCategoryDefine['page'][0]) | null;
 }>();
 
+const noDeletePageNames = ['Home', 'CommonDetails'];
+
 const emit = defineEmits<{
   (e: 'update:selectedPage', page: IHomeCommonCategoryDefine['page'][0]): void;
   (e: 'move-up', page: IHomeCommonCategoryDefine['page'][0]): void;

+ 131 - 14
src/pages/article/details.vue

@@ -4,7 +4,11 @@
       <StatusBarSpace backgroundColor="transparent" />
       <NavBar leftButton="back" :iconProps="{ color: 'white', innerStyle: { backgroundColor: 'rgba(0,0,0,0.2)' } }" textColor="white" />
     </FlexCol>
-    <SimplePageContentLoader :loader="loader">
+    <FlexCol v-if="errorMessage" :padding="30" :gap="30" center height="100%">
+      <Result status="error" :description="errorMessage" />
+      <Button type="primary" @click="loadPageConfig">重新加载</Button>
+    </FlexCol>
+    <SimplePageContentLoader v-else :loader="loader">
       <template v-if="loader.content.value">
         <view class="d-flex flex-col">
 
@@ -53,24 +57,27 @@
               class="w-100"
               titleColor="title-text"
               title2
-              :image="appConfiguration?.articleBorrow?.icon"
+              :image="currentCommonCategoryContentDefine?.props.articleBorrow?.icon"
               :title="loader.content.value.title"
-              :desc="appConfiguration?.articleBorrow?.text"
+              :desc="currentCommonCategoryContentDefine?.props.articleBorrow?.text"
               @click="goBorrow(loader.content.value.title)"
             />
           </view>
 
           <!-- 源网页 -->
-          <view v-if="loader.content.value.externalLink && querys.navToExternalLink !== 'none'" class="mt-3">
+          <view v-if="currentCommonCategoryContentDefine?.props.topButtons" class="mt-3">
+            <template v-for="button in currentCommonCategoryContentDefine?.props.topButtons">
             <Box2LineImageRightShadow
+              v-if="evaluateButtonVisible(button.visible)"
               class="w-100"
               titleColor="title-text"
               title2
               :image="archiveInfo.archiveIcon"
-              :title="loader.content.value.title"
-              desc="点击查看源网页"
-              @click="goExternalLink(loader.content.value.externalLink)"
+              :title="button.text"
+              :desc="loader.content.value.title"
+              @click="executeButtonAction(button.expression)"
             />
+          </template>
           </view>
 
           <!-- 内容 -->
@@ -117,19 +124,23 @@
 </template>
 
 <script setup lang="ts">
-import type { GetContentDetailItem } from "@/api/CommonContent";
 import { onShareTimeline, onShareAppMessage } from "@dcloudio/uni-app";
 import { DataDateUtils } from "@imengyu/js-request-transform";
 import { useSimplePageContentLoader } from "@/common/composeabe/SimplePageContentLoader";
 import { useLoadQuerys } from "@/common/composeabe/LoadQuerys";
-import { computed } from "vue";
-import { FormatUtils, StringUtils } from "@imengyu/imengyu-utils";
+import { computed, onMounted, ref } from "vue";
+import { FormatUtils, StringUtils, waitTimeOut } from "@imengyu/imengyu-utils";
 import { injectAppConfiguration } from "@/api/system/useAppConfiguration";
 import { useSimpleDataLoader } from "@/common/composeabe/SimpleDataLoader";
 import { navTo, redirectTo } from "@/components/utils/PageAction";
+import { resolveCommonContentFormData } from "./common/CommonContent";
+import { injectCommonCategory } from "./data/CommonCategoryGlobalLoader";
+import { getIsDevtoolsPlatform } from "@/common/utils/MpVersions";
+import { doCallDynamicFunction, doEvaluateDynamicCompareExpression } from "./data/CommonCategoryDynamicEvax";
+import type { GetContentDetailItem } from "@/api/CommonContent";
+import type { IHomeCommonArticleDetailDefine } from "./data/CommonCategoryDefine";
 import CommonContent, { GetContentListParams } from "@/api/CommonContent";
 import Box2LineImageRightShadow from "../parts/Box2LineImageRightShadow.vue";
-import AppCofig from "@/common/config/AppCofig";
 import LikeFooter from "../parts/LikeFooter.vue";
 import Image from "@/components/basic/Image.vue";
 import ArticleCorrect from "../parts/ArticleCorrect.vue";
@@ -148,8 +159,64 @@ import FlexCol from "@/components/layout/FlexCol.vue";
 import Footer from "@/components/display/Footer.vue";
 import NavBar from "@/components/nav/NavBar.vue";
 import StatusBarSpace from "@/components/layout/space/StatusBarSpace.vue";
-import { resolveCommonContentFormData } from "./common/CommonContent";
 
+export interface CommonArticleDetailProps {
+  /**
+   * 查询借阅功能按钮
+   */
+  articleBorrow: {
+    /**
+     * 查询借阅功能按钮链接
+     */
+    url: string,
+    /**
+     * 查询借阅功能按钮文本
+     */
+    text: string,
+    /**
+     * 查询借阅功能按钮图标
+     */
+    icon: string,
+  },
+  /**
+   * 顶部按钮配置
+   */
+  topButtons: {
+    /**
+     * 按钮可见性表达式
+     */
+    visible: string,
+    /**
+     * 按钮文本
+     */
+    text: string,
+    /**
+     * 按钮图标
+     */
+    icon: string,
+    /**
+     * 按钮动作表达式
+     */
+    expression: string,
+  }[],
+  /**
+   * TODO: 顶部显示信息配置
+   */
+  topShowInfos: {
+    /**
+     * 显示信息键
+     */
+    key: string,
+    /**
+     * 显示信息前缀
+     */
+    prefix: string,
+    /**
+     * 显示信息表达式
+     */
+    expression: string,
+  }[],
+}
 
 const { querys } = useLoadQuerys({ 
   id: 0,
@@ -180,6 +247,12 @@ const { querys } = useLoadQuerys({
    * @description 'auto' 自动跳转,'manual' 手动跳转,'none' 不跳转
    */
   navToExternalLink: 'auto',
+  /**
+   * 顶部显示信息
+   * @default 'all'
+   * @description 'all' 显示所有信息,'none' 不显示任何信息,多个信息用逗号分隔
+   */
+  topShowInfos: 'all',
 }, (t) => loader.loadData(t));
 
 const loader = useSimplePageContentLoader<
@@ -198,8 +271,11 @@ const loader = useSimplePageContentLoader<
   return res;
 });
 
+const currentCommonCategoryContentDefine = ref<IHomeCommonArticleDetailDefine>();
+const commonCategory = injectCommonCategory();
 const appConfiguration = injectAppConfiguration();
 
+const errorMessage = ref('');
 const emptyContent = computed(() => (loader.content.value?.content || '').trim() === '')
 const archiveInfo = computed(() => {
   const hasArchive = Boolean(loader.content.value?.archives);
@@ -244,6 +320,22 @@ const recommendListLoader = useSimpleDataLoader(async () => {
   return resolveCommonContentFormData(res);
 });
 
+function evaluateButtonVisible(expression: string) {
+  return doEvaluateDynamicCompareExpression(expression, {
+    sourceData: {
+      main: loader.content.value || {},
+      customData: {},
+    }
+  });
+}
+function executeButtonAction(expression: string) {
+  doCallDynamicFunction(expression, {
+    sourceData: {
+      main: loader.content.value || {},
+      customData: {},
+    }
+  });
+}
 function goExternalLink(url: string) {
   redirectTo('/pages/article/web/ewebview', { url });
 }
@@ -306,10 +398,9 @@ function goDetails(id: number) {
 }
 function goBorrow(title: string) {
   navTo('/pages/article/web/ewebview', { 
-    url: FormatUtils.formatString(appConfiguration.value?.articleBorrow?.url || '', { title })
+    url: FormatUtils.formatString(currentCommonCategoryContentDefine.value?.props.articleBorrow?.url || '', { title })
   });
 }
-
 function getPageShareData() {
   if (!loader.content.value)
     return { title: '文章详情', imageUrl: '' }
@@ -318,6 +409,32 @@ function getPageShareData() {
     imageUrl: loader.content.value.images[0],
   }
 }
+
+
+async function loadPageConfig() {
+  if (getIsDevtoolsPlatform()) {
+    await waitTimeOut(500);
+  }
+  const pageConfigName = 'common-details';
+  const currentCommonCategoryDefine = commonCategory.value.page
+    .find((item) => item.name === pageConfigName);
+  if (!currentCommonCategoryDefine) {
+    errorMessage.value = '未找到指定的分类配置:' + pageConfigName;
+    return;
+  }
+  if (currentCommonCategoryDefine.content.type !== 'CommonDetails') {
+    errorMessage.value = '分类配置:' + pageConfigName + ' 不是默认详情类型';
+    return;
+  }
+  currentCommonCategoryContentDefine.value = currentCommonCategoryDefine.content;
+  uni.setNavigationBarTitle({
+    title: currentCommonCategoryDefine.title || '',
+  })
+}
+
+onMounted(() => {
+  loadPageConfig();
+});
 onShareTimeline(() => {
   return getPageShareData(); 
 })

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

@@ -80,7 +80,7 @@ const mapLoader = useSimpleDataLoader(async () => {
         longitude: p.longitude,
       }
     }),
-    padding: [20, 20, 20, 20],
+    padding: [70, 40, 40, 40],
   });
   return res;
 }, true, undefined, true);

+ 32 - 0
src/pages/blocks/RichBlock.vue

@@ -0,0 +1,32 @@
+<template>
+  <view>
+    <Parse :content="content" />
+  </view>
+</template>
+
+<script setup lang="ts">
+import Parse from '@/components/display/parse/Parse.vue';
+import { onMounted, ref } from 'vue';
+import { doLoadDynamicDetailData, type IHomeCommonCategoryDynamicData } from '../article/data/CommonCategoryDynamicData';
+
+const props = defineProps({
+  content: {
+    type: null,
+    default: ''
+  },
+});
+
+const finalContent = ref('');
+
+async function loadContent() {
+  if (typeof props.content === 'string') {
+    finalContent.value = props.content;
+  } else if (typeof props.content === 'object' && props.content.content) {
+    finalContent.value = await doLoadDynamicDetailData(props.content.content as IHomeCommonCategoryDynamicData);
+  }
+}
+
+onMounted(() => {
+  loadContent();
+});
+</script>