Selaa lähdekoodia

📦 富文本渲染嵌入功能,按要求调整栏目

快乐的梦鱼 1 viikko sitten
vanhempi
commit
c4f1f778bd

+ 87 - 0
package-lock.json

@@ -27,6 +27,7 @@
         "@imengyu/imengyu-utils": "^0.0.26",
         "@imengyu/js-request-transform": "^0.3.7",
         "async-validator": "^4.2.5",
+        "htmlparser2": "^10.1.0",
         "pinia": "^3.0.1",
         "tslib": "^2.8.1",
         "vue": "3.5.27",
@@ -7143,11 +7144,37 @@
         "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",
@@ -7174,6 +7201,35 @@
         "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",
@@ -8078,6 +8134,37 @@
       "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",

+ 1 - 0
package.json

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

+ 5 - 0
src/App.vue

@@ -49,5 +49,10 @@ configTheme(false, (theme, darkTheme) => {
 @use "@/common/scss/common.scss" as *;
 @use "@/common/scss/global/base.scss" as *;
 @use "@/components/index.scss" as *;
+
+page {
+  background-color: #f6f2e7;
+  overflow-x: hidden;
+}
 </style>
 

+ 13 - 0
src/common/components/rich/InjectMPRender.vue

@@ -0,0 +1,13 @@
+<template>
+  <History v-if="type === 'IntrodHistory'" />
+  <Alert v-else type="error" :message="'不支持的渲染类型:' + type" />
+</template>
+
+<script setup lang="ts">
+import Alert from '@/components/feedback/Alert.vue';
+import History from '@/pages/home/history.vue';
+
+const props = withDefaults(defineProps<{
+  type: string;
+}>(), {});
+</script>

+ 1 - 1
src/common/composeabe/TabControl.ts

@@ -23,7 +23,7 @@ export function useTabControl(options: {
   const tabCurrentId = computed(() => {
     return tabsArray.value
       .filter(t => t.visible !== false)[tabCurrentIndex.value]
-      ?.id || tabCurrentIndex.value
+      ?.id
   });
   const tabsArray = ref<TabControlItem[]>(options.tabs ?? []);
 

+ 31 - 0
src/components/display/parse/Parse.ts

@@ -0,0 +1,31 @@
+export interface ParseNode {
+  /**
+   * 标签名
+   */
+  tag: string;
+  /**
+   * 标签属性
+   */
+  attrs?: {
+    /**
+     * 元素id
+     */
+    id?: string;
+    /**
+     * 内容
+     */
+    content?: string;
+    /**
+     * 样式
+     */
+    style?: Record<string, string>;
+    /**
+     * 其他属性
+     */
+    [key: string]: unknown;
+  };
+  /**
+   * 子节点
+   */
+  children?: ParseNode[];
+}

+ 81 - 485
src/components/display/parse/Parse.vue

@@ -1,499 +1,95 @@
 <template>
-	<view id="_root" :class="(selectable ? '_select ' : '') + '_root'" :style="containerStyle">
-		<slot v-if="!nodes[0]" />
-		<!-- #ifndef APP-PLUS-NVUE -->
-		<node v-else :childs="nodes" :opts="[lazyLoad, loadingImg, errorImg, showImgMenu, selectable]" name="span" />
-		<!-- #endif -->
-		<!-- #ifdef APP-PLUS-NVUE -->
-		<web-view ref="web" src="https://mncdn.wenlvti.net/app_static/minnan/app-plus/mp-html/local.html" :style="'margin-top:-2px;height:' + height + 'px'" @onPostMessage="_onMessage" />
-		<!-- #endif -->
-	</view>
+  <view class="nana-Parse-container" :style="contentStyle">
+    <ParseNodeRender v-for="(node, index) in nodes" :key="index" :node="node" />
+  </view>
 </template>
 
-<script>
-/**
- * mp-html v2.4.1
- * @description 富文本组件
- * @tutorial https://github.com/jin-yufeng/mp-html
- * @property {String} container-style 容器的样式
- * @property {String} content 用于渲染的 html 字符串
- * @property {Boolean} copy-link 是否允许外部链接被点击时自动复制
- * @property {String} domain 主域名,用于拼接链接
- * @property {String} error-img 图片出错时的占位图链接
- * @property {Boolean} lazy-load 是否开启图片懒加载
- * @property {string} loading-img 图片加载过程中的占位图链接
- * @property {Boolean} pause-video 是否在播放一个视频时自动暂停其他视频
- * @property {Boolean} preview-img 是否允许图片被点击时自动预览
- * @property {Boolean} scroll-table 是否给每个表格添加一个滚动层使其能单独横向滚动
- * @property {Boolean | String} selectable 是否开启长按复制
- * @property {Boolean} set-title 是否将 title 标签的内容设置到页面标题
- * @property {Boolean} show-img-menu 是否允许图片被长按时显示菜单
- * @property {Object} tag-style 标签的默认样式
- * @property {Boolean | Number} use-anchor 是否使用锚点链接
- * @event {Function} load dom 结构加载完毕时触发
- * @event {Function} ready 所有图片加载完毕时触发
- * @event {Function} imgTap 图片被点击时触发
- * @event {Function} linkTap 链接被点击时触发
- * @event {Function} play 音视频播放时触发
- * @event {Function} error 媒体加载出错时触发
- */
-// #ifndef APP-PLUS-NVUE
-import node from './node/node'
-// #endif
-import Parser from './parser'
-const plugins = []
-// #ifdef APP-PLUS-NVUE
-const dom = weex.requireModule('dom')
-// #endif
-export default {
-	name: 'u-parse',
-	data() {
-		return {
-			nodes: [],
-			// #ifdef APP-PLUS-NVUE
-			height: 3
-			// #endif
-		}
-	},
-	props: {
-		containerStyle: {
-			type: String,
-			default: ''
-		},
-		content: {
-			type: String,
-			default: ''
-		},
-		copyLink: {
-			type: [Boolean, String],
-			default: true
-		},
-		domain: String,
-		errorImg: {
-			type: String,
-			default: ''
-		},
-		lazyLoad: {
-			type: [Boolean, String],
-			default: false
-		},
-		loadingImg: {
-			type: String,
-			default: ''
-		},
-		pauseVideo: {
-			type: [Boolean, String],
-			default: true
-		},
-		previewImg: {
-			type: [Boolean, String],
-			default: true
-		},
-		scrollTable: [Boolean, String],
-		selectable: [Boolean, String],
-		setTitle: {
-			type: [Boolean, String],
-			default: true
-		},
-		showImgMenu: {
-			type: [Boolean, String],
-			default: true
-		},
-		tagStyle: Object,
-		useAnchor: [Boolean, Number]
-	},
-	// #ifdef VUE3
-	emits: ['load', 'ready', 'imgTap', 'linkTap', 'play', 'error'],
-	// #endif
-	// #ifndef APP-PLUS-NVUE
-	components: {
-		node
-	},
-	// #endif
-	watch: {
-		content(content) {
-			this.setContent(content)
-		}
-	},
-	created() {
-		this.plugins = []
-		for (let i = plugins.length; i--;) {
-			this.plugins.push(new plugins[i](this))
-		}
-	},
-	mounted() {
-		if (this.content && !this.nodes.length) {
-			this.setContent(this.content)
-		}
-	},
-	beforeUnmount() {
-		this._hook('onDetached')
-	},
-	methods: {
-		/**
-		 * @description 将锚点跳转的范围限定在一个 scroll-view 内
-		 * @param {Object} page scroll-view 所在页面的示例
-		 * @param {String} selector scroll-view 的选择器
-		 * @param {String} scrollTop scroll-view scroll-top 属性绑定的变量名
-		 */
-		in(page, selector, scrollTop) {
-			// #ifndef APP-PLUS-NVUE
-			if (page && selector && scrollTop) {
-				this._in = {
-					page,
-					selector,
-					scrollTop
-				}
-			}
-			// #endif
-		},
+<script setup lang="ts">
+import ParseNodeRender from './ParseNodeRender.vue'
+import { parseDocument } from 'htmlparser2'
+import { computed, provide, toRef } from 'vue';
+import type { ParseNode } from './Parse'
+import type { Element } from 'domhandler';
 
-		/**
-		 * @description 锚点跳转
-		 * @param {String} id 要跳转的锚点 id
-		 * @param {Number} offset 跳转位置的偏移量
-		 * @returns {Promise}
-		 */
-		navigateTo(id, offset) {
-			return new Promise((resolve, reject) => {
-				if (!this.useAnchor) {
-					reject(Error('Anchor is disabled'))
-					return
-				}
-				offset = offset || parseInt(this.useAnchor) || 0
-				// #ifdef APP-PLUS-NVUE
-				if (!id) {
-					dom.scrollToElement(this.$refs.web, {
-						offset
-					})
-					resolve()
-				} else {
-					this._navigateTo = {
-						resolve,
-						reject,
-						offset
-					}
-					this.$refs.web.evalJs('uni.postMessage({data:{action:"getOffset",offset:(document.getElementById(' + id + ')||{}).offsetTop}})')
-				}
-				// #endif
-				// #ifndef APP-PLUS-NVUE
-				let deep = ' '
-				// #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO
-				deep = '>>>'
-				// #endif
-				const selector = uni.createSelectorQuery()
-					// #ifndef MP-ALIPAY
-					.in(this._in ? this._in.page : this)
-					// #endif
-					.select((this._in ? this._in.selector : '._root') + (id ? `${deep}#${id}` : '')).boundingClientRect()
-				if (this._in) {
-					selector.select(this._in.selector).scrollOffset()
-						.select(this._in.selector).boundingClientRect()
-				} else {
-					// 获取 scroll-view 的位置和滚动距离
-					selector.selectViewport().scrollOffset() // 获取窗口的滚动距离
-				}
-				selector.exec(res => {
-					if (!res[0]) {
-						reject(Error('Label not found'))
-						return
-					}
-					const scrollTop = res[1].scrollTop + res[0].top - (res[2] ? res[2].top : 0) + offset
-					if (this._in) {
-						// scroll-view 跳转
-						this._in.page[this._in.scrollTop] = scrollTop
-					} else {
-						// 页面跳转
-						uni.pageScrollTo({
-							scrollTop,
-							duration: 300
-						})
-					}
-					resolve()
-				})
-				// #endif
-			})
-		},
-
-		/**
-		 * @description 获取文本内容
-		 * @return {String}
-		 */
-		getText(nodes) {
-			let text = '';
-			(function traversal(nodes) {
-				for (let i = 0; i < nodes.length; i++) {
-					const node = nodes[i]
-					if (node.type === 'text') {
-						text += node.text.replace(/&amp;/g, '&')
-					} else if (node.name === 'br') {
-						text += '\n'
-					} else {
-						// 块级标签前后加换行
-						const isBlock = node.name === 'p' || node.name === 'div' || node.name === 'tr' || node.name === 'li' || (node.name[0] === 'h' && node.name[1] > '0' && node.name[1] < '7')
-						if (isBlock && text && text[text.length - 1] !== '\n') {
-							text += '\n'
-						}
-						// 递归获取子节点的文本
-						if (node.children) {
-							traversal(node.children)
-						}
-						if (isBlock && text[text.length - 1] !== '\n') {
-							text += '\n'
-						} else if (node.name === 'td' || node.name === 'th') {
-							text += '\t'
-						}
-					}
-				}
-			})(nodes || this.nodes)
-			return text
-		},
-
-		/**
-		 * @description 获取内容大小和位置
-		 * @return {Promise}
-		 */
-		getRect() {
-			return new Promise((resolve, reject) => {
-				uni.createSelectorQuery()
-					// #ifndef MP-ALIPAY
-					.in(this)
-					// #endif
-					.select('#_root').boundingClientRect().exec(res => res[0] ? resolve(res[0]) : reject(Error('Root label not found')))
-			})
-		},
-
-		/**
-		 * @description 暂停播放媒体
-		 */
-		pauseMedia() {
-			for (let i = (this._videos || []).length; i--;) {
-				this._videos[i].pause()
-			}
-			// #ifdef APP-PLUS
-			const command = 'for(var e=document.getElementsByTagName("video"),i=e.length;i--;)e[i].pause()'
-			// #ifndef APP-PLUS-NVUE
-			let page = this.$parent
-			while (!page.$scope) page = page.$parent
-			page.$scope.$getAppWebview().evalJS(command)
-			// #endif
-			// #ifdef APP-PLUS-NVUE
-			this.$refs.web.evalJs(command)
-			// #endif
-			// #endif
-		},
+export interface ParseProps {
+  /**
+   * HTML解析内容
+   */
+  content: string;
+  /**
+   * 标签样式。键为标签名,值为样式
+   */
+  tagStyle?: Record<string, string>;
+  /**
+   * 容器样式
+   */
+  contentStyle?: any;
+}
 
-		/**
-		 * @description 设置媒体播放速率
-		 * @param {Number} rate 播放速率
-		 */
-		setPlaybackRate(rate) {
-			this.playbackRate = rate
-			for (let i = (this._videos || []).length; i--;) {
-				this._videos[i].playbackRate(rate)
-			}
-			// #ifdef APP-PLUS
-			const command = 'for(var e=document.getElementsByTagName("video"),i=e.length;i--;)e[i].playbackRate=' + rate
-			// #ifndef APP-PLUS-NVUE
-			let page = this.$parent
-			while (!page.$scope) page = page.$parent
-			page.$scope.$getAppWebview().evalJS(command)
-			// #endif
-			// #ifdef APP-PLUS-NVUE
-			this.$refs.web.evalJs(command)
-			// #endif
-			// #endif
-		},
+const props = withDefaults(defineProps<ParseProps>(), {
+  tagStyle: () => ({})
+});
 
-		/**
-		 * @description 设置内容
-		 * @param {String} content html 内容
-		 * @param {Boolean} append 是否在尾部追加
-		 */
-		setContent(content, append) {
-			if (!append || !this.imgList) {
-				this.imgList = []
-			}
-			const nodes = new Parser(this).parse(content)
-			// #ifdef APP-PLUS-NVUE
-			if (this._ready) {
-				this._set(nodes, append)
-			}
-			// #endif
-			this.$set(this, 'nodes', append ? (this.nodes || []).concat(nodes) : nodes)
+provide('tagStyle', toRef(props, 'tagStyle'));
 
-			// #ifndef APP-PLUS-NVUE
-			this._videos = []
-			this.$nextTick(() => {
-				this._hook('onLoad')
-				this.$emit('load')
-			})
+// 解析HTML为节点树
+const parseHtml = (html: string): ParseNode[] => {
+  const nodes: ParseNode[] = [];
+  const doc = parseDocument(html);
+  
+  const traverse = (element: Element): ParseNode => {
+    if (element.type !== 'tag') {
+      return { tag: 'text' };
+    }
 
-			if (this.lazyLoad || this.imgList._unloadimgs < this.imgList.length / 2) {
-				// 设置懒加载,每 350ms 获取高度,不变则认为加载完毕
-				let height = 0
-				const callback = rect => {
-					if (!rect || !rect.height) rect = {}
-					// 350ms 总高度无变化就触发 ready 事件
-					if (rect.height === height) {
-						this.$emit('ready', rect)
-					} else {
-						height = rect.height
-						setTimeout(() => {
-							this.getRect().then(callback).catch(callback)
-						}, 350)
-					}
-				}
-				this.getRect().then(callback).catch(callback)
-			} else {
-				// 未设置懒加载,等待所有图片加载完毕
-				if (!this.imgList._unloadimgs) {
-					this.getRect().then(rect => {
-						this.$emit('ready', rect)
-					}).catch(() => {
-						this.$emit('ready', {})
-					})
-				}
-			}
-			// #endif
-		},
+    const node: ParseNode = {
+      tag: element.name,
+      attrs: element.attribs || {},
+      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()) {
+          node.children?.push({
+            tag: 'text',
+            attrs: {
+              content: child.data
+            }
+          });
+        }
+      }
+    }
+    
+    return node;
+  };
+  for (const child of doc.children) {
+    if (child.type === 'tag') {
+      nodes.push(traverse(child));
+    } else if (child.type === 'text' && child.data?.trim()) {
+      nodes.push({
+        tag: 'text',
+        attrs: {
+          content: child.data
+        }
+      });
+    }
+  }
 
-		/**
-		 * @description 调用插件钩子函数
-		 */
-		_hook(name) {
-			for (let i = plugins.length; i--;) {
-				if (this.plugins[i][name]) {
-					this.plugins[i][name]()
-				}
-			}
-		},
+  console.log(nodes);
+  return nodes;
+};
 
-		// #ifdef APP-PLUS-NVUE
-		/**
-		 * @description 设置内容
-		 */
-		_set(nodes, append) {
-			this.$refs.web.evalJs('setContent(' + JSON.stringify(nodes).replace(/%22/g, '') + ',' + JSON.stringify([this.containerStyle.replace(/(?:margin|padding)[^;]+/g, ''), this.errorImg, this.loadingImg, this.pauseVideo, this.scrollTable, this.selectable]) + ',' + append + ')')
-		},
+// 计算属性,获取解析后的节点树
+const nodes = computed(() => parseHtml(props.content));
 
-		/**
-		 * @description 接收到 web-view 消息
-		 */
-		_onMessage(e) {
-			const message = e.detail.data[0]
-			switch (message.action) {
-				// web-view 初始化完毕
-				case 'onJSBridgeReady':
-					this._ready = true
-					if (this.nodes) {
-						this._set(this.nodes)
-					}
-					break
-				// 内容 dom 加载完毕
-				case 'onLoad':
-					this.height = message.height
-					this._hook('onLoad')
-					this.$emit('load')
-					break
-				// 所有图片加载完毕
-				case 'onReady':
-					this.getRect().then(res => {
-						this.$emit('ready', res)
-					}).catch(() => {
-						this.$emit('ready', {})
-					})
-					break
-				// 总高度发生变化
-				case 'onHeightChange':
-					this.height = message.height
-					break
-				// 图片点击
-				case 'onImgTap':
-					this.$emit('imgTap', message.attrs)
-					if (this.previewImg) {
-						uni.previewImage({
-							current: parseInt(message.attrs.i),
-							urls: this.imgList
-						})
-					}
-					break
-				// 链接点击
-				case 'onLinkTap': {
-					const href = message.attrs.href
-					this.$emit('linkTap', message.attrs)
-					if (href) {
-						// 锚点跳转
-						if (href[0] === '#') {
-							if (this.useAnchor) {
-								dom.scrollToElement(this.$refs.web, {
-									offset: message.offset
-								})
-							}
-						} else if (href.includes('://')) {
-							// 打开外链
-							if (this.copyLink) {
-								plus.runtime.openWeb(href)
-							}
-						} else {
-							uni.navigateTo({
-								url: href,
-								fail() {
-									uni.switchTab({
-										url: href
-									})
-								}
-							})
-						}
-					}
-					break
-				}
-				case 'onPlay':
-					this.$emit('play')
-					break
-				// 获取到锚点的偏移量
-				case 'getOffset':
-					if (typeof message.offset === 'number') {
-						dom.scrollToElement(this.$refs.web, {
-							offset: message.offset + this._navigateTo.offset
-						})
-						this._navigateTo.resolve()
-					} else {
-						this._navigateTo.reject(Error('Label not found'))
-					}
-					break
-				// 点击
-				case 'onClick':
-					this.$emit('tap')
-					this.$emit('click')
-					break
-				// 出错
-				case 'onError':
-					this.$emit('error', {
-						source: message.source,
-						attrs: message.attrs
-					})
-			}
-		}
-		// #endif
-	}
-}
 </script>
 
-<style>
-/* #ifndef APP-PLUS-NVUE */
-/* 根节点样式 */
-._root {
-	padding: 1px 0;
-	overflow-x: auto;
-	overflow-y: hidden;
-	-webkit-overflow-scrolling: touch;
-}
-
-/* 长按复制 */
-._select {
-	user-select: text;
+<style scoped>
+.nana-Parse-container {
+  width: 100%;
 }
-
-/* #endif */
-</style>
+</style>

+ 188 - 0
src/components/display/parse/ParseNodeRender.vue

@@ -0,0 +1,188 @@
+<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 || ''" 
+  />
+  
+  <!-- 嵌入小程序内容 -->
+  <view v-else-if="node.tag === 'inject-mp'">
+    <InjectMPRender :type="node.attrs?.type as string || ''" v-bind="node.attrs" />
+  </view>
+
+  <!-- 其他标签 -->
+  <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>
+
+  <!-- 文本 -->
+  <text 
+    v-else
+    :style="style"
+    :class="node.attrs?.class || ''" 
+  >
+    {{ node.attrs?.content }}
+  </text>
+</template>
+
+<script setup lang="ts">
+import { computed, inject, ref, type Ref } from 'vue';
+import ParseNodeRender from './ParseNodeRender.vue';
+import type { ParseNode } from './Parse';
+import InjectMPRender from '@/common/components/rich/InjectMPRender.vue';
+
+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>

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

@@ -0,0 +1,182 @@
+<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 - 584
src/components/display/parse/node/node.vue

@@ -1,584 +0,0 @@
-<template>
-  <view :id="attrs.id" :class="'_block _'+name+' '+attrs.class" :style="attrs.style">
-    <block v-for="(n, i) in childs" v-bind:key="i">
-      <!-- 图片 -->
-      <!-- 占位图 -->
-      <image v-if="n.name==='img'&&!n.t&&((opts[1]&&!ctrl[i])||ctrl[i]<0)" class="_img" :style="n.attrs.style" :src="ctrl[i]<0?opts[2]:opts[1]" mode="widthFix" />
-      <!-- 显示图片 -->
-      <!-- #ifdef H5 || (APP-PLUS && VUE2) -->
-      <img v-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+n.attrs.style" :src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
-      <!-- #endif -->
-      <!-- #ifndef H5 || (APP-PLUS && VUE2) -->
-      <!-- 表格中的图片,使用 rich-text 防止大小不正确 -->
-      <rich-text v-if="n.name==='img'&&n.t" :style="'display:'+n.t" :nodes="'<img class=\'_img\' style=\''+n.attrs.style+'\' src=\''+n.attrs.src+'\'>'" :data-i="i" @tap.stop="imgTap" />
-      <!-- #endif -->
-      <!-- #ifndef H5 || APP-PLUS -->
-      <image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+'width:'+(ctrl[i]||1)+'px;height:1px;'+n.attrs.style" :src="n.attrs.src" :mode="!n.h?'widthFix':(!n.w?'heightFix':'')" :lazy-load="opts[0]" :webp="n.webp" :show-menu-by-longpress="opts[3]&&!n.attrs.ignore" :image-menu-prevent="!opts[3]||n.attrs.ignore" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
-      <!-- #endif -->
-      <!-- #ifdef APP-PLUS && VUE3 -->
-      <image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+'width:'+(ctrl[i]||1)+'px;'+n.attrs.style" :src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')" :mode="!n.h?'widthFix':(!n.w?'heightFix':'')" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
-      <!-- #endif -->
-      <!-- 文本 -->
-      <!-- #ifdef MP-WEIXIN -->
-      <text v-else-if="n.text" :user-select="opts[4]=='force'&&isiOS" decode>{{n.text}}</text>
-      <!-- #endif -->
-      <!-- #ifndef MP-WEIXIN || MP-BAIDU || MP-ALIPAY || MP-TOUTIAO -->
-      <text v-else-if="n.text" decode>{{n.text}}</text>
-      <!-- #endif -->
-      <text v-else-if="n.name==='br'">\n</text>
-      <!-- 链接 -->
-      <view v-else-if="n.name==='a'" :id="n.attrs.id" :class="(n.attrs.href?'_a ':'')+n.attrs.class" hover-class="_hover" :style="'display:inline;'+n.attrs.style" :data-i="i" @tap.stop="linkTap">
-        <node name="span" :childs="n.children" :opts="opts" style="display:inherit" />
-      </view>
-      <!-- 视频 -->
-      <!-- #ifdef APP-PLUS -->
-      <view v-else-if="n.html" :id="n.attrs.id" :class="'_video '+n.attrs.class" :style="n.attrs.style" v-html="n.html" @vplay.stop="play" />
-      <!-- #endif -->
-      <!-- #ifndef APP-PLUS -->
-      <video v-else-if="n.name==='video'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :autoplay="n.attrs.autoplay" :controls="n.attrs.controls" :loop="n.attrs.loop" :muted="n.attrs.muted" :object-fit="n.attrs['object-fit']" :poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i" @play="play" @error="mediaError" />
-      <!-- #endif -->
-      <!-- #ifdef H5 || APP-PLUS -->
-      <iframe v-else-if="n.name==='iframe'" :style="n.attrs.style" :allowfullscreen="n.attrs.allowfullscreen" :frameborder="n.attrs.frameborder" :src="n.attrs.src" />
-      <embed v-else-if="n.name==='embed'" :style="n.attrs.style" :src="n.attrs.src" />
-      <!-- #endif -->
-      <!-- #ifndef MP-TOUTIAO || ((H5 || APP-PLUS) && VUE3) -->
-      <!-- 音频 -->
-      <audio v-else-if="n.name==='audio'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :author="n.attrs.author" :controls="n.attrs.controls" :loop="n.attrs.loop" :name="n.attrs.name" :poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i" @play="play" @error="mediaError" />
-      <!-- #endif -->
-      <view v-else-if="(n.name==='table'&&n.c)||n.name==='li'" :id="n.attrs.id" :class="'_'+n.name+' '+n.attrs.class" :style="n.attrs.style">
-        <node v-if="n.name==='li'" :childs="n.children" :opts="opts" />
-        <view v-else v-for="(tbody, x) in n.children" v-bind:key="x" :class="'_'+tbody.name+' '+tbody.attrs.class" :style="tbody.attrs.style">
-          <node v-if="tbody.name==='td'||tbody.name==='th'" :childs="tbody.children" :opts="opts" />
-          <block v-else v-for="(tr, y) in tbody.children" v-bind:key="y">
-            <view v-if="tr.name==='td'||tr.name==='th'" :class="'_'+tr.name+' '+tr.attrs.class" :style="tr.attrs.style">
-              <node :childs="tr.children" :opts="opts" />
-            </view>
-            <view v-else :class="'_'+tr.name+' '+tr.attrs.class" :style="tr.attrs.style">
-              <view v-for="(td, z) in tr.children" v-bind:key="z" :class="'_'+td.name+' '+td.attrs.class" :style="td.attrs.style">
-                <node :childs="td.children" :opts="opts" />
-              </view>
-            </view>
-          </block>
-        </view>
-      </view>
-
-      <!-- 富文本 -->
-      <!-- #ifdef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->
-      <rich-text v-else-if="!n.c&&!handler.isInline(n.name, n.attrs.style)" :id="n.attrs.id" :style="n.f" :user-select="opts[4]" :nodes="[n]" />
-      <!-- #endif -->
-      <!-- #ifndef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->
-      <rich-text v-else-if="!n.c" :id="n.attrs.id" :style="n.f+';display:inline'" :preview="false" :selectable="opts[4]" :user-select="opts[4]" :nodes="[n]" />
-      <!-- #endif -->
-      <!-- 继续递归 -->
-      <view v-else-if="n.c===2" :id="n.attrs.id" :class="'_block _'+n.name+' '+n.attrs.class" :style="n.f+';'+n.attrs.style">
-        <node v-for="(n2, j) in n.children" v-bind:key="j" :style="n2.f" :name="n2.name" :attrs="n2.attrs" :childs="n2.children" :opts="opts" />
-      </view>
-      <node v-else :style="n.f" :name="n.name" :attrs="n.attrs" :childs="n.children" :opts="opts" />
-    </block>
-  </view>
-</template>
-<script module="handler" lang="wxs">
-// 行内标签列表
-var inlineTags = {
-  abbr: true,
-  b: true,
-  big: true,
-  code: true,
-  del: true,
-  em: true,
-  i: true,
-  ins: true,
-  label: true,
-  q: true,
-  small: true,
-  span: true,
-  strong: true,
-  sub: true,
-  sup: true
-}
-/**
- * @description 判断是否为行内标签
- */
-module.exports = {
-  isInline: function (tagName, style) {
-    return inlineTags[tagName] || (style || '').indexOf('display:inline') !== -1
-  }
-}
-</script>
-<script>
-
-import node from './node'
-export default {
-  name: 'node',
-  options: {
-    // #ifdef MP-WEIXIN
-    virtualHost: true,
-    // #endif
-    // #ifdef MP-TOUTIAO
-    addGlobalClass: false
-    // #endif
-  },
-  data () {
-    return {
-      ctrl: {},
-      // #ifdef MP-WEIXIN
-      isiOS: uni.getDeviceInfo().system.includes('iOS')
-      // #endif
-    }
-  },
-  props: {
-    name: String,
-    attrs: {
-      type: Object,
-      default () {
-        return {}
-      }
-    },
-    childs: Array,
-    opts: Array
-  },
-  components: {
-
-    // #ifndef (H5 || APP-PLUS) && VUE3
-    node
-    // #endif
-  },
-  mounted () {
-    this.$nextTick(() => {
-      for (this.root = this.$parent; this.root.$options.name !== 'u-parse'; this.root = this.root.$parent);
-    })
-    // #ifdef H5 || APP-PLUS
-    if (this.opts[0]) {
-      let i
-      for (i = this.childs.length; i--;) {
-        if (this.childs[i].name === 'img') break
-      }
-      if (i !== -1) {
-        this.observer = uni.createIntersectionObserver(this).relativeToViewport({
-          top: 500,
-          bottom: 500
-        })
-        this.observer.observe('._img', res => {
-          if (res.intersectionRatio) {
-            this.$set(this.ctrl, 'load', 1)
-            this.observer.disconnect()
-          }
-        })
-      }
-    }
-    // #endif
-  },
-  beforeUnmount () {
-    // #ifdef H5 || APP-PLUS
-    if (this.observer) {
-      this.observer.disconnect()
-    }
-    // #endif
-  },
-  methods:{
-    // #ifdef MP-WEIXIN
-    toJSON () { return this },
-    // #endif
-    /**
-     * @description 播放视频事件
-     * @param {Event} e
-     */
-    play (e) {
-      this.root.$emit('play')
-      // #ifndef APP-PLUS
-      if (this.root.pauseVideo) {
-        let flag = false
-        const id = e.target.id
-        for (let i = this.root._videos.length; i--;) {
-          if (this.root._videos[i].id === id) {
-            flag = true
-          } else {
-            this.root._videos[i].pause() // 自动暂停其他视频
-          }
-        }
-        // 将自己加入列表
-        if (!flag) {
-          const ctx = uni.createVideoContext(id
-            // #ifndef MP-BAIDU
-            , this
-            // #endif
-          )
-          ctx.id = id
-          if (this.root.playbackRate) {
-            ctx.playbackRate(this.root.playbackRate)
-          }
-          this.root._videos.push(ctx)
-        }
-      }
-      // #endif
-    },
-
-    /**
-     * @description 图片点击事件
-     * @param {Event} e
-     */
-    imgTap (e) {
-      const node = this.childs[e.currentTarget.dataset.i]
-      if (node.a) {
-        this.linkTap(node.a)
-        return
-      }
-      if (node.attrs.ignore) return
-      // #ifdef H5 || APP-PLUS
-      node.attrs.src = node.attrs.src || node.attrs['data-src']
-      // #endif
-      this.root.$emit('imgTap', node.attrs)
-      // 自动预览图片
-      if (this.root.previewImg) {
-        uni.previewImage({
-          // #ifdef MP-WEIXIN
-          showmenu: this.root.showImgMenu,
-          // #endif
-          // #ifdef MP-ALIPAY
-          enablesavephoto: this.root.showImgMenu,
-          enableShowPhotoDownload: this.root.showImgMenu,
-          // #endif
-          current: parseInt(node.attrs.i),
-          urls: this.root.imgList
-        })
-      }
-    },
-
-    /**
-     * @description 图片长按
-     */
-    imgLongTap (e) {
-      // #ifdef APP-PLUS
-      const attrs = this.childs[e.currentTarget.dataset.i].attrs
-      if (this.opts[3] && !attrs.ignore) {
-        uni.showActionSheet({
-          itemList: ['保存图片'],
-          success: () => {
-            const save = path => {
-              uni.saveImageToPhotosAlbum({
-                filePath: path,
-                success () {
-                  uni.showToast({
-                    title: '保存成功'
-                  })
-                }
-              })
-            }
-            if (this.root.imgList[attrs.i].startsWith('http')) {
-              uni.downloadFile({
-                url: this.root.imgList[attrs.i],
-                success: res => save(res.tempFilePath)
-              })
-            } else {
-              save(this.root.imgList[attrs.i])
-            }
-          }
-        })
-      }
-      // #endif
-    },
-
-    /**
-     * @description 图片加载完成事件
-     * @param {Event} e
-     */
-    imgLoad (e) {
-      const i = e.currentTarget.dataset.i
-      /* #ifndef H5 || (APP-PLUS && VUE2) */
-      if (!this.childs[i].w) {
-        // 设置原宽度
-        this.$set(this.ctrl, i, e.detail.width)
-      } else /* #endif */ if ((this.opts[1] && !this.ctrl[i]) || this.ctrl[i] === -1) {
-        // 加载完毕,取消加载中占位图
-        this.$set(this.ctrl, i, 1)
-      }
-      this.checkReady()
-    },
-
-    /**
-     * @description 检查是否所有图片加载完毕
-     */
-    checkReady () {
-      if (!this.root.lazyLoad) {
-        this.root._unloadimgs -= 1
-        if (!this.root._unloadimgs) {
-          setTimeout(() => {
-            this.root.getRect().then(rect => {
-              this.root.$emit('ready', rect)
-            }).catch(() => {
-              this.root.$emit('ready', {})
-            })
-          }, 350)
-        }
-      }
-    },
-
-    /**
-     * @description 链接点击事件
-     * @param {Event} e
-     */
-    linkTap (e) {
-      const node = e.currentTarget ? this.childs[e.currentTarget.dataset.i] : {}
-      const attrs = node.attrs || e
-      const href = attrs.href
-      this.root.$emit('linkTap', Object.assign({
-        innerText: this.root.getText(node.children || []) // 链接内的文本内容
-      }, attrs))
-      if (href) {
-        if (href[0] === '#') {
-          // 跳转锚点
-          this.root.navigateTo(href.substring(1)).catch(() => { })
-        } else if (href.split('?')[0].includes('://')) {
-          // 复制外部链接
-          if (this.root.copyLink) {
-            // #ifdef H5
-            window.open(href)
-            // #endif
-            // #ifdef MP
-            uni.setClipboardData({
-              data: href,
-              success: () =>
-                uni.showToast({
-                  title: '链接已复制'
-                })
-            })
-            // #endif
-            // #ifdef APP-PLUS
-            plus.runtime.openWeb(href)
-            // #endif
-          }
-        } else {
-          // 跳转页面
-          uni.navigateTo({
-            url: href,
-            fail () {
-              uni.switchTab({
-                url: href,
-                fail () { }
-              })
-            }
-          })
-        }
-      }
-    },
-
-    /**
-     * @description 错误事件
-     * @param {Event} e
-     */
-    mediaError (e) {
-      const i = e.currentTarget.dataset.i
-      const node = this.childs[i]
-      // 加载其他源
-      if (node.name === 'video' || node.name === 'audio') {
-        let index = (this.ctrl[i] || 0) + 1
-        if (index > node.src.length) {
-          index = 0
-        }
-        if (index < node.src.length) {
-          this.$set(this.ctrl, i, index)
-          return
-        }
-      } else if (node.name === 'img') {
-        // #ifdef H5 && VUE3
-        if (this.opts[0] && !this.ctrl.load) return
-        // #endif
-        // 显示错误占位图
-        if (this.opts[2]) {
-          this.$set(this.ctrl, i, -1)
-        }
-        this.checkReady()
-      }
-      if (this.root) {
-        this.root.$emit('error', {
-          source: node.name,
-          attrs: node.attrs,
-          // #ifndef H5 && VUE3
-          errMsg: e.detail.errMsg
-          // #endif
-        })
-      }
-    }
-  }
-}
-</script>
-<style>
-/* a 标签默认效果 */
-._a {
-  padding: 1.5px 0 1.5px 0;
-  color: #366092;
-  /* #ifndef APP-NVUE */
-  word-break: break-all;
-  /* #endif */
-}
-
-/* a 标签点击态效果 */
-._hover {
-  text-decoration: underline;
-  opacity: 0.7;
-}
-
-/* 图片默认效果 */
-._img {
-  max-width: 100%;
-  -webkit-touch-callout: none;
-}
-
-/* 内部样式 */
-
-._block {
-  /* #ifndef APP-NVUE */
-  display: block;
-  /* #endif */
-}
-
-._b,
-._strong {
-  font-weight: bold;
-}
-
-._code {
-  font-family: monospace;
-}
-
-._del {
-  text-decoration: line-through;
-}
-
-._em,
-._i {
-  font-style: italic;
-}
-
-._h1 {
-  font-size: 2em;
-}
-
-._h2 {
-  font-size: 1.5em;
-}
-
-._h3 {
-  font-size: 1.17em;
-}
-
-._h5 {
-  font-size: 0.83em;
-}
-
-._h6 {
-  font-size: 0.67em;
-}
-
-._h1,
-._h2,
-._h3,
-._h4,
-._h5,
-._h6 {
-  /* #ifndef APP-NVUE */
-  display: block;
-  /* #endif */
-  font-weight: bold;
-}
-
-._image {
-  height: 1px;
-}
-
-._ins {
-  text-decoration: underline;
-}
-
-._li {
-  display: list-item;
-}
-
-._ol {
-  list-style-type: decimal;
-}
-
-._ol,
-._ul {
-  /* #ifndef APP-NVUE */
-  display: block;
-  /* #endif */
-  padding-left: 40px;
-  margin: 1em 0;
-}
-
-._q::before {
-  content: '"';
-}
-
-._q::after {
-  content: '"';
-}
-
-._sub {
-  font-size: smaller;
-  vertical-align: sub;
-}
-
-._sup {
-  font-size: smaller;
-  vertical-align: super;
-}
-
-._thead,
-._tbody,
-._tfoot {
-  display: table-row-group;
-}
-
-._tr {
-  display: table-row;
-}
-
-._td,
-._th {
-  display: table-cell;
-  vertical-align: middle;
-}
-
-._th {
-  font-weight: bold;
-  text-align: center;
-}
-
-._ul {
-  list-style-type: disc;
-}
-
-._ul ._ul {
-  margin: 0;
-  list-style-type: circle;
-}
-
-._ul ._ul ._ul {
-  list-style-type: square;
-}
-
-._abbr,
-._b,
-._code,
-._del,
-._em,
-._i,
-._ins,
-._label,
-._q,
-._span,
-._strong,
-._sub,
-._sup {
-  display: inline;
-}
-
-/* #ifdef APP-PLUS */
-._video {
-  width: 300px;
-  height: 225px;
-}
-/* #endif */
-</style>

+ 0 - 22
src/components/display/parse/parse.js

@@ -1,22 +0,0 @@
-/*
- * @Author       : LQ
- * @Description  :
- * @version      : 1.0
- * @Date         : 2021-08-20 16:44:21
- * @LastAuthor   : LQ
- * @lastTime     : 2021-08-20 17:17:33
- * @FilePath     : /u-view2.0/uview-ui/libs/config/props/parse.js
- */
-export default {
-    // parse
-    parse: {
-        copyLink: true,
-        errorImg: '',
-        lazyLoad: false,
-        loadingImg: '',
-        pauseVideo: true,
-        previewImg: true,
-        setTitle: true,
-        showImgMenu: true
-    }
-}

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 1337
src/components/display/parse/parser.js


+ 0 - 48
src/components/display/parse/props.js

@@ -1,48 +0,0 @@
-import { defineMixin } from '../../libs/vue'
-import defProps from '../../libs/config/props.js'
-export const props = defineMixin({
-    props: {
-		containerStyle: {
-          type: String,
-          default: null
-		},
-        content: String,
-        copyLink: {
-		  type: Boolean,
-		  default: () => defProps.parse.copyLink
-        },
-        domain: String,
-        errorImg: {
-		  type: String,
-		  default: () => defProps.parse.errorImg
-        },
-        lazyLoad: {
-		  type: Boolean,
-		  default: () => defProps.parse.lazyLoad
-        },
-        loadingImg: {
-		  type: String,
-		  default: () => defProps.parse.loadingImg
-        },
-        pauseVideo: {
-		  type: Boolean,
-		  default: () => defProps.parse.pauseVideo
-        },
-        previewImg: {
-		  type: Boolean,
-		  default: () => defProps.parse.previewImg
-        },
-        scrollTable: Boolean,
-        selectable: Boolean,
-        setTitle: {
-		  type: Boolean,
-		  default: () => defProps.parse.setTitle
-        },
-        showImgMenu: {
-		  type: Boolean,
-		  default: () => defProps.parse.showImgMenu
-        },
-        tagStyle: Object,
-        useAnchor: null
-	  }
-}

+ 0 - 13
src/pages.json

@@ -34,12 +34,6 @@
       }
     },
     {
-      "path": "pages/introduction/inhert",
-      "style": {
-        "navigationBarTitleText": "遗产报你知"
-      }
-    },
-    {
       "path": "pages/introduction/character/list",
       "style": {
         "navigationBarTitleText": "历史人物列表",
@@ -147,13 +141,6 @@
       }
     },
     {
-      "path": "pages/inhert/seminar/list",
-      "style": {
-        "navigationBarTitleText": "传习所列表",
-        "enablePullDownRefresh": true
-      }
-    },
-    {
       "path": "pages/inhert/seminar/details",
       "style": {
         "navigationBarTitleText": "传习所详情"

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

@@ -21,7 +21,7 @@
     <view v-if="showSearch && showList" class="d-flex flex-col">
       <SearchBar
         v-model="searchValue"
-        :placeholder="`输入关键词搜索${title}`" 
+        placeholder="输入关键词搜索" 
         @search="doSearch"
         @cancel="doSearch"
       />

+ 42 - 9
src/pages/article/data/CommonCategoryBlocks.vue

@@ -65,6 +65,7 @@
                   :title="item.title"
                   :desc="item.desc"
                   :image="item.thumbnail || item.image"
+                  :tags="(item.bottomTags as string[])"
                   @click="category.detailsPage(item)"
                 />
               </view>
@@ -81,6 +82,7 @@
                 :title="item.title"
                 :desc="item.desc"
                 :image="item.image"
+                :tags="(item.bottomTags as string[])"
                 @click="category.detailsPage(item)"
               />
             </FlexRow>
@@ -109,6 +111,21 @@
               </view>  
             </view>
           </template>
+          <template v-else-if="category.type === 'simple-text'">
+            <FlexCol>
+              <Box2LineImageRightShadow
+                v-for="(item, i) in category.data.content.value"
+                titleColor="title-text"
+                :border="false"
+                fixSize
+                :key="i"
+                :title="item.title"
+                :desc="item.desc"
+                :showImage="false"
+                :tags="(item.bottomTags as string[])"
+              />
+            </FlexCol>
+          </template>
           <template v-else>
             <Box2LineImageRightShadow
               v-for="(item, i) in category.data.content.value"
@@ -129,7 +146,7 @@
 </template>
 
 <script setup lang="ts">;
-import { type PropType } from 'vue';
+import { computed, onMounted, watch, type PropType } from 'vue';
 import { CommonContentApi, GetContentListItem, GetContentListParams } from '@/api/CommonContent';
 import { navCommonDetail, navCommonList, resolveCommonContentGetPageDetailUrlAuto, resolveCommonContentSolveProps, useHomeCommonCategoryBlock, type HomeCommonCategoryBlockProps, type IHomeCommonCategoryBlock } from '../common/CommonContent';
 import { useSimpleDataLoader } from '@/common/composeabe/SimpleDataLoader';
@@ -152,14 +169,14 @@ export interface CategoryDefine extends Omit<IHomeCommonCategoryListTabNestCateg
   title: string;
   showTitle: boolean;
   content: CommonContentApi|IHomeCommonCategoryBlock|HomeCommonCategoryBlockProps|null;
-  type?: 'article'|'large-image2'|'horizontal-large'|'large-grid2'
+  type?: 'article'|'large-image2'|'horizontal-large'|'large-grid2'|'small-grid2'|'simple-text'
     |'CalendarBlock'|'StatsBlock'|'MapBlock'|'CalendarBlock'
     |undefined;
 }
 
 const props = defineProps({
   /**
-   * 分类定义。仅支持初始化后立即使用,后续修改不会生效。
+   * 分类定义
    */
   categoryDefine: {
     type: Array as PropType<CategoryDefine[]>,
@@ -167,14 +184,17 @@ const props = defineProps({
   }
 });
 
-const categoryDatas = props.categoryDefine.map(item => { 
+const categoryDatas = computed(() => props.categoryDefine.map(item => { 
   if (!item.content)
     return {
       ...item,
       detailsPage: () => {},
       morePage: () => {
         if (item.morePage) {
-          navTo(item.morePage, {});
+          if (Array.isArray(item.morePage))
+            navTo(item.morePage[0], item.morePage[1] || {});
+          else
+            navTo(item.morePage, {});
         }
       },
       data: null,
@@ -201,7 +221,10 @@ const categoryDatas = props.categoryDefine.map(item => {
       morePage: () => {
         const content = item.content as CommonContentApi;
         if (item.morePage) {
-          navTo(item.morePage, {});
+          if (Array.isArray(item.morePage))
+            navTo(item.morePage[0], item.morePage[1] || {});
+          else
+            navTo(item.morePage, {});
         } else {
           navCommonList({
             title: item.title,
@@ -216,7 +239,7 @@ const categoryDatas = props.categoryDefine.map(item => {
           .getContentList(new GetContentListParams(), 1, item.count || 4))
           .list;
         return resolveCommonContentSolveProps(res, item.dataSolve || []);;
-      })
+      }, false)
     }
   } else {
     const block = item.content.type === 'CommonCategoryBlock' ? 
@@ -224,7 +247,7 @@ const categoryDatas = props.categoryDefine.map(item => {
       useHomeCommonCategoryBlock({
         ...item.content,
         dataSolve: item.dataSolve ?? [],
-      });
+      }, false);
     return {
       ...item,
       detailsPage: block.goDetail,
@@ -232,5 +255,15 @@ const categoryDatas = props.categoryDefine.map(item => {
       data: block.loader,
     }
   }
-});
+}));
+
+function loadCategoryDatas() {
+  categoryDatas.value.forEach(item => {
+    if (item.data)
+      item.data.loadData(undefined, true);
+  })
+}
+
+watch(categoryDatas, loadCategoryDatas);
+onMounted( loadCategoryDatas);
 </script>

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

@@ -24,6 +24,8 @@ import SeaContent from "@/api/introduction/SeaContent";
 import VictualsContent from "@/api/introduction/VictualsContent";
 import TeamsContent from "@/api/research/TeamsContent";
 import type { IHomeCommonCategoryListTabListDropdownDefine } from "./CommonCategoryDefine";
+import DiscussContent from "@/api/research/DiscussContent";
+import ResultContent from "@/api/research/ResultContent";
 
 //默认动态数据接口定义
 
@@ -109,6 +111,8 @@ export function CommonCategoryDynamicDataSerializedApi(item: IHomeCommonCategory
     case 'VictualsContent': return VictualsContent;
     case 'CustomContent': return CustomContent;
     case 'TeamsContent': return TeamsContent;
+    case 'DiscussContent': return DiscussContent;
+    case 'ResultContent': return ResultContent;
     default:
       throw new Error(`未实现的序列化接口 ${item.name}`);
   }

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

@@ -218,7 +218,7 @@ async function loadData(
   if (!tab) 
     throw new Error(`配置有误 tab:${tabSelect}`);
   if (tab.type !== 'list')
-    throw new Error(`配置有误 tab:${tabSelect} type:${tab.type}`);
+    return { list: [], total: 0 };
   const res = await doLoadDynamicListData(
     tab.data, 
     page, 

+ 266 - 33
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/introduction/inhert", {}]
+              "link": ["/pages/article/data/inhert", { "pageConfigName": "inhert" }]
             },
             {
               "title": "文化新视角",
@@ -63,7 +63,7 @@
                   },
                   {
                     "name": "ichCenter",
-                    "title": "ichCenter"
+                    "title": "非遗传习中心(所)"
                   },
                   {
                     "name": "historyData",
@@ -90,7 +90,7 @@
                     }
                   },
                   {
-                    "title": "非遗传习所",
+                    "title": "非遗传习中心()",
                     "icon": "icon-task-trip",
                     "data": {
                       "type": "serializedApi",
@@ -254,6 +254,32 @@
               "type": "nestCategory",
               "categorys": [
                 {
+                  "text": "“双歌赛”",
+                  "data": {
+                    "type": "commonContent",
+                    "params": {
+                      "mainBodyColumnId": [315],
+                      "modelId": 16
+                    }
+                  },
+                  "morePage": "/pages/travel/fashion/list",
+                  "detailsPage": "/pages/video/details",
+                  "type": "large-grid2"
+                },
+                {
+                  "text": "“世界闽南语金曲盛典”",
+                  "data": {
+                    "type": "commonContent",
+                    "params": {
+                      "mainBodyColumnId": [375],
+                      "modelId": 18
+                    }
+                  },
+                  "morePage": "/pages/travel/fashion/list",
+                  "detailsPage": "/pages/video/details",
+                  "type": "large-grid2"
+                },
+                {
                   "text": "精品剧目",
                   "data": {
                     "type": "commonContent",
@@ -348,13 +374,40 @@
               ]
             },
             {
-              "text": "研究机构",
-              "type": "list",
-              "data": {
-                "type": "serializedApi",
-                "name": "TeamsContent"
-              },
-              "dataSolve": [ "common" ]
+              "text": "研讨交流",
+              "type": "nestCategory",
+              "categorys": [
+                {
+                  "text": "研究机构",
+                  "data": {
+                    "type": "serializedApi",
+                    "name": "TeamsContent"
+                  },
+                  "dataSolve": [ "common" ],
+                  "detailsPage": "byContent",
+                  "type": ""
+                },
+                {
+                  "text": "研究活动",
+                  "data": {
+                    "type": "serializedApi",
+                    "name": "DiscussContent"
+                  },
+                  "dataSolve": [ "common" ],
+                  "detailsPage": "byContent",
+                  "type": ""
+                },
+                {
+                  "text": "研究成果",
+                  "data": {
+                    "type": "serializedApi",
+                    "name": "ResultContent"
+                  },
+                  "dataSolve": [ "common" ],
+                  "detailsPage": "byContent",
+                  "type": ""
+                }
+              ]
             }
           ]
         }
@@ -373,41 +426,27 @@
               "type": "nestCategory",
               "categorys": [
                 {
-                  "text": "闽南语双歌赛",
-                  "data": {
-                    "type": "commonContent",
-                    "params": {
-                      "mainBodyColumnId": [315],
-                      "modelId": 16
-                    }
-                  },
-                  "morePage": "/pages/travel/fashion/list",
-                  "detailsPage": "/pages/video/details",
-                  "type": "large-grid2"
-                },
-                {
-                  "text": "闽南语金曲大赛",
+                  "text": "交流活动",
                   "data": {
                     "type": "commonContent",
                     "params": {
-                      "mainBodyColumnId": [375],
+                      "mainBodyColumnId": [260, 261, 262],
                       "modelId": 18
                     }
                   },
-                  "morePage": "/pages/travel/fashion/list",
-                  "detailsPage": "/pages/video/details",
-                  "type": "large-grid2"
+                  "dataSolve": [ "common" ]
                 },
                 {
-                  "text": "交流活动",
+                  "text": "闽南语在线课程",
                   "data": {
                     "type": "commonContent",
                     "params": {
-                      "mainBodyColumnId": [260, 261, 262],
-                      "modelId": 18
+                      "mainBodyColumnId": 257,
+                      "modelId": 5
                     }
                   },
-                  "dataSolve": [ "common" ]
+                  "detailsPage": "/pages/video/details",
+                  "type": "horizontal-large"
                 }
               ]
             }
@@ -516,6 +555,62 @@
       }
     },
     {
+      "name": "seminar",
+      "title": "非遗传习中心(所)",
+      "content": {
+        "type": "CommonList",
+        "props": {
+          "itemType": "article-common",
+          "showTab": false,
+          "showTotal": true,
+          "tabs": [
+            {
+              "text": "非遗传习中心(所)",
+              "type": "list",
+              "data": {
+                "type": "serializedApi",
+                "name": "SeminarContent"
+              },
+              "dataSolve": [ "ich" ],
+              "detailsPage": "/pages/inhert/seminar/details",
+              "dropdownDefines": [
+                {
+                  "key": "level",
+                  "text": "级别",
+                  "addAll": "全部地区",
+                  "formQueryKey": "level",
+                  "data": {
+                    "type": "commonContent",
+                    "typeId": 2
+                  }
+                },
+                {
+                  "key": "region",
+                  "text": "地区",
+                  "defaultValue": 0,
+                  "addAll": "全部地区",
+                  "formQueryKey": "region",
+                  "data": {
+                    "type": "commonContent",
+                    "typeId": 1
+                  }
+                }
+              ]
+            },
+            {
+              "text": "保护单位",
+              "type": "list",
+              "data": {
+                "type": "serializedApi",
+                "name": "UnitContent"
+              },
+              "dataSolve": [ "ich" ]
+            }
+          ]
+        }
+      }
+    },
+    {
       "name": "intangible",
       "title": "非物质文化遗产",
       "content": {
@@ -529,6 +624,7 @@
             {
               "text": "非遗项目",
               "type": "list",
+              "width": 150,
               "data": {
                 "type": "serializedApi",
                 "name": "ProjectsContent"
@@ -571,11 +667,13 @@
             {
               "text": "非遗传承人",
               "type": "jump",
+              "width": 150,
               "url": "/pages/inhert/inheritor/list"
             },
             {
-              "text": "传习中心(所)",
+              "text": "非遗传习中心(所)",
               "type": "list",
+              "width": 250,
               "data": {
                 "type": "serializedApi",
                 "name": "SeminarContent"
@@ -597,6 +695,7 @@
             },
             {
               "text": "保护单位",
+              "width": 150,
               "type": "list",
               "data": {
                 "type": "serializedApi",
@@ -607,6 +706,140 @@
           ]
         }
       }
+    },
+    {
+      "name": "inhert",
+      "title": "遗产报你知",
+      "content": {
+        "type": "CommonList",
+        "props": {
+          "tabsScrollable": false,
+          "showTab": true,
+          "tabs": [
+            {
+              "text": "非物质文化遗产",
+              "type": "nestCategory",
+              "categorys": [
+                {
+                  "text": "非遗项目",
+                  "data": {
+                    "type": "serializedApi",
+                    "name": "ProjectsContent"
+                  },
+                  "dataSolve": [ "ich" ],
+                  "morePage": ["/pages/article/data/list", { "pageConfigName": "intangible", "tab": 0 }],
+                  "detailsPage": "/pages/inhert/intangible/details",
+                  "type": "horizontal-large"
+                },
+                {
+                  "text": "非遗传承人",
+                  "data": {
+                    "type": "serializedApi",
+                    "name": "InheritorContent"
+                  },
+                  "dataSolve": [ "ich" ],
+                  "morePage": "/pages/inhert/inheritor/list",
+                  "type": "horizontal-large"
+                },
+                {
+                  "text": "保护单位",
+                  "data": {
+                    "type": "serializedApi",
+                    "name": "UnitContent"
+                  },
+                  "dataSolve": [ "ich" ],
+                  "morePage": "/pages/inhert/unit/list",
+                  "type": "simple-text"
+                },
+                {
+                  "text": "非遗传习中心(所)",
+                  "data": {
+                    "type": "serializedApi",
+                    "name": "SeminarContent"
+                  },
+                  "dataSolve": [ "ich" ],
+                  "morePage": "/pages/inhert/seminar/list",
+                  "detailsPage": "/pages/inhert/seminar/details",
+                  "type": ""
+                },
+                {
+                  "text": "非遗活动",
+                  "data": {
+                    "type": "commonContent",
+                    "params": {
+                      "mainBodyColumnId": 290,
+                      "modelId": 18
+                    }
+                  },
+                  "dataSolve": [ "common" ],
+                  "detailsPage": "byContent",
+                  "type": "large-grid2"
+                }
+              ]
+            },
+            {
+              "text": "物质文化遗产",
+              "type": "list",
+              "data": {
+                "type": "serializedApi",
+                "name": "UnmoveableContent"
+              },
+              "dataSolve": [ "ich" ],
+              "morePage": "/pages/inhert/artifact/list",
+              "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
+                  }
+                }
+              ]
+            },
+            {
+              "text": "重点保护区域",
+              "type": "list",
+              "data": {
+                "type": "commonContent",
+                "params": {
+                  "mainBodyColumnId": 283,
+                  "modelId": 17
+                }
+              },
+              "dataSolve": [ "ich" ],
+              "morePage": "/pages/inhert/artifact/list",
+              "detailsPage": "/pages/inhert/artifact/details",
+              "itemType": "image-large-2",
+              "dropdownDefines": []
+            }
+          ]
+        }
+      }
     }
   ]
 }

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

@@ -51,7 +51,7 @@ export interface IHomeCommonCategoryListTabNestCategoryItemDefine {
   type: string,
   itemType?: string,
   data: IHomeCommonCategoryDynamicData,
-  morePage?: string,
+  morePage?: string|[string, Record<string, any>],
   detailsPage?: string,
   count?: number,
   dataSolve?: IHomeCommonCategoryListTabListDataSolve[],

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

@@ -109,7 +109,7 @@ const statsLoader = useSimpleDataLoader(async () => {
         value: item.total,
         titleSuffix: '处',
         type: 'normal',
-        onClick: () => navTo('/pages/inhert/seminar/list', { region: item.id }),
+        onClick: () => navTo('/pages/article/data/list', { pageConfigName: 'seminar', tab: 0, region: item.id }),
       }
     }),
     historyData: data.historyData.map((item: any) => {

+ 2 - 2
src/pages/home/history.vue

@@ -1,5 +1,5 @@
 <template>
-  <view class="d-flex flex-col bg-base">
+  <view class="d-flex flex-col">
     <SimplePageContentLoader :loader="recordData">
       <view class="main-time-line">
         <view 
@@ -50,7 +50,7 @@ const recordData = useSimpleDataLoader<GetContentListItem[]>(async () => {
   $color-primary: map.get($colors, "primary");
   $color-text: map.get($colors, "text");
   $box-color: map.get($colors, "pure");
-  $size: 750rpx;
+  $size: 700rpx;
 
   position: relative;
   width: $size;

+ 1 - 1
src/pages/home/index.vue

@@ -21,7 +21,7 @@
             innerClass="logo"
             src="https://mncdn.wenlvti.net/app_static/minnan/images/home/MainLogo.png"
             :width="150"
-            mode="widthFix"
+            :height="150"
           />
           <view>
             <text class="title">{{pageContentDefine?.props.title || ''}}</text>

+ 2 - 2
src/pages/inhert/index.vue

@@ -11,7 +11,7 @@
         />
       </template>
     </NavBar>
-    <Inhert />
+    <CommonCategoryList pageConfigName="inhert" />
     <Height :height="150" />
     <Tabbar :current="2" />
   </FlexCol>
@@ -20,12 +20,12 @@
 <script setup lang="ts">
 import Tabbar from '@/common/components/tabs/Tabbar.vue';
 import { onShareTimeline, onShareAppMessage } from '@dcloudio/uni-app';
-import Inhert from '../introduction/inhert.vue';
 import FlexCol from '@/components/layout/FlexCol.vue';
 import StatusBarSpace from '@/components/layout/space/StatusBarSpace.vue';
 import NavBar from '@/components/nav/NavBar.vue';
 import Image from '@/components/basic/Image.vue';
 import Height from '@/components/layout/space/Height.vue';
+import CommonCategoryList from '../article/data/CommonCategoryList.vue';
 
 onShareTimeline(() => {
   return {}; 

+ 0 - 249
src/pages/introduction/inhert.vue

@@ -1,249 +0,0 @@
-<template>
-  <FlexCol :padding="[0, 30]" backgroundColor="background.page">
-    
-    <Tabs
-      v-model:currentIndex="activeIndex"
-      :width="690"
-      :tabs="[
-        {
-          text: '非物质文化遗产',
-        },
-        {
-          text: '物质文化遗产',
-        },
-      ]"
-    />
-
-    <template v-if="activeIndex === 0">
-      <HomeTitle title="非遗项目" showMore @clickMore="navTo('/pages/article/data/list', { pageConfigName: 'intangible', tab: 0 })" />
-      <!-- 非遗项目 -->
-      <SimplePageContentLoader :loader="intangibleData" >
-        <scroll-view scroll-x>
-          <FlexRow>
-            <Box2LineLargeImageUserShadow
-              v-for="(item, i) in intangibleData.content.value"
-              classNames="width-2-3 mr-2"
-              titleColor="title-text"
-              fixSize
-              title1
-              :key="i"
-              :title="item.title"
-              :desc="item.desc"
-              :image="item.image"
-              :tags="item.bottomTags"
-              @click="navTo('/pages/inhert/intangible/details', { id: item.id })"
-            />
-          </FlexRow>
-        </scroll-view>
-      </SimplePageContentLoader>
-
-      <!-- 非遗传承人 -->
-      <HomeTitle title="非遗传承人" showMore @clickMore="navTo('/pages/inhert/inheritor/list')" />
-      <SimplePageContentLoader :loader="inheritorData">
-        <FlexCol>
-          <Box2LineImageRightShadow
-            v-for="(item, i) in inheritorData.content.value"
-            titleColor="title-text"
-            fixSize
-            :key="i"
-            :title="item.title"
-            :desc="item.desc"
-            :image="item.image"
-            :tags="item.bottomTags"
-            @click="navTo('/pages/inhert/inheritor/details', { id: item.id })"
-          />
-        </FlexCol>
-      </SimplePageContentLoader>
-
-      <!-- 保护单位 -->
-      <HomeTitle title="保护单位" showMore @clickMore="navTo('/pages/inhert/unit/list')" />
-      <SimplePageContentLoader :loader="unitData">
-        <FlexCol>
-          <Box2LineImageRightShadow
-            v-for="(item, i) in unitData.content.value"
-            titleColor="title-text"
-            :border="false"
-            fixSize
-            :key="i"
-            :title="item.title"
-            :desc="item.desc"
-            :showImage="false"
-            :tags="item.bottomTags"
-          />
-        </FlexCol>
-      </SimplePageContentLoader>
-
-      <!-- 非遗传习所 -->
-      <HomeTitle title="非遗传习所" showMore @clickMore="navCommonList({
-        title: '非遗传习所',
-        modelId: SeminarContent.modelId,
-        mainBodyColumnId: SeminarContent.mainBodyColumnId,
-        detailsPage: '/pages/inhert/seminar/details',
-      })" />
-      <SimplePageContentLoader :loader="seminarData">
-        <FlexCol overflow="visible">
-          <Box2LineImageRightShadow
-            v-for="(item, i) in seminarData.content.value"
-            titleColor="title-text"
-            fixSize
-            :key="i"
-            :title="item.title"
-            :desc="item.desc"
-            :image="item.image"
-            :tags="item.bottomTags"
-            @click="navTo('/pages/inhert/seminar/details', { id: item.id })"
-          />
-        </FlexCol>
-      </SimplePageContentLoader>
-
-      <!-- 非遗活动 -->
-      <HomeTitle title="非遗活动" showMore @clickMore="goActivityList" />
-      <SimplePageContentLoader :loader="activityData">
-        <FlexRow wrap align="stretch" justify="space-between" overflow="visible">
-          <Box2LineLargeImageUserShadow
-            v-for="(item, i) in activityData.content.value"
-            titleColor="title-text"
-            width="calc(50% - 10rpx)"
-            fixSize
-            :key="i"
-            :title="item.title"
-            :desc="item.desc"
-            :image="item.image"
-            @click="goActivityDetail(item)"
-          />
-        </FlexRow>
-      </SimplePageContentLoader>
-    </template>
-    <template v-else-if="activeIndex === 1">
-      <!-- 文物 -->
-      <HomeTitle title="文物古迹" showMore @clickMore="navTo('/pages/inhert/artifact/list')" />
-      <SimplePageContentLoader :loader="artifactData">
-        <FlexRow wrap align="stretch" justify="space-between" overflow="visible">
-          <Box2LineLargeImageUserShadow
-            v-for="(item, i) in artifactData.content.value"
-            width="calc(50% - 10rpx)"
-            titleColor="title-text"
-            fixSize
-            :key="i"
-            :title="item.title"
-            :image="item.image"
-            :tags="item.tags"
-            title1
-            @click="navTo('/pages/inhert/artifact/details', { id: item.id })"
-          />
-        </FlexRow>
-        <Touchable direction="row" center :padding="10" :gap="20" @click="navTo('/pages/inhert/artifact/list')">
-          <text>查看全部</text>
-          <Icon name="arrow-right" />
-        </Touchable>
-      </SimplePageContentLoader>
-    </template>
-
-    <Footer text="到底了~" />
-  </FlexCol>
-</template>
-
-<script setup lang="ts">
-import { ref } from 'vue';
-import { navTo } from '@/components/utils/PageAction';
-import { useSimpleDataLoader } from '@/common/composeabe/SimpleDataLoader';
-import { GetContentListParams } from '@/api/CommonContent';
-import { navCommonList, useHomeCommonCategoryBlock } from '../article/common/CommonContent';
-import SimplePageContentLoader from '@/common/components/SimplePageContentLoader.vue';
-import FlexCol from '@/components/layout/FlexCol.vue';
-import HomeTitle from '../parts/HomeTitle.vue';
-import Box2LineLargeImageUserShadow from '../parts/Box2LineLargeImageUserShadow.vue';
-import Box2LineImageRightShadow from '../parts/Box2LineImageRightShadow.vue';
-import InheritorContent from '@/api/inheritor/InheritorContent';
-import ProjectsContent from '@/api/inheritor/ProjectsContent';
-import UnitContent from '@/api/inheritor/UnitContent';
-import SeminarContent from '@/api/inheritor/SeminarContent';
-import FlexRow from '@/components/layout/FlexRow.vue';
-import UnmoveableContent from '@/api/inheritor/UnmoveableContent';
-import Footer from '@/components/display/Footer.vue';
-import Tabs from '@/components/nav/Tabs.vue';
-import Touchable from '@/components/feedback/Touchable.vue';
-import Icon from '@/components/basic/Icon.vue';
-
-const activeIndex = ref(0);
-
-const intangibleData = useSimpleDataLoader(async () => 
-  (await ProjectsContent.getContentList(new GetContentListParams(), 1, 4)).list.map(p => ({
-    id: p.id,
-    title: p.title, 
-    desc: p.desc, 
-    image: p.thumbnail || p.image,
-    bottomTags: [
-      p.levelText, 
-      p.ichTypeText, 
-      p.batchText,
-      p.regionText,
-    ] as string[],
-  }))
-);
-const inheritorData = useSimpleDataLoader(async () => 
-  (await InheritorContent.getContentList(new GetContentListParams(), 2, 4)).list.map(p => ({
-    id: p.id,
-    title: p.title, 
-    desc: p.desc, 
-    image: p.thumbnail || p.image,
-    titleBox: Boolean(p.deathBirth),
-    bottomTags: [
-      p.levelText, 
-      p.nation,
-      p.ichName
-    ] as string[],
-  }))
-);
-const unitData = useSimpleDataLoader(async () => 
-  (await UnitContent.getContentList(new GetContentListParams(), 1, 4)).list.map(p => ({
-    id: p.id,
-    title: p.title, 
-    desc: '', 
-    image: p.thumbnail || p.image,
-    bottomTags: [
-      p.levelText, 
-      p.ichTypeText, 
-      p.batchText,
-      p.regionText,
-    ] as string[],
-  }))
-);
-const seminarData = useSimpleDataLoader(async () => 
-  (await SeminarContent.getContentList(new GetContentListParams(), 1, 4)).list.map(p => ({
-    id: p.id,
-    title: p.title, 
-    desc: p.desc, 
-    image: p.thumbnail || p.image,
-    bottomTags: [
-      p.levelText, 
-      p.ichTypeText, 
-      p.batchText,
-      p.regionText,
-    ] as string[],
-  }))
-);
-const artifactData = useSimpleDataLoader(async () => 
-  (await UnmoveableContent.getContentList(new GetContentListParams(), 1, 16)).list.map(p => ({
-    id: p.id,
-    title: p.title, 
-    desc: '', 
-    image: p.thumbnail || p.image,
-    likes: p.likes,
-    tags: [p.levelText, p.district] as string[],
-    comment: p.comments,
-  }))
-);
-
-const {
-  loader: activityData,
-  goList: goActivityList,
-  goDetail: goActivityDetail,
-} = useHomeCommonCategoryBlock({
-  title: '非遗活动',
-  mainBodyColumnId: 290,
-  modelId: 18,
-  itemType: 'article-common',
-  detailsPage: '/pages/article/details',
-});
-</script>