Explorar o código

⚙️ 修改详情页核心

快乐的梦鱼 hai 1 mes
pai
achega
72be05be09

+ 2 - 0
src/api/CommonContent.ts

@@ -215,6 +215,7 @@ export class GetContentListItem extends DataModel<GetContentListItem> {
   thumbnail = '';
   desc = '!desc';
   content = '!content';
+  externalLink = '';
   type = 0;
   keywords ?: string[];
   flag ?: string[];
@@ -295,6 +296,7 @@ export class GetContentDetailItem extends DataModel<GetContentDetailItem> {
   modelName = '';
   mainBodyColumnId = 0;
   mainBodyColumnName = '';
+  externalLink = '';
   type = 0;
   title = '';
   region = 0;

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

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

+ 10 - 2
src/components/display/parse/Parse.vue

@@ -7,7 +7,7 @@
 <script setup lang="ts">
 import ParseNodeRender from './ParseNodeRender.vue'
 import { parse, type DefaultTreeAdapterTypes } from 'parse5'
-import { computed, provide, toRef } from 'vue';
+import { computed, provide, ref, toRef } from 'vue';
 import type { ParseNode } from './Parse'
 
 export interface ParseProps {
@@ -29,7 +29,10 @@ const props = withDefaults(defineProps<ParseProps>(), {
   tagStyle: () => ({})
 });
 
+const praseImages = ref<string[]>([]);
+
 provide('tagStyle', toRef(props, 'tagStyle'));
+provide('praseImages', praseImages);
 
 const toObj = (attrs: DefaultTreeAdapterTypes.Element['attrs']) => {
   const obj: Record<string, string> = {};
@@ -42,6 +45,7 @@ const toObj = (attrs: DefaultTreeAdapterTypes.Element['attrs']) => {
 const parseHtml = (html: string): ParseNode[] => {
   const nodes: ParseNode[] = [];
   const doc = parse(html);
+  praseImages.value = [];
   
   const traverse = (element: DefaultTreeAdapterTypes.Element): ParseNode => {
     const node: ParseNode = {
@@ -61,7 +65,11 @@ const parseHtml = (html: string): ParseNode[] => {
             }
           });
         } else if (child.nodeName !== '#comment' && child.nodeName !== '#documentType') {
-          node.children?.push(traverse(child as DefaultTreeAdapterTypes.Element));
+          const childNode = traverse(child as DefaultTreeAdapterTypes.Element);
+          node.children?.push(childNode);
+          if (childNode.tag === 'img') {
+            praseImages.value.push(childNode.attrs?.src as string);
+          }
         }
       }
     }

+ 11 - 3
src/components/display/parse/ParseNodeRender.vue

@@ -102,6 +102,7 @@ const props = withDefaults(defineProps<{
 });
 
 const tagStyle = inject<Ref<Record<string, string>>>('tagStyle', ref({}));
+const praseImages = inject<Ref<string[]>>('praseImages', ref([]));
 const builtInStyles = {
   // 标题标签
   'h1': 'font-size: 2em; font-weight: bold; margin: 0.67em 0;',
@@ -207,9 +208,16 @@ const linkTap = (e: any) => {
 
 function preview(url: string) {
   if (url) {
-    uni.previewImage({
-      urls: [url],
-    })
+    if (praseImages.value.includes(url)) {
+      uni.previewImage({
+        urls: praseImages.value,
+        current: praseImages.value.indexOf(url),
+      })
+    } else {
+      uni.previewImage({
+        urls: [url],
+      })
+    }
   }
 }
 

+ 5 - 11
src/pages/article/common/DetailTabPage.ts

@@ -1,18 +1,12 @@
 import type { GetContentDetailItem } from "@/api/CommonContent";
-import type { TabControlItem } from "@/common/composeabe/TabControl";
-import type { Ref } from "vue";
+import type { TabsItemData } from "@/components/nav/Tabs.vue";
 
-export interface DetailTabPageTabsArray {
-  tabsArray: Ref<TabControlItem[]>,
-  getTabById(id: number): TabControlItem | undefined;
+export interface DetailTabPageTabItem extends TabsItemData {
+  id: number
 }
-
 export interface DetailTabPageProps {
-  load?: (
-    id: number, 
-    tabsArray: DetailTabPageTabsArray
-  ) => Promise<GetContentDetailItem>;
-  extraTabs?: TabControlItem[];
+  load?: (id: number) => Promise<GetContentDetailItem>;
+  tabs?: DetailTabPageTabItem[];
   showHead?: boolean;
   showDeadBox?: boolean;
   overrideInternalTabsName?: undefined|{

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

@@ -30,7 +30,7 @@
           <!-- 内容切换标签 -->
           <view class="ml-2 mr-2">
             <Tabs
-              :tabs="tabs" 
+              :tabs="props.tabs" 
               v-model:currentIndex="tabCurrentIndex"
               :autoScroll="true"
               :autoItemWidth="false"
@@ -54,10 +54,9 @@
   </view>
 </template>
 <script setup lang="ts">
-import { computed } from "vue";
+import { computed, ref } from "vue";
 import { useSimplePageContentLoader } from "@/common/composeabe/SimplePageContentLoader";
 import { useLoadQuerys } from "@/common/composeabe/LoadQuerys";
-import { useTabControl } from "@/common/composeabe/TabControl";
 import SimplePageContentLoader from "@/common/components/SimplePageContentLoader.vue";
 import ImageSwiper from "@/pages/parts/ImageSwiper.vue";
 import ContentNote from "@/pages/parts/ContentNote.vue";
@@ -65,10 +64,10 @@ 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 type { DetailTabPageProps, DetailTabPageTabsArray } from "./DetailTabPage";
+import type { DetailTabPageProps } from "./DetailTabPage";
 
 const props = withDefaults(defineProps<DetailTabPageProps>(), {
-  extraTabs: () => [],
+  tabs: () => [],
   showHead: true,
   showDeadBox: false,
 })
@@ -78,6 +77,11 @@ const emit = defineEmits([
   "loaded"
 ])
 
+const tabCurrentIndex = ref(0);
+const tabCurrentId = computed(() => {
+  return props.tabs[tabCurrentIndex.value]?.id ?? -1;
+})
+
 const emptyContent = computed(() => {
   return !(loader.content.value?.intro as string || '').trim() && !(loader.content.value?.content || '').trim();
 })
@@ -91,7 +95,7 @@ const loader = useSimplePageContentLoader<
   if (!props.load)
     throw new Error("!props.load");
 
-  const d = await props.load(params.id, tabsArrayObject);
+  const d = await props.load(params.id);
 
   if (d.title)
     uni.setNavigationBarTitle({ title: d.title });
@@ -107,30 +111,12 @@ const loader = useSimplePageContentLoader<
   return d;
 });
 
-const { 
-  tabCurrentId,
-  tabCurrentIndex,
-  tabsArray,
-  tabs,
-} = useTabControl({
-  tabs: props.extraTabs,
-  onTabChange(a, b) {
-    emit("tabChange", a, b);
-  },
-})
-
-const tabsArrayObject : DetailTabPageTabsArray = {
-  tabsArray,
-  getTabById(id: number) {
-    return tabsArray.value.find(t => t.id == id);
-  }
-}
-
 useLoadQuerys({ id : 0 }, (p) => loader.loadData(p));
 
 defineExpose({
-  load(params: { id: number }) {
-    loader.loadData(params, true);
+  async load(params: { id: number }) {
+    await loader.loadData(params, true);
+    return loader.content.value;
   },
   getPageShareData() {
     const content = loader.content.value;

+ 133 - 37
src/pages/article/data/CommonCategoryDetail.vue

@@ -13,11 +13,11 @@
       :showHead="currentCommonCategoryContentDefine?.props.showHead"
       :showDeadBox="currentCommonCategoryContentDefine?.props.showDeadBox"
       :load="load"
-      :extraTabs="tabRenderDefinesArray"
+      :tabs="tabRenderDefinesArray"
       @loaded="onLoaded"
     >
       <template #extraTabs="{ content, tabCurrentId }">
-        <template v-if="tabRenderDefines[tabCurrentId].type === 'intro'">
+        <template v-if="tabRenderDefines[tabCurrentId]?.type === 'intro'">
           <!-- 简介 -->
           <Parse
             v-if="content.intro"
@@ -33,7 +33,7 @@
             {{ content.from }}
           </text>
         </template>
-        <template v-else-if="tabRenderDefines[tabCurrentId].type === 'images'">
+        <template v-else-if="tabRenderDefines[tabCurrentId]?.type === 'images'">
           <!-- 图片 -->
           <view v-if="tabRenderDefines[tabCurrentId].prefix" class="d-flex flex-row justify-center align-center mt-2 mb-2">
             <text class="size-s font-bold color-text-content">{{ tabRenderDefines[tabCurrentId].prefix }}</text>
@@ -45,7 +45,7 @@
             imageHeight="200rpx"
           />
         </template>
-        <template v-else-if="tabRenderDefines[tabCurrentId].type === 'video'">
+        <template v-else-if="tabRenderDefines[tabCurrentId]?.type === 'video'">
           <!-- 视频 -->
           <video
             v-if="content.video"
@@ -56,7 +56,7 @@
             controls
           />
         </template>
-        <template v-else-if="tabRenderDefines[tabCurrentId].type === 'audio'">
+        <template v-else-if="tabRenderDefines[tabCurrentId]?.type === 'audio'">
           <!-- 视频 -->
           <video
             v-if="content.audio"
@@ -67,7 +67,7 @@
             controls
           />
         </template>
-        <template v-else-if="tabRenderDefines[tabCurrentId].type === 'list'">
+        <template v-else-if="tabRenderDefines[tabCurrentId]?.type === 'list'">
           <!-- 列表 -->
           <CommonCategoryListBlock
             v-if="currentCommonCategoryDefine"
@@ -84,17 +84,17 @@
             @error="errorMessage = $event"
           />
         </template>
-        <template v-else-if="tabRenderDefines[tabCurrentId].type === 'rich'">
+        <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'">
+        <template v-else-if="tabRenderDefines[tabCurrentId]?.type === 'nestCategory'">
           <!-- 嵌套分类 -->
           <CommonCategoryBlocks :categoryDefine="tabRenderDefines[tabCurrentId].categoryDefine" />
         </template>
-        <template v-else-if="tabRenderDefines[tabCurrentId].key === 'vr'">
+        <template v-else-if="tabRenderDefines[tabCurrentId]?.key === 'vr'">
           <!-- VR参观 -->
           <view class="d-flex flex-row justify-center p-5">
             <Button @click="handleGoToVr(content.vr as string)">
@@ -138,9 +138,10 @@ import { injectAppConfiguration } from '@/api/system/useAppConfiguration';
 import { injectCommonCategory } from './CommonCategoryGlobalLoader';
 import { doLoadDynamicCategoryDataMergeTypeGetColumns } from './CommonCategoryDynamicData';
 import { formatError, StringUtils, waitTimeOut } from '@imengyu/imengyu-utils';
+import { getIsDevtoolsPlatform } from '@/common/utils/MpVersions';
 import type { IHomeCommonCategoryDefine, IHomeCommonCategoryListTabNestCategoryItemDefine } from './CommonCategoryDefine';
 import type { IHomeCommonCategoryDetailDefine, IHomeCommonCategoryDetailTabItemDefine } from './defines/Details';
-import type { DetailTabPageProps, DetailTabPageTabsArray } from '../common/DetailTabPage';
+import type { DetailTabPageProps } from '../common/DetailTabPage';
 import type { CategoryDefine } from './CommonCategoryBlocks';
 import LoadingPage from '@/components/display/loading/LoadingPage.vue';
 import FlexCol from '@/components/layout/FlexCol.vue';
@@ -152,7 +153,7 @@ import CommonCategoryDetailIntroBlocks from './CommonCategoryDetailIntroBlocks.v
 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 CommonContent, { GetContentDetailItem, GetContentListParams } from '@/api/CommonContent';
 import CommonCategoryDetailContentBlocks from './CommonCategoryDetailContentBlocks.vue';
 import ImageGrid from '@/pages/parts/ImageGrid.vue';
 import CommonCategoryListBlock from './CommonCategoryListBlock.vue';
@@ -207,7 +208,7 @@ const currentCommonCategoryDefine = ref<IHomeCommonCategoryDefine['page'][0]>();
 const currentCommonCategoryContentDefine = ref<IHomeCommonCategoryDetailDefine>();
 const commonCategory = injectCommonCategory();
 
-const tabDefines = computed(() => currentCommonCategoryContentDefine.value?.props.tabs || []);
+const tabDefines = ref<IHomeCommonCategoryDetailTabItemDefine[]>([]);
 const tabRenderDefines = computed(() => {
   const result = {} as Record<number, RenderTabDefine>;
   try {
@@ -242,10 +243,13 @@ const tabRenderDefines = computed(() => {
   return result;
 });
 const tabRenderDefinesArray = computed(() => {
-  return Object.values(tabRenderDefines.value);
+  return Object.values(tabRenderDefines.value) || [];
 });
 
 async function loadPageConfig() {
+  if (getIsDevtoolsPlatform()) {
+    await waitTimeOut(1000);
+  }
   if (!props.pageConfigName) {
     errorMessage.value = '配置有误';
     return;
@@ -266,12 +270,14 @@ async function loadPageConfig() {
     title: currentCommonCategoryDefine.value?.title || '',
   })
 
+  const tabs = currentCommonCategoryContentDefine.value?.props.tabs || [];
+
   await waitTimeOut(50);
   
   try {
     //特殊处理
     let hasNestCategory = false;
-    for (const [_, tab] of Object.entries(tabDefines.value)) {
+    for (const tab of tabs) {
       if (tab.type === 'nestCategory') {
         tab.categorys = await doLoadDynamicCategoryDataMergeTypeGetColumns(tab.categorys)
         hasNestCategory = true;
@@ -281,12 +287,63 @@ async function loadPageConfig() {
       await waitTimeOut(50);
     loadState.value = true;
     await waitTimeOut(50);
-    pageRef.value?.load(props.pageQuerys);
+    const content = (await pageRef.value?.load(props.pageQuerys)) as GetContentDetailItem;
+
+    //Tab标签动态处理
+    function loadByContentData(text: string) {
+      let result = text;
+      const keys = text.split(':');
+      if (keys.length >= 2) {
+        switch (keys[1]) {
+          case 'byContentType': 
+            switch(content.type) {
+              default:
+              case GetContentListParams.TYPE_ARTICLE: result = '相关文章'; break;
+              case GetContentListParams.TYPE_VIDEO: result = '视频'; break;
+            }
+            break;
+          case 'bySubListType':  {
+            const subListKey = keys[2];
+            if (subListKey) {
+              const list = content?.[subListKey];
+              console.log('aaaa', list);
+              if (Array.isArray(list) && list.length > 0) {
+                switch(list[0].type) {
+                  default:
+                  case GetContentListParams.TYPE_ARTICLE: result = '相关文章'; break;
+                  case GetContentListParams.TYPE_ARCHIVE: result = '相关文档'; break;
+                  case GetContentListParams.TYPE_VIDEO: result = '视频'; break;
+                  case GetContentListParams.TYPE_IMAGE: result = '相册'; break;
+                }
+              } else {
+                result = '相关';
+              }
+            }
+            break;
+          }
+          case 'idMap': {
+            if (currentCommonCategoryContentDefine.value?.props.customTabNameIdMap) {
+              result = currentCommonCategoryContentDefine.value.props.customTabNameIdMap[content.id];
+            }
+          }
+        }
+      }
+      return result;
+    }
+    for (const tab of tabs) {
+      if (tab.text.startsWith('dynamic')) {
+        tab.text = loadByContentData(tab.text);
+      }
+    }
+    await loadTabVisible(tabs, content);
+
   } catch (error) {
     console.error(error);
     loadState.value = false;
     errorMessage.value = formatError(error);
   }
+
+  tabDefines.value = tabs;
 }
 
 watch(() => props.pageConfigName, loadPageConfig);
@@ -309,34 +366,73 @@ const descItems = computed(() => (
 function onLoaded(d: any) {
   content.value = d;
 }
-async function load(id: number, tabsArray: DetailTabPageTabsArray) {
+async function loadTabVisible(tabs: IHomeCommonCategoryDetailTabItemDefine[], d: any) {
+  for (const tab of tabs) {
+    const v = d[tab.key];
+    let check = true
+    let visibleCheckBy = 'auto';
+    let visibleCheckKeys = [] as string[];
+    if (tab.visibleVia && tab.visibleVia.includes(':')) {
+      visibleCheckKeys = tab.visibleVia.split(':');
+      visibleCheckBy = visibleCheckKeys[0];
+    }
+    switch (visibleCheckBy) {
+      case 'auto': {
+        switch (tab.type) {
+          case 'intro': check = true; break;
+          case 'audio': check = Boolean(d.audio); break;
+          case 'video': check = Boolean(d.video); break;
+          case 'images': check = Boolean(d.images) && (d.images as string[]).length > 1; break;
+          case 'map': check = Boolean(d.latitude) && Boolean(d.longitude); break;
+          default: 
+            if (!v)
+              check = false;
+            else if (Array.isArray(v))
+              check = (v as any[]).length > 0;
+            break;
+        }
+        break;
+      }
+      case 'contentProps': {
+        for (let i = 1; i < visibleCheckKeys.length; i++) {
+          let key = visibleCheckKeys[i];
+          let keyMatchProps2 = '';
+          if (key.includes(',')) {
+            const arr = key.split(',');
+            key = arr[0];
+            keyMatchProps2 = arr[1];
+          }
+          const value = d[key];
+          switch (keyMatchProps2) {
+            case 'array0': check = Array.isArray(value) && value.length > 0; break;
+            case 'array1': check = Array.isArray(value) && value.length > 1; break;
+            case 'noZero': check = Number(value) !== 0 && !isNaN(Number(value)); break;
+            case 'trim': check = String(value).trim() !== ''; break;
+            default: check = Boolean(value); break;
+          }
+          if (!check)
+            break;
+        }
+        break;
+      }
+      case 'idMap': {
+        const idMap = currentCommonCategoryContentDefine.value?.props.customTabVisibleViaIdMap;
+        if (idMap && visibleCheckKeys.length >= 2)
+          check = idMap[`${d.id}:${visibleCheckKeys[1]}`];
+        break;
+      }
+    }
+    tab.visible = tab.visible !== false && check;
+  }
+}
+async function load(id: number) {
   if (isNaN(id) || id <= 0)
     throw new Error("请输入ID。如果正在测试,可在后台复制ID");
-  const d = await CommonContent.getContentDetail(
+  return 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 (['intro'].includes(tab.type))
-      check = true;
-    else if (tab.type === 'audio')
-      check = Boolean(d.audio);
-    else if (tab.type === 'video')
-      check = Boolean(d.video);
-    else if (tab.type === 'images')
-      check = Boolean(d.images) && (d.images as string[]).length > 1;
-    else if (tab.type === 'map')
-      check = Boolean(d.latitude) && Boolean(d.longitude);
-    else 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;
 }
 function handleGoToVr(vr: string) {
   navTo('/pages/article/web/ewebview', { url: vr })

+ 20 - 0
src/pages/article/data/defines/Details.ts

@@ -16,6 +16,22 @@ export interface IHomeCommonCategoryDetailDefine {
      * 列表选项卡定义
      */
     tabs?: IHomeCommonCategoryDetailTabItemDefine[],
+    /**
+     * 自定义标签页名称ID映射
+     */
+    customTabNameIdMap?: Record<string, string>,
+    /**
+     * 自定义标签页动态可见性判断ID映射
+     * * auto: 自动判断
+     * * contentProps: 内容属性判断 格式:contentProps:key1:key2:key3 (keys可包含逗号,逗号后为匹配属性)
+     * * * array0: 数组长度为0
+     * * * array1: 数组长度为1
+     * * * trim: 字符串长度不为0
+     * * * default: 布尔值
+     * * * noZero: 数值不为0或nan
+     * * idMap: 标签页ID映射判断 格式:id:categoy (category为visibleVia中定义的分类ID)
+     */
+    customTabVisibleViaIdMap?: Record<string, boolean>,
   },
 }
 
@@ -49,6 +65,10 @@ export interface IHomeCommonCategoryDetailTabItemBaseDefine {
    * @default 180
    */
   width?: number,
+  /**
+   * 标签页动态可见性判断方式
+   */
+  visibleVia?: string,
 
   /**
    * 判断数据键

+ 4 - 4
src/pages/article/data/editor/editors/DetailPropsEditor.vue

@@ -76,7 +76,7 @@
       </a-collapse-panel>
 
       <a-collapse-panel key="tabs" header="详情 Tab (tabs)">
-        <div v-for="(tab, i) in tabItems" :key="tabKey(tab, i)" class="nested-item tab-item">
+        <div v-for="(tab, i) in tabItems" :key="i" class="nested-item tab-item">
           <a-collapse>
             <a-collapse-panel :key="i" :header="tabHeader(tab)">
               <template #extra>
@@ -136,6 +136,9 @@
             </a-collapse-panel>
           </a-collapse>
         </div>
+        <a-form-item label="自定义标签页名称ID映射">
+          <KeyValueEditor v-model:value="props.props.customTabNameIdMap" />
+        </a-form-item>
         <div class="section-footer">
           <a-button style="flex: 4;" type="dashed" block size="small" @click="addTab">+ 添加 Tab</a-button>
           <a-button style="flex: 2;" type="dashed" block size="small" @click="pasteTab">粘贴</a-button>
@@ -186,9 +189,6 @@ const introBlockDescsList = computed(() => props.props?.introBlockDescs ?? []);
 const introBlocksList = computed(() => props.props?.introBlocks ?? []);
 const tabItems = computed(() => (props.props?.tabs || []) as IHomeCommonCategoryDetailTabItemDefine[]);
 
-function tabKey(tab: IHomeCommonCategoryDetailTabItemDefine, i: number) {
-  return `detail-tab-${i}-${tab.text}-${tab.type}`;
-}
 function tabHeader(tab: IHomeCommonCategoryDetailTabItemDefine) {
   return `${tab.text || 'Tab'} (${tab.type || '?'})`;
 }

+ 98 - 27
src/pages/article/details.vue

@@ -7,28 +7,36 @@
     <SimplePageContentLoader :loader="loader">
       <template v-if="loader.content.value">
         <view class="d-flex flex-col">
-          <ImageSwiper 
-            v-if="loader.content.value.images && loader.content.value.images.length > 0"
-            :images="loader.content.value.images"
-            height="500rpx"
-          />
-          <Image 
-            v-else-if="loader.content.value.image"
-            width="100%"
-            height="500rpx"
-            :radius="15"
-            :src="loader.content.value.image"
-            :defaultImage="AppCofig.defaultImage"
-            mode="widthFix"
-          />
-          <view v-else class="height-150"></view>
+
+          <!-- 头部 -->
+          <template v-if="querys.showHead">
+            <ImageSwiper 
+              v-if="loader.content.value.images && loader.content.value.images.length > 0"
+              :images="loader.content.value.images"
+              height="500rpx"
+            />
+            <Image 
+              v-else-if="loader.content.value.image"
+              width="100%"
+              height="500rpx"
+              :radius="15"
+              :src="loader.content.value.image"
+              :defaultImage="AppCofig.defaultImage"
+              mode="widthFix"
+            />
+            <view v-else class="height-150"></view>
+          </template>
+
+          <!-- 标题区域 -->
           <view class="d-flex flex-col p-3">
             <view class="size-ll color-title-text">{{ loader.content.value.title }}</view>
             <view class="d-flex flex-row mt-2">
               <text class="size-s color-text-content-second text-nowrap">{{ DataDateUtils.formatDate(loader.content.value.publishAt, 'YYYY-MM-dd') }}</text>
             </view>
           </view>
-          <view v-if="archiveInfo.hasArchive" class="mt-3">
+
+          <!-- 归档 -->
+          <view v-if="archiveInfo.hasArchive && querys.showArchive" class="mt-3">
             <Box2LineImageRightShadow
               class="w-100"
               titleColor="title-text"
@@ -39,6 +47,34 @@
               @click="goArchive(loader.content.value.id)"
             />
           </view>
+
+          <!-- 查询借阅功能按钮 -->
+          <view v-if="querys.showBorrow" class="mt-3">
+            <Box2LineImageRightShadow
+              class="w-100"
+              titleColor="title-text"
+              title2
+              image="https://mn.wenlvti.net/app_static/minnan/images/inhert/IconBorrow.png"
+              :title="loader.content.value.title"
+              desc="点击查询借阅"
+              @click="goBorrow(loader.content.value.title)"
+            />
+          </view>
+
+          <!-- 源网页 -->
+          <view v-if="loader.content.value.externalLink && querys.navToExternalLink !== 'none'" class="mt-3">
+            <Box2LineImageRightShadow
+              class="w-100"
+              titleColor="title-text"
+              title2
+              :image="archiveInfo.archiveIcon"
+              :title="loader.content.value.title"
+              desc="点击查看源网页"
+              @click="goExternalLink(loader.content.value.externalLink)"
+            />
+          </view>
+
+          <!-- 内容 -->
           <view class="p-3 radius-ll bg-light mt-3">
             <Parse
               v-if="loader.content.value.content"
@@ -91,7 +127,7 @@ import { computed } from "vue";
 import { StringUtils } from "@imengyu/imengyu-utils";
 import { injectAppConfiguration } from "@/api/system/useAppConfiguration";
 import { useSimpleDataLoader } from "@/common/composeabe/SimpleDataLoader";
-import { navTo } from "@/components/utils/PageAction";
+import { navTo, redirectTo } from "@/components/utils/PageAction";
 import CommonContent, { GetContentListParams } from "@/api/CommonContent";
 import Box2LineImageRightShadow from "../parts/Box2LineImageRightShadow.vue";
 import AppCofig from "@/common/config/AppCofig";
@@ -115,6 +151,38 @@ import NavBar from "@/components/nav/NavBar.vue";
 import StatusBarSpace from "@/components/layout/space/StatusBarSpace.vue";
 import { resolveCommonContentFormData } from "./common/CommonContent";
 
+
+const { querys } = useLoadQuerys({ 
+  id: 0,
+  mainBodyColumnId: 0,
+  modelId: 0,
+  /**
+   * 是否显示推荐
+   */
+  showRecommend: true,
+  /**
+   * 是否显示头部
+   * @default true
+   */
+  showHead: true,
+  /**
+   * 是否显示归档
+   * @default true
+   */
+  showArchive: true,
+  /**
+   * 是否显示查询借阅功能按钮
+   * @default false
+   */
+  showBorrow: false,
+  /**
+   * 是否自动跳转外部链接
+   * @default 'auto'
+   * @description 'auto' 自动跳转,'manual' 手动跳转,'none' 不跳转
+   */
+  navToExternalLink: 'auto',
+}, (t) => loader.loadData(t));
+
 const loader = useSimplePageContentLoader<
   GetContentDetailItem, 
   { id: number }
@@ -123,6 +191,11 @@ const loader = useSimplePageContentLoader<
     throw new Error("!params");
   const res = await NewsIndexContent.getContentDetail(params.id);
   uni.setNavigationBarTitle({ title: res.title });
+  if (querys.value.navToExternalLink === 'auto' && (!res.content || res.content.trim() === '') && res.externalLink) {
+    goExternalLink(res.externalLink);
+  } else if (querys.value.navToExternalLink !== 'none' && res.externalLink) {
+    goExternalLink(res.externalLink);
+  }
   return res;
 });
 
@@ -172,6 +245,9 @@ const recommendListLoader = useSimpleDataLoader(async () => {
   return resolveCommonContentFormData(res);
 });
 
+function goExternalLink(url: string) {
+  redirectTo('/pages/article/web/ewebview', { url });
+}
 function goArchive(id: number) {
   const archiveUrl = loader.content.value?.archives || '';
   if (!archiveUrl)
@@ -224,18 +300,13 @@ function goArchive(id: number) {
 }
 function goDetails(id: number) {
   navTo('/pages/article/details', { 
-    id, 
-    mainBodyColumnId: querys.value.mainBodyColumnId, 
-    modelId: querys.value.modelId 
+    ...querys.value,
+    id,
   });
 }
-
-const { querys } = useLoadQuerys({ 
-  id: 0,
-  mainBodyColumnId: 0,
-  modelId: 0,
-  showRecommend: true,
-}, (t) => loader.loadData(t));
+function goBorrow(title: string) {
+  navTo('/pages/article/web/ewebview', { url: `https://mn.wenlvti.net/xmlib/opac/m/search?q=${encodeURIComponent(title)}&curlibcode=&searchWay=title&hasholding=1` });
+}
 
 function getPageShareData() {
   if (!loader.content.value)

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

@@ -2,6 +2,7 @@
   <web-view 
     class="w-100 h-100vh"
     :src="finalUrl"
+    @error="onError"
   />
 </template>
 
@@ -16,4 +17,12 @@ const { querys } = useLoadQuerys({
   finalUrl.value = decodeURIComponent(url)
   console.log('web-view', finalUrl.value)
 });
+
+function onError(e: any) {
+  console.error('web-view error', e)
+  uni.showToast({
+    title: '打开网页失败',
+    icon: 'none',
+  })
+}
 </script>

+ 28 - 30
src/pages/inhert/map/index.vue

@@ -48,40 +48,38 @@ import { onLoad } from '@dcloudio/uni-app';
 import AppCofig from '@/common/config/AppCofig';
 import VillageApi from '@/api/inhert/VillageApi';
 import ScenicSpotContent from '@/api/fusion/ScenicSpotContent';
-import { useTabControl } from '@/common/composeabe/TabControl';
 import Tabs from '@/components/nav/Tabs.vue';
 import SearchBar from '@/components/form/SearchBar.vue';
 
-const { 
-  tabCurrentIndex,
-  tabs,
-} = useTabControl({
-  tabs: [
-    {
-      id: 0,
-      text: '非遗项目'
-    },
-    {
-      id: 1,
-      text: '非遗传习所'
-    },
-    {
-      id: 2,
-      text: '文物古迹'
-    },
-    {
-      id: 3,
-      text: '传统村落'
-    },
-    {
-      id: 4,
-      text: '闽南文化景区'
-    },
-  ],
-  onTabChange() {
-    listLoader.loadData(undefined, true);
+const tabCurrentIndex = ref(0);
+const tabs = [
+  {
+    id: 0,
+    text: '非遗项目'
   },
-})
+  {
+    id: 1,
+    text: '非遗传习所'
+  },
+  {
+    id: 2,
+    text: '文物古迹'
+  },
+  {
+    id: 3,
+    text: '传统村落'
+  },
+  {
+    id: 4,
+    text: '闽南文化景区'
+  },
+];
+
+
+watch(tabCurrentIndex, () => {
+  listLoader.loadData(undefined, true);
+});
+
 const mapCtx = uni.createMapContext('map');
 const categoryData = useSimpleDataLoader(async () => 
   [{