Эх сурвалжийг харах

!26 接口层兼容
Merge pull request !26 from Bluemark/vue3_tmp

芋道源码 1 жил өмнө
parent
commit
82407aa87c

+ 0 - 20
.env

@@ -1,20 +0,0 @@
-# 版本号
-SHOPRO_VERSION = v1.8.3
-
-# 正式环境接口域名
-SHOPRO_BASE_URL = https://api.shopro.sheepjs.com
-
-# 开发环境接口域名
-SHOPRO_DEV_BASE_URL = https://api.shopro.sheepjs.com
-
-# 开发环境运行端口
-SHOPRO_DEV_PORT = 3000
-
-# 接口地址前缀
-SHOPRO_API_PATH = /shop/api/
-
-# 客户端静态资源地址 空=默认使用服务端指定的CDN资源地址前缀 | local=本地  |  http(s)://xxx.xxx=自定义静态资源地址前缀
-SHOPRO_STATIC_URL = https://file.sheepjs.com
-
-# 是否开启直播  1 开启直播 | 0 关闭直播 (小程序官方后台未审核开通直播权限时请勿开启)
-SHOPRO_MPLIVE_ON = 0

+ 21 - 21
LICENSE

@@ -1,21 +1,21 @@
-MIT License
-
-Copyright (c) 2022 lidongtony
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+MIT License
+
+Copyright (c) 2022 lidongtony
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 11 - 1
pages/coupon/list.vue

@@ -148,11 +148,20 @@
 		});
 		});
 		if (res.code === 0) {
 		if (res.code === 0) {
 			// 拦截修改数据
 			// 拦截修改数据
+			let obj2 = {
+				2: '折扣',
+				1: '满减'
+			}
 			let obj = {
 			let obj = {
 				1: '可用',
 				1: '可用',
 				2: '已用',
 				2: '已用',
 				3: '过期'
 				3: '过期'
 			}
 			}
+			let obj3 = {
+				1: '已领取',
+				2: '已使用',
+				3: '已过期'
+			}
 			res.data.list = res.data.list.map(item => {
 			res.data.list = res.data.list.map(item => {
 				return {
 				return {
 					...item,
 					...item,
@@ -160,7 +169,8 @@
 					amount: (item.discountPrice / 100).toFixed(2),
 					amount: (item.discountPrice / 100).toFixed(2),
 					use_start_time: sheep.$helper.timeFormat(item.validStartTime, 'yyyy-mm-dd hh:MM:ss'),
 					use_start_time: sheep.$helper.timeFormat(item.validStartTime, 'yyyy-mm-dd hh:MM:ss'),
 					use_end_time: sheep.$helper.timeFormat(item.validEndTime, 'yyyy-mm-dd hh:MM:ss'),
 					use_end_time: sheep.$helper.timeFormat(item.validEndTime, 'yyyy-mm-dd hh:MM:ss'),
-					status_text: obj[item.status]
+					status_text: obj[item.status],
+					type_text: obj2[item.discountType]
 				}
 				}
 			});
 			});
 			if (page >= 2) {
 			if (page >= 2) {

+ 149 - 155
pages/goods/components/detail/detail-tabbar.vue

@@ -1,171 +1,165 @@
 <template>
 <template>
-  <su-fixed bottom placeholder bg="bg-white">
-    <view class="ui-tabbar-box">
-      <view class="ui-tabbar ss-flex ss-col-center ss-row-between">
-        <view
-          v-if="collectIcon"
-          class="detail-tabbar-item ss-flex ss-flex-col ss-row-center ss-col-center"
-          @tap="onFavorite"
-        >
-          <block v-if="modelValue.favorite">
-            <image
-              class="item-icon"
-              :src="sheep.$url.static('/static/img/shop/goods/collect_1.gif')"
-              mode="aspectFit"
-            ></image>
-            <view class="item-title">已收藏</view>
-          </block>
-          <block v-else>
-            <image
-              class="item-icon"
-              :src="sheep.$url.static('/static/img/shop/goods/collect_0.png')"
-              mode="aspectFit"
-            ></image>
-            <view class="item-title">收藏</view>
-          </block>
-        </view>
-        <view
-          v-if="serviceIcon"
-          class="detail-tabbar-item ss-flex ss-flex-col ss-row-center ss-col-center"
-          @tap="onChat"
-        >
-          <image
-            class="item-icon"
-            :src="sheep.$url.static('/static/img/shop/goods/message.png')"
-            mode="aspectFit"
-          ></image>
-          <view class="item-title">客服</view>
-        </view>
-        <view
-          v-if="shareIcon"
-          class="detail-tabbar-item ss-flex ss-flex-col ss-row-center ss-col-center"
-          @tap="showShareModal"
-        >
-          <image
-            class="item-icon"
-            :src="sheep.$url.static('/static/img/shop/goods/share.png')"
-            mode="aspectFit"
-          ></image>
-          <view class="item-title">分享</view>
-        </view>
-        <slot></slot>
-      </view>
-    </view>
-  </su-fixed>
+	<su-fixed bottom placeholder bg="bg-white">
+		<view class="ui-tabbar-box">
+			<view class="ui-tabbar ss-flex ss-col-center ss-row-between">
+				<view v-if="collectIcon" class="detail-tabbar-item ss-flex ss-flex-col ss-row-center ss-col-center"
+					@tap="onFavorite">
+					<block v-if="modelValue.favorite">
+						<image class="item-icon" :src="sheep.$url.static('/static/img/shop/goods/collect_1.gif')"
+							mode="aspectFit"></image>
+						<view class="item-title">已收藏</view>
+					</block>
+					<block v-else>
+						<image class="item-icon" :src="sheep.$url.static('/static/img/shop/goods/collect_0.png')"
+							mode="aspectFit"></image>
+						<view class="item-title">收藏</view>
+					</block>
+				</view>
+				<view v-if="serviceIcon" class="detail-tabbar-item ss-flex ss-flex-col ss-row-center ss-col-center"
+					@tap="onChat">
+					<image class="item-icon" :src="sheep.$url.static('/static/img/shop/goods/message.png')"
+						mode="aspectFit"></image>
+					<view class="item-title">客服</view>
+				</view>
+				<view v-if="shareIcon" class="detail-tabbar-item ss-flex ss-flex-col ss-row-center ss-col-center"
+					@tap="showShareModal">
+					<image class="item-icon" :src="sheep.$url.static('/static/img/shop/goods/share.png')"
+						mode="aspectFit"></image>
+					<view class="item-title">分享</view>
+				</view>
+				<slot></slot>
+			</view>
+		</view>
+	</su-fixed>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-  /**
-   *
-   * 底部导航
-   *
-   * @property {String} bg 			 			- 背景颜色Class
-   * @property {String} ui 			 			- 自定义样式Class
-   * @property {Boolean} noFixed 		 			- 是否定位
-   * @property {Boolean} topRadius 		 		- 上圆角
-   *
-   *
-   */
+	/**
+	 *
+	 * 底部导航
+	 *
+	 * @property {String} bg 			 			- 背景颜色Class
+	 * @property {String} ui 			 			- 自定义样式Class
+	 * @property {Boolean} noFixed 		 			- 是否定位
+	 * @property {Boolean} topRadius 		 		- 上圆角
+	 *
+	 *
+	 */
 
 
-  import { computed, reactive } from 'vue';
-  import sheep from '@/sheep';
-  import { showShareModal } from '@/sheep/hooks/useModal';
+	import {
+		computed,
+		reactive
+	} from 'vue';
+	import sheep from '@/sheep';
+	import {
+		showShareModal
+	} from '@/sheep/hooks/useModal';
 
 
-  // 数据
-  const state = reactive({});
+	// 数据
+	const state = reactive({});
 
 
-  // 接收参数
-  const props = defineProps({
-    modelValue: {
-      type: Object,
-      default() {},
-    },
-    bg: {
-      type: String,
-      default: 'bg-white',
-    },
-    bgStyles: {
-      type: Object,
-      default() {},
-    },
-    ui: {
-      type: String,
-      default: '',
-    },
+	// 接收参数
+	const props = defineProps({
+		modelValue: {
+			type: Object,
+			default () {},
+		},
+		bg: {
+			type: String,
+			default: 'bg-white',
+		},
+		bgStyles: {
+			type: Object,
+			default () {},
+		},
+		ui: {
+			type: String,
+			default: '',
+		},
 
 
-    noFixed: {
-      type: Boolean,
-      default: false,
-    },
-    topRadius: {
-      type: Number,
-      default: 0,
-    },
-    collectIcon: {
-      type: Boolean,
-      default: true,
-    },
-    serviceIcon: {
-      type: Boolean,
-      default: true,
-    },
-    shareIcon: {
-      type: Boolean,
-      default: true,
-    },
-  });
-  const elStyles = computed(() => {
-    return {
-      'border-top-left-radius': props.topRadius + 'rpx',
-      'border-top-right-radius': props.topRadius + 'rpx',
-      overflow: 'hidden',
-    };
-  });
+		noFixed: {
+			type: Boolean,
+			default: false,
+		},
+		topRadius: {
+			type: Number,
+			default: 0,
+		},
+		collectIcon: {
+			type: Boolean,
+			default: true,
+		},
+		serviceIcon: {
+			type: Boolean,
+			default: true,
+		},
+		shareIcon: {
+			type: Boolean,
+			default: true,
+		},
+	});
+	const elStyles = computed(() => {
+		return {
+			'border-top-left-radius': props.topRadius + 'rpx',
+			'border-top-right-radius': props.topRadius + 'rpx',
+			overflow: 'hidden',
+		};
+	});
 
 
-  const tabbarheight = (e) => {
-    uni.setStorageSync('tabbar', e);
-  };
-  async function onFavorite() {
-    const { error } = await sheep.$api.user.favorite.do(props.modelValue.id);
-    if (error === 0) {
-      if (props.modelValue.favorite) {
-        props.modelValue.favorite = 0;
-      } else {
-        props.modelValue.favorite = 1;
-      }
-    }
-  }
+	const tabbarheight = (e) => {
+		uni.setStorageSync('tabbar', e);
+	};
+	async function onFavorite() {
+		// const { error } = await sheep.$api.user.favorite.do(props.modelValue.id);
+		// if (error === 0) {
+		//   if (props.modelValue.favorite) {
+		//     props.modelValue.favorite = 0;
+		//   } else {
+		//     props.modelValue.favorite = 1;
+		//   }
+		// }
+		let data;
+		if (props.modelValue.favorite) {
+			data = await sheep.$api.user.favorite.dos(props.modelValue.id);
+		} else {
+			data = await sheep.$api.user.favorite.do(props.modelValue.id);
+		}
+		if (data.data) {
+			props.modelValue.favorite = !props.modelValue.favorite;
+		}
+	}
 
 
-  const onChat = () => {
-    sheep.$router.go('/pages/chat/index', {
-      id: props.modelValue.id,
-    });
-  };
+	const onChat = () => {
+		sheep.$router.go('/pages/chat/index', {
+			id: props.modelValue.id,
+		});
+	};
 </script>
 </script>
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
-  .ui-tabbar-box {
-    box-shadow: 0px -6px 10px 0px rgba(51, 51, 51, 0.2);
-  }
-  .ui-tabbar {
-    display: flex;
-    height: 50px;
-    background: #fff;
+	.ui-tabbar-box {
+		box-shadow: 0px -6px 10px 0px rgba(51, 51, 51, 0.2);
+	}
 
 
-    .detail-tabbar-item {
-      width: 100rpx;
+	.ui-tabbar {
+		display: flex;
+		height: 50px;
+		background: #fff;
 
 
-      .item-icon {
-        width: 40rpx;
-        height: 40rpx;
-      }
+		.detail-tabbar-item {
+			width: 100rpx;
 
 
-      .item-title {
-        font-size: 20rpx;
-        font-weight: 500;
-        line-height: 20rpx;
-        margin-top: 12rpx;
-      }
-    }
-  }
-</style>
+			.item-icon {
+				width: 40rpx;
+				height: 40rpx;
+			}
+
+			.item-title {
+				font-size: 20rpx;
+				font-weight: 500;
+				line-height: 20rpx;
+				margin-top: 12rpx;
+			}
+		}
+	}
+</style>

+ 98 - 20
pages/goods/index.vue

@@ -12,9 +12,7 @@
 			<block v-else>
 			<block v-else>
 				<view class="detail-swiper-selector">
 				<view class="detail-swiper-selector">
 					<!-- 商品轮播图  -->
 					<!-- 商品轮播图  -->
-					<su-swiper class="ss-m-b-14" isPreview :list="formatGoodsSwiper(state.goodsInfo.sliderPicUrls)"
-                     dotStyle="tag" imageMode="widthFix" dotCur="bg-mask-40" :seizeHeight="750" />
-
+					<su-swiper class="ss-m-b-14" isPreview :list="formatGoodsSwiper(state.goodsInfo.sliderPicUrls)" dotStyle="tag" imageMode="widthFix" dotCur="bg-mask-40" :seizeHeight="750" />
 					<!-- 价格+标题 -->
 					<!-- 价格+标题 -->
 					<view class="title-card detail-card ss-p-y-40 ss-p-x-20">
 					<view class="title-card detail-card ss-p-y-40 ss-p-x-20">
 						<view class="ss-flex ss-row-between ss-col-center ss-m-b-26">
 						<view class="ss-flex ss-row-between ss-col-center ss-m-b-26">
@@ -31,6 +29,8 @@
 							</view>
 							</view>
 						</view>
 						</view>
 						<view class="discounts-box ss-flex ss-row-between ss-m-b-28">
 						<view class="discounts-box ss-flex ss-row-between ss-m-b-28">
+							<!-- 满减送/限时折扣活动的提示 TODO 芋艿:promos 未写 -->
+							<div class="tag-content">
               <!-- 满减送/限时折扣活动的提示 TODO 芋艿:promos 未写 -->
               <!-- 满减送/限时折扣活动的提示 TODO 芋艿:promos 未写 -->
               <div class="tag-content">
               <div class="tag-content">
 								<view class="tag-box ss-flex">
 								<view class="tag-box ss-flex">
@@ -39,8 +39,7 @@
 									</view>
 									</view>
 								</view>
 								</view>
 							</div>
 							</div>
-
-              <!-- 优惠劵 -->
+							<!-- 优惠劵 -->
 							<view class="get-coupon-box ss-flex ss-col-center ss-m-l-20" @tap="state.showModel = true"
 							<view class="get-coupon-box ss-flex ss-col-center ss-m-l-20" @tap="state.showModel = true"
 								v-if="state.couponInfo.length">
 								v-if="state.couponInfo.length">
 								<view class="discounts-title ss-m-r-8">领券</view>
 								<view class="discounts-title ss-m-r-8">领券</view>
@@ -54,8 +53,8 @@
 					<!-- 功能卡片 -->
 					<!-- 功能卡片 -->
 					<view class="detail-cell-card detail-card ss-flex-col">
 					<view class="detail-cell-card detail-card ss-flex-col">
 						<detail-cell-sku v-model="state.selectedSku.goods_sku_text" :sku="state.selectedSku"
 						<detail-cell-sku v-model="state.selectedSku.goods_sku_text" :sku="state.selectedSku"
-                             @tap="state.showSelectSku = true" />
-            <!-- TODO 芋艿:可能暂时不考虑使用 -->
+							@tap="state.showSelectSku = true" />
+						<!-- TODO 芋艿:可能暂时不考虑使用 -->
 						<detail-cell-service v-if="state.goodsInfo.service" v-model="state.goodsInfo.service" />
 						<detail-cell-service v-if="state.goodsInfo.service" v-model="state.goodsInfo.service" />
 						<detail-cell-params v-if="state.goodsInfo.params" v-model="state.goodsInfo.params" />
 						<detail-cell-params v-if="state.goodsInfo.params" v-model="state.goodsInfo.params" />
 					</view>
 					</view>
@@ -87,12 +86,13 @@
 						<button class="ss-reset-button disabled-btn" disabled> 已售罄 </button>
 						<button class="ss-reset-button disabled-btn" disabled> 已售罄 </button>
 					</view>
 					</view>
 				</detail-tabbar>
 				</detail-tabbar>
+				<!-- 优惠劵弹窗 -->
+				<s-coupon-get v-model="state.couponInfo" :show="state.showModel" @close="state.showModel = false" @get="onGet" />
 
 
-        <!-- 优惠劵弹窗 -->
-				<s-coupon-get v-model="state.couponInfo" :show="state.showModel" @close="state.showModel = false"
-                      @get="onGet" />
-
-        <!-- 满减送/限时折扣活动弹窗 -->
+				<!-- 满减送/限时折扣活动弹窗 -->
+        		<!-- 优惠劵弹窗 -->
+				<s-coupon-get v-model="state.couponInfo" :show="state.showModel" @close="state.showModel = false" @get="onGet" />
+       			 <!-- 满减送/限时折扣活动弹窗 -->
 				<s-activity-pop v-model="state.activityInfo" :show="state.showActivityModel"
 				<s-activity-pop v-model="state.activityInfo" :show="state.showActivityModel"
                         @close="state.showActivityModel = false" />
                         @close="state.showActivityModel = false" />
 			</block>
 			</block>
@@ -104,9 +104,16 @@
 	import { reactive, computed } from 'vue';
 	import { reactive, computed } from 'vue';
 	import { onLoad, onPageScroll } from '@dcloudio/uni-app';
 	import { onLoad, onPageScroll } from '@dcloudio/uni-app';
 	import sheep from '@/sheep';
 	import sheep from '@/sheep';
-  import CouponApi from '@/sheep/api/promotion/coupon';
-  import ActivityApi from '@/sheep/api/promotion/activity';
-  import { formatSales, formatGoodsSwiper, fen2yuan, } from '@/sheep/hooks/useGoods';
+	import CouponApi from '@/sheep/api/promotion/coupon';
+	import ActivityApi from '@/sheep/api/promotion/activity';
+	import {
+		formatSales,
+		formatGoodsSwiper,
+		fen2yuan,
+	} from '@/sheep/hooks/useGoods';
+    import CouponApi from '@/sheep/api/promotion/coupon';
+    import ActivityApi from '@/sheep/api/promotion/activity';
+    import { formatSales, formatGoodsSwiper, fen2yuan, } from '@/sheep/hooks/useGoods';
 	import detailNavbar from './components/detail/detail-navbar.vue';
 	import detailNavbar from './components/detail/detail-navbar.vue';
 	import detailCellSku from './components/detail/detail-cell-sku.vue';
 	import detailCellSku from './components/detail/detail-cell-sku.vue';
 	import detailCellService from './components/detail/detail-cell-service.vue';
 	import detailCellService from './components/detail/detail-cell-service.vue';
@@ -130,16 +137,17 @@
 		couponInfo: [], // 可领取的 Coupon 优惠劵的列表
 		couponInfo: [], // 可领取的 Coupon 优惠劵的列表
 		showActivityModel: false, // 【满减送/限时折扣】是否展示 Activity 营销活动的弹窗
 		showActivityModel: false, // 【满减送/限时折扣】是否展示 Activity 营销活动的弹窗
 		activityInfo: [], // 【满减送/限时折扣】可参与的 Activity 营销活动的列表
 		activityInfo: [], // 【满减送/限时折扣】可参与的 Activity 营销活动的列表
-    activityList: [], // 【秒杀/拼团/砍价】可参与的 Activity 营销活动的列表
-	});
+		activityList: [], // 【秒杀/拼团/砍价】可参与的 Activity 营销活动的列表
+ 	});
 
 
 	// 规格变更
 	// 规格变更
 	function onSkuChange(e) {
 	function onSkuChange(e) {
 		state.selectedSku = e;
 		state.selectedSku = e;
 	}
 	}
 
 
-	// 添加购物车
+	// 添加购物车  TODO 芋艿:待测试
 	function onAddCart(e) {
 	function onAddCart(e) {
+		console.log(e, '加入购物车');
 		sheep.$store('cart').add(e);
 		sheep.$store('cart').add(e);
 	}
 	}
 
 
@@ -166,10 +174,10 @@
 	// 立即领取  TODO 芋艿:待测试
 	// 立即领取  TODO 芋艿:待测试
 	async function onGet(id) {
 	async function onGet(id) {
 		const {
 		const {
-			error,
+			code,
 			msg
 			msg
 		} = await sheep.$api.coupon.get(id);
 		} = await sheep.$api.coupon.get(id);
-		if (error === 0) {
+		if (code === 0) {
 			uni.showToast({
 			uni.showToast({
 				title: msg,
 				title: msg,
 			});
 			});
@@ -180,6 +188,7 @@
 	}
 	}
 
 
   //  TODO 芋艿:待测试
   //  TODO 芋艿:待测试
+
 	const shareInfo = computed(() => {
 	const shareInfo = computed(() => {
 		if (isEmpty(state.goodsInfo)) return {};
 		if (isEmpty(state.goodsInfo)) return {};
 		return sheep.$platform.share.getShareInfo({
 		return sheep.$platform.share.getShareInfo({
@@ -207,6 +216,75 @@
 		}
 		}
 		state.goodsId = options.id;
 		state.goodsId = options.id;
 		// 1. 加载商品信息
 		// 1. 加载商品信息
+		sheep.$api.goods.detail(state.goodsId).then(async (res) => {
+			// 未找到商品
+			if (res.code !== 0 || !res.data) {
+				state.goodsInfo = null;
+				return;
+			}
+			// 加载到商品
+			state.skeletonLoading = false;
+			// 获取收藏信息
+			let dasa = await sheep.$api.goods.exits(options.id);
+			res.data.favorite = dasa.data;
+			state.goodsInfo = res.data;
+			console.log(state.goodsInfo, '商品信息');
+
+			// 此处调试默认弹出可以修改为点击弹出
+			// 2. 加载优惠劵信息
+			CouponApi.getCouponTemplateList({
+				price: state.goodsInfo.price,
+				spuIds: [state.goodsInfo.id],
+				skuIds: state.goodsInfo.skus.map(item => item.id),
+				// 先写死
+				categoryIds: [52]
+			}).then((res) => {
+				console.log(res, '优惠券信息进行对接')
+				if (res.code !== 0) {
+					return;
+				}
+				// 拦截修改数据
+				let obj2 = {
+					2: '折扣',
+					1: '满减'
+				}
+				let obj = {
+					1: '可用',
+					2: '已用',
+					3: '过期'
+				}
+				let obj3 = {
+					1: '已领取',
+					2: '已使用',
+					3: '已过期'
+				}
+				res.data = res.data.map(item => {
+					return {
+						...item,
+						enough: (item.usePrice / 100).toFixed(2),
+						amount: (item.discountPrice / 100).toFixed(2),
+						use_start_time: sheep.$helper.timeFormat(item
+							.validStartTime,
+							'yyyy-mm-dd hh:MM:ss'),
+						use_end_time: sheep.$helper.timeFormat(item.validEndTime,
+							'yyyy-mm-dd hh:MM:ss'),
+						status_text: obj[item.status],
+						type_text: obj2[item.discountType],
+						get_status_text: obj3[item.status],
+						type_text: obj2[item.discountType]
+					}
+				});
+				state.couponInfo = res.data;
+			});
+		});
+		// return;
+		// 3. 加载营销活动信息
+		ActivityApi.getActivityListBySpuId(state.goodsId).then((res) => {
+			if (res.code !== 0) {
+				return;
+			}
+			state.activityList = res.data;
+		});
 		sheep.$api.goods.detail(state.goodsId).then((res) => {
 		sheep.$api.goods.detail(state.goodsId).then((res) => {
       // 未找到商品
       // 未找到商品
       if (res.code !== 0 || !res.data) {
       if (res.code !== 0 || !res.data) {

+ 31 - 30
pages/index/user.vue

@@ -1,41 +1,42 @@
 <template>
 <template>
-  <s-layout
-    title="我的"
-    tabbar="/pages/index/user"
-    navbar="custom"
-    :bgStyle="template.page"
-    :navbarStyle="template.style?.navbar"
-    onShareAppMessage
-  >
-    <s-block v-for="(item, index) in template.components" :key="index" :styles="item.property.style">
-      <s-block-item :type="item.id" :data="item.property" :styles="item.property.style" />
-    </s-block>
-  </s-layout>
+
+	<s-layout title="我的" tabbar="/pages/index/user" navbar="custom" :bgStyle="template.page"
+		:navbarStyle="template.style?.navbar" onShareAppMessage>
+		<s-block v-for="(item, index) in template.components" :key="index" :styles="item.property.style">
+			<s-block-item :type="item.id" :data="item.property" :styles="item.property.style" />
+		</s-block>
+	</s-layout>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-  import { computed } from 'vue';
-  import { onShow, onPageScroll, onPullDownRefresh } from '@dcloudio/uni-app';
-  import sheep from '@/sheep';
+	import {
+		computed
+	} from 'vue';
+	import {
+		onShow,
+		onPageScroll,
+		onPullDownRefresh
+	} from '@dcloudio/uni-app';
+	import sheep from '@/sheep';
 
 
-  // 隐藏原生tabBar
-  uni.hideTabBar();
+	// 隐藏原生tabBar
+	uni.hideTabBar();
 
 
-  const template = computed(() => sheep.$store('app').template.user);
-  const isLogin = computed(() => sheep.$store('user').isLogin);
+	const template = computed(() => sheep.$store('app').template.user);
+	const isLogin = computed(() => sheep.$store('user').isLogin);
 
 
-  onShow(() => {
-    sheep.$store('user').updateUserData();
-  });
+	onShow(() => {
+		sheep.$store('user').updateUserData();
+	});
 
 
-  onPullDownRefresh(() => {
-    sheep.$store('user').updateUserData();
-    setTimeout(function () {
-      uni.stopPullDownRefresh();
-    }, 800);
-  });
+	onPullDownRefresh(() => {
+		sheep.$store('user').updateUserData();
+		setTimeout(function() {
+			uni.stopPullDownRefresh();
+		}, 800);
+	});
 
 
-  onPageScroll(() => {});
+	onPageScroll(() => {});
 </script>
 </script>
 
 
-<style></style>
+<style></style>

+ 21 - 9
sheep/api/cart.js

@@ -1,10 +1,9 @@
 import request from '@/sheep/request';
 import request from '@/sheep/request';
-import request2 from '@/sheep/request2';
 
 
 export default {
 export default {
 	list: (data) =>
 	list: (data) =>
-		request2({
-			url: 'trade/cart/list',
+		request({
+			url: '/app-api/trade/cart/list',
 			method: 'GET',
 			method: 'GET',
 			custom: {
 			custom: {
 				showLoading: false,
 				showLoading: false,
@@ -13,7 +12,7 @@ export default {
 		}),
 		}),
 	append: (data) =>
 	append: (data) =>
 		request({
 		request({
-			url: 'cart',
+			url: '/app-api/trade/cart/add',
 			method: 'POST',
 			method: 'POST',
       // TODO 芋艿:这里没提示
       // TODO 芋艿:这里没提示
 			custom: {
 			custom: {
@@ -22,18 +21,31 @@ export default {
 			},
 			},
 			data: {
 			data: {
 				...data,
 				...data,
-				type: 'inc',
+				// type: 'inc',
 			},
 			},
 		}),
 		}),
+	// append: (data) =>
+	// 	request({
+	// 		url: 'cart',
+	// 		method: 'POST',
+	// 		custom: {
+	// 			showSuccess: true,
+	// 			successMsg: '已添加到购物车~',
+	// 		},
+	// 		data: {
+	// 			...data,
+	// 			type: 'inc',
+	// 		},
+	// 	}),
 	// 删除购物车
 	// 删除购物车
 	delete: (ids) =>
 	delete: (ids) =>
-		request2({
-			url: 'trade/cart/delete?ids=' + ids,
+		request({
+			url: '/app-api/trade/cart/delete?ids=' + ids,
 			method: 'DELETE',
 			method: 'DELETE',
 		}),
 		}),
 	update: (data) =>
 	update: (data) =>
-		request2({
-			url: 'trade/cart/update-count',
+		request({
+			url: '/app-api/trade/cart/update-count',
 			method: 'PUT',
 			method: 'PUT',
 			data: {
 			data: {
 				...data,
 				...data,

+ 7 - 9
sheep/api/category.js

@@ -1,10 +1,8 @@
-import request2 from '@/sheep/request2';
-
 export default {
 export default {
-  list: (params) =>
-    request2({
-      url: 'product/category/list',
-      method: 'GET',
-      params,
-    }),
-};
+	list: (params) =>
+		request({
+			url: '/app-api/product/category/list',
+			method: 'GET',
+			params,
+		}),
+};

+ 14 - 4
sheep/api/coupon.js

@@ -1,5 +1,4 @@
 import request from '@/sheep/request';
 import request from '@/sheep/request';
-import request2 from '@/sheep/request2';
 
 
 export default {
 export default {
 	// 我的拼团
 	// 我的拼团
@@ -13,8 +12,8 @@ export default {
 			},
 			},
 		}),
 		}),
 	userCoupon: (params) =>
 	userCoupon: (params) =>
-		request2({
-			url: 'promotion/coupon/page',
+		request({
+			url: '/app-api/promotion/coupon/page',
 			method: 'GET',
 			method: 'GET',
 			params,
 			params,
 		}),
 		}),
@@ -34,9 +33,20 @@ export default {
 		}),
 		}),
 	get: (id) =>
 	get: (id) =>
 		request({
 		request({
-			url: 'coupon/get/' + id,
+			url: '/app-api/promotion/coupon/take',
 			method: 'POST',
 			method: 'POST',
+			data: {
+				templateId: id
+			},
+			params: {
+				templateId: id
+			},
 		}),
 		}),
+	// get: (id) =>
+	// 	request({
+	// 		url: 'coupon/get/' + id,
+	// 		method: 'POST',
+	// 	}),
 	listByGoods: (id) =>
 	listByGoods: (id) =>
 		request({
 		request({
 			url: 'coupon/listByGoods/' + id,
 			url: 'coupon/listByGoods/' + id,

+ 21 - 22
sheep/api/data.js

@@ -1,25 +1,24 @@
 import request from '@/sheep/request';
 import request from '@/sheep/request';
-import request2 from '@/sheep/request2';
 
 
 export default {
 export default {
-  area: () =>
-    request2({
-      url: 'system/area/tree',
-      method: 'GET',
-    }),
-  // area: () =>
-  //   request({
-  //     url: 'data/area',
-  //     method: 'GET',
-  //   }),
-  faq: () =>
-    request({
-      url: 'data/faq',
-      method: 'GET',
-    }),
-  richtext: (id) =>
-    request({
-      url: 'data/richtext/' + id,
-      method: 'GET',
-    }),
-};
+	area: () =>
+		request({
+			url: '/app-api/system/area/tree',
+			method: 'GET',
+		}),
+	// area: () =>
+	//   request({
+	//     url: 'data/area',
+	//     method: 'GET',
+	//   }),
+	faq: () =>
+		request({
+			url: 'data/faq',
+			method: 'GET',
+		}),
+	richtext: (id) =>
+		request({
+			url: 'data/richtext/' + id,
+			method: 'GET',
+		}),
+};

+ 79 - 75
sheep/api/goods.js

@@ -1,80 +1,84 @@
 import request from '@/sheep/request';
 import request from '@/sheep/request';
-import request2 from '@/sheep/request2';
 
 
 export default {
 export default {
-  // 商品详情
-  detail: (id, params = {}) =>
-    request2({
-      url: 'product/spu/get-detail?id=' + id,
-      method: 'GET',
-      params,
-      custom: {
-        showLoading: false,
-        showError: false,
-      },
-    }),
+	// 商品详情
+	detail: (id, params = {}) =>
+		request({
+			url: '/app-api/product/spu/get-detail?id=' + id,
+			method: 'GET',
+			params,
+			custom: {
+				showLoading: false,
+				showError: false,
+			},
+		}),
 
 
-  // 商品列表
-  list: (params) =>
-    request2({
-      url: 'product/spu/page',
-      method: 'GET',
-      params,
-      custom: {
-        showLoading: false,
-        showError: false,
-      },
-    }),
+	// 商品列表
+	list: (params) =>
+		request({
+			url: '/app-api/product/spu/page',
+			method: 'GET',
+			params,
+			custom: {
+				showLoading: false,
+				showError: false,
+			},
+		}),
 
 
-  // 商品查询
-  ids: (params = {}) =>
-    request({
-      url: 'goods/goods/ids',
-      method: 'GET',
-      params,
-      custom: {
-        showLoading: false,
-        showError: false,
-      },
-    }),
-
-  // 商品评价列表
-  comment: (id, params = {}) =>
-    request2({
-      url: 'product/comment/list?spuId=' + id,
-      method: 'GET',
-      params,
-      custom: {
-        showLoading: false,
-        showError: false,
-      },
-    }),
-  // 商品评价类型
-  getType: (id) =>
-    request({
-      url: 'goods/comment/getType/' + id,
-      method: 'GET',
-      custom: {
-        showLoading: false,
-        showError: false,
-      },
-    }),
-  // 活动商品查询
-  // 商品查询
-  activity: (params = {}) =>
-    request({
-      url: 'goods/goods/activity',
-      method: 'GET',
-      params,
-      custom: {
-        showLoading: false,
-        showError: false,
-      },
-    }),
-  activityList: (params = {}) =>
-    request({
-      url: 'goods/goods/activityList',
-      method: 'GET',
-      params,
-    }),
-};
+	// 商品查询
+	ids: (params = {}) =>
+		request({
+			url: 'goods/goods/ids',
+			method: 'GET',
+			params,
+			custom: {
+				showLoading: false,
+				showError: false,
+			},
+		}),
+	// 商品评价列表
+	comment: (id, params = {}) =>
+		request({
+			url: '/app-api/product/comment/list?spuId=' + id,
+			method: 'GET',
+			params,
+			custom: {
+				showLoading: false,
+				showError: false,
+			},
+		}),
+	// 商品评价类型
+	getType: (id) =>
+		request({
+			url: 'goods/comment/getType/' + id,
+			method: 'GET',
+			custom: {
+				showLoading: false,
+				showError: false,
+			},
+		}),
+	// 活动商品查询
+	// 商品查询
+	activity: (params = {}) =>
+		request({
+			url: 'goods/goods/activity',
+			method: 'GET',
+			params,
+			custom: {
+				showLoading: false,
+				showError: false,
+			},
+		}),
+	activityList: (params = {}) =>
+		request({
+			url: 'goods/goods/activityList',
+			method: 'GET',
+			params,
+		}),
+	// 检查是否收藏商品
+	exits: (id) =>
+		request({
+			url: '/app-api/product/favorite/exits?spuId=' + id,
+			method: 'GET',
+		}),
+};

+ 12 - 12
sheep/api/index2.js

@@ -1,14 +1,14 @@
-import request2 from '@/sheep/request2';
+import request from '@/sheep/request';
 
 
 export default {
 export default {
-  decorate: () =>
-    request2({
-      url: 'promotion/decorate/list?page=1',
-      method: 'GET',
-    }),
-  spids: () =>
-    request2({
-      url: 'product/spu/page?recommendType=best&pageNo=1&pageSize=10',
-      method: 'GET',
-    }),
-};
+	decorate: () =>
+		request({
+			url: '/app-api/promotion/decorate/list?page=1',
+			method: 'GET',
+		}),
+	spids: () =>
+		request({
+			url: '/app-api/product/spu/page?recommendType=best&pageNo=1&pageSize=10',
+			method: 'GET',
+		}),
+};

+ 26 - 18
sheep/api/order.js

@@ -1,11 +1,10 @@
 import request from '@/sheep/request';
 import request from '@/sheep/request';
-import request2 from '@/sheep/request2';
 
 
 export default {
 export default {
 	// 订单详情
 	// 订单详情
 	detail: (id, params) =>
 	detail: (id, params) =>
-		request2({
-			url: 'trade/order/get-detail?id=' + id,
+		request({
+			url: '/app-api/trade/order/get-detail?id=' + id,
 			method: 'GET',
 			method: 'GET',
 			params,
 			params,
 		}),
 		}),
@@ -40,8 +39,8 @@ export default {
 		}),
 		}),
 	// 订单列表
 	// 订单列表
 	list: (params) =>
 	list: (params) =>
-		request2({
-			url: 'trade/order/page',
+		request({
+			url: '/app-api/trade/order/page',
 			method: 'GET',
 			method: 'GET',
 			params,
 			params,
 			custom: {
 			custom: {
@@ -65,16 +64,25 @@ export default {
 		// 解决 SpringMVC 接受 List<Item> 参数的问题
 		// 解决 SpringMVC 接受 List<Item> 参数的问题
 		delete data2.items
 		delete data2.items
 		for (let i = 0; i < data.items.length; i++) {
 		for (let i = 0; i < data.items.length; i++) {
+			// 此处转码问题,待解决方案
 			data2[encodeURIComponent('items[' + i + '' + '].skuId')] = data.items[i].skuId + '';
 			data2[encodeURIComponent('items[' + i + '' + '].skuId')] = data.items[i].skuId + '';
 			data2[encodeURIComponent('items[' + i + '' + '].count')] = data.items[i].count + '';
 			data2[encodeURIComponent('items[' + i + '' + '].count')] = data.items[i].count + '';
-      if (data.items[i].cartId) {
-        data2[encodeURIComponent('items[' + i + '' + '].cartId')] = data.items[i].cartId + '';
-      }
+			data2[encodeURIComponent('items[' + i + '' + '].cartId')] = data.items[i].cartId + '';
+
+			// data2['items' + `[${i}]` + '.skuId'] = data.items[i].skuId + '';
+			// data2['items' + `[${i}]` + '.count'] = data.items[i].count + '';
+			// data2['items' + `[${i}]` + '.cartId'] = data.items[i].cartId + '';
+
+			// data2['items' + `%5B${i}%5D` + '.skuId'] = data.items[i].skuId + '';
+			// data2['items' + `%5B${i}%5D` + '.count'] = data.items[i].count + '';
+			// data2['items' + `%5B${i}%5D` + '.cartId'] = data.items[i].cartId + '';
 		}
 		}
-    const queryString= Object.keys(data2).map(key => key + '=' + data2[key]).join('&')
-		return request2({
-			url: `trade/order/settlement?${queryString}`,
-			method: 'GET'
+		console.log(data2, '手动转码的参数')
+		return request({
+			url: '/app-api/trade/order/settlement',
+			method: 'GET',
+			// data: data2,
+			params: data2
 		})
 		})
 	},
 	},
 	// 创建订单
 	// 创建订单
@@ -99,8 +107,8 @@ export default {
 		}),
 		}),
 	// 评价订单
 	// 评价订单
 	comment: (data) =>
 	comment: (data) =>
-		request2({
-			url: 'trade/order/item/create-comment',
+		request({
+			url: '/app-api/trade/order/item/create-comment',
 			method: 'POST',
 			method: 'POST',
 			data,
 			data,
 		}),
 		}),
@@ -138,8 +146,8 @@ export default {
 				data,
 				data,
 			}),
 			}),
 		list: (params) =>
 		list: (params) =>
-			request2({
-				url: 'trade/after-sale/page',
+			request({
+				url: '/app-api/trade/after-sale/page',
 				method: 'GET',
 				method: 'GET',
 				params,
 				params,
 				custom: {
 				custom: {
@@ -169,8 +177,8 @@ export default {
 			}),
 			}),
 		// 售后详情
 		// 售后详情
 		detail: (id) =>
 		detail: (id) =>
-			request2({
-				url: 'trade/after-sale/get?id=' + id,
+			request({
+				url: '/app-api/trade/after-sale/get?id=' + id,
 				method: 'GET',
 				method: 'GET',
 			}),
 			}),
 	},
 	},

+ 15 - 14
sheep/api/product/comment.js

@@ -1,18 +1,19 @@
 import request from '@/sheep/request';
 import request from '@/sheep/request';
 
 
 const CommentApi = {
 const CommentApi = {
-  // 获得商品评价分页
-  getCommentPage: (spuId, pageNo, pageSize, type) => {
-    return request({
-      url: '/app-api/product/comment/page',
-      method: 'GET',
-      params: {
-        spuId,
-        pageNo,
-        pageSize,
-        type
-      },
-    });
-  },
+
+	// 获得商品评价分页
+	getCommentPage: (spuId, pageNo, pageSize, type) => {
+		return request({
+			url: '/app-api/product/comment/page',
+			method: 'GET',
+			params: {
+				spuId,
+				pageNo,
+				pageSize,
+				type
+			},
+		});
+	},
 };
 };
-export default CommentApi;
+export default CommentApi;

+ 11 - 11
sheep/api/promotion/activity.js

@@ -1,16 +1,16 @@
-import request2 from '@/sheep/request2';
+import request from '@/sheep/request';
 
 
 const ActivityApi = {
 const ActivityApi = {
-  // 获得单个商品,近期参与的每个活动
-  getActivityListBySpuId: (spuId) => {
-    return request2({
-      url: '/app-api/promotion/activity/list-by-spu-id',
-      method: 'GET',
-      params: {
-        spuId,
-      },
-    });
-  },
+	// 获得单个商品,近期参与的每个活动
+	getActivityListBySpuId: (spuId) => {
+		return request({
+			url: '/app-api/promotion/activity/list-by-spu-id',
+			method: 'GET',
+			params: {
+				spuId,
+			},
+		});
+	},
 };
 };
 
 
 export default ActivityApi;
 export default ActivityApi;

+ 59 - 57
sheep/api/promotion/combination.js

@@ -1,67 +1,69 @@
-import request2 from "@/sheep/request2";
+import request from "@/sheep/request";
 
 
 // 拼团 API
 // 拼团 API
 const CombinationApi = {
 const CombinationApi = {
-    // 获得拼团活动列表
-    getCombinationActivityList: (count) => {
-        return request2({
-            url: "promotion/combination-activity/list",
-            method: 'GET',
-            params: {count}
-        });
-    },
+	// 获得拼团活动列表
+	getCombinationActivityList: (count) => {
+		return request({
+			url: "/app-api/promotion/combination-activity/list",
+			method: 'GET',
+			params: {
+				count
+			}
+		});
+	},
 
 
-    // 获得拼团活动分页
-    getCombinationActivityPage: (params) => {
-        return request2({
-            url: "promotion/combination-activity/page",
-            method: 'GET',
-            params
-        });
-    },
+	// 获得拼团活动分页
+	getCombinationActivityPage: (params) => {
+		return request({
+			url: "/app-api/promotion/combination-activity/page",
+			method: 'GET',
+			params
+		});
+	},
 
 
-    // 获得拼团活动明细
-    getCombinationActivity: (id) => {
-        return request2({
-            url: "promotion/combination-activity/get-detail",
-            method: 'GET',
-            params: {
-                id
-            }
-        });
-    },
+	// 获得拼团活动明细
+	getCombinationActivity: (id) => {
+		return request({
+			url: "/app-api/promotion/combination-activity/get-detail",
+			method: 'GET',
+			params: {
+				id
+			}
+		});
+	},
 
 
-    // 获得最近 n 条拼团记录(团长发起的)
-    getHeadCombinationRecordList: (activityId, status, count) => {
-        return request2({
-            url: "promotion/combination-record/get-head-list",
-            method: 'GET',
-            params: {
-                activityId,
-                status,
-                count
-            }
-        });
-    },
+	// 获得最近 n 条拼团记录(团长发起的)
+	getHeadCombinationRecordList: (activityId, status, count) => {
+		return request({
+			url: "/app-api/promotion/combination-record/get-head-list",
+			method: 'GET',
+			params: {
+				activityId,
+				status,
+				count
+			}
+		});
+	},
 
 
-    // 获得拼团记录明细
-    getCombinationRecordDetail: (id) => {
-        return request2({
-            url: "promotion/combination-record/get-detail",
-            method: 'GET',
-            params: {
-                id
-            }
-        });
-    },
+	// 获得拼团记录明细
+	getCombinationRecordDetail: (id) => {
+		return request({
+			url: "/app-api/promotion/combination-record/get-detail",
+			method: 'GET',
+			params: {
+				id
+			}
+		});
+	},
 
 
-    // 获得拼团记录的概要信息
-    getCombinationRecordSummary: () => {
-        return request2({
-            url: "promotion/combination-record/get-summary",
-            method: 'GET',
-        });
-    }
+	// 获得拼团记录的概要信息
+	getCombinationRecordSummary: () => {
+		return request({
+			url: "/app-api/promotion/combination-record/get-summary",
+			method: 'GET',
+		});
+	}
 }
 }
 
 
-export default CombinationApi
+export default CombinationApi

+ 37 - 26
sheep/api/promotion/seckill.js

@@ -1,33 +1,44 @@
-import request2 from "@/sheep/request2";
+import request from "@/sheep/request";
 
 
 const SeckillApi = {
 const SeckillApi = {
-  // 获得秒杀时间段列表
-  getSeckillConfigList: () => {
-    return request2({ url: 'promotion/seckill-config/list', method: 'GET' });
-  },
+	// 获得秒杀时间段列表
+	getSeckillConfigList: () => {
+		return request({
+			url: '/app-api/promotion/seckill-config/list',
+			method: 'GET'
+		});
+	},
 
 
-  // 获得当前秒杀活动
-  getNowSeckillActivity: () => {
-    return request2({ url: 'promotion/seckill-activity/get-now', method: 'GET' });
-  },
+	// 获得当前秒杀活动
+	getNowSeckillActivity: () => {
+		return request({
+			url: '/app-api/promotion/seckill-activity/get-now',
+			method: 'GET'
+		});
+	},
 
 
-  // 获得秒杀活动分页
-  getSeckillActivityPage: () => {
-    return request2({ url: 'promotion/seckill-activity/page', method: 'GET' });
-  },
+	// 获得秒杀活动分页
+	getSeckillActivityPage: () => {
+		return request({
+			url: '/app-api/promotion/seckill-activity/page',
+			method: 'GET'
+		});
+	},
 
 
-  /**
-   * 获得秒杀活动明细
-   * @param {number} id 秒杀活动编号
-   * @return {*}
-   */
-  getSeckillActivity: (id) => {
-    return request2({
-      url: 'promotion/seckill-activity/get-detail',
-      method: 'GET',
-      params: { id }
-    });
-  }
+	/**
+	 * 获得秒杀活动明细
+	 * @param {number} id 秒杀活动编号
+	 * @return {*}
+	 */
+	getSeckillActivity: (id) => {
+		return request({
+			url: '/app-api/promotion/seckill-activity/get-detail',
+			method: 'GET',
+			params: {
+				id
+			}
+		});
+	}
 }
 }
 
 
-export default SeckillApi;
+export default SeckillApi;

+ 45 - 123
sheep/api/user.js

@@ -1,11 +1,10 @@
 import request from '@/sheep/request';
 import request from '@/sheep/request';
-import request2 from '@/sheep/request2';
 import $platform from '@/sheep/platform';
 import $platform from '@/sheep/platform';
 
 
 export default {
 export default {
 	getUnused: () =>
 	getUnused: () =>
-		request2({
-			url: 'promotion/coupon/get-unused-count',
+		request({
+			url: '/app-api/promotion/coupon/get-unused-count',
 			method: 'GET',
 			method: 'GET',
 			custom: {
 			custom: {
 				showLoading: false,
 				showLoading: false,
@@ -13,8 +12,8 @@ export default {
 			},
 			},
 		}),
 		}),
 	profile: () =>
 	profile: () =>
-		request2({
-			url: 'member/user/get',
+		request({
+			url: '/app-api/member/user/get',
 			method: 'GET',
 			method: 'GET',
 			custom: {
 			custom: {
 				showLoading: false,
 				showLoading: false,
@@ -22,7 +21,7 @@ export default {
 			},
 			},
 		}),
 		}),
 	balance: () =>
 	balance: () =>
-		request2({
+		request({
 			url: '/app-api/pay/wallet/get',
 			url: '/app-api/pay/wallet/get',
 			method: 'GET',
 			method: 'GET',
 			custom: {
 			custom: {
@@ -30,28 +29,9 @@ export default {
 				auth: true,
 				auth: true,
 			},
 			},
 		}),
 		}),
-	// profile: () =>
-	//   request({
-	//     url: '/user/api/user/profile',
-	//     method: 'GET',
-	//     custom: {
-	//       showLoading: false,
-	//       auth: true,
-	//     },
-	//   }),
-	// update: (data) =>
-	//   request({
-	//     url: '/user/api/user/update',
-	//     method: 'POST',
-	//     custom: {
-	//       showSuccess: true,
-	//       auth: true,
-	//     },
-	//     data,
-	//   }),
 	update: (data) =>
 	update: (data) =>
-		request2({
-			url: 'member/user/update',
+		request({
+			url: '/app-api/member/user/update',
 			method: 'PUT',
 			method: 'PUT',
 			custom: {
 			custom: {
 				showSuccess: true,
 				showSuccess: true,
@@ -196,90 +176,48 @@ export default {
 		}),
 		}),
 
 
 	address: {
 	address: {
-		// default: () =>
-		// 	request({
-		// 		url: 'user/address/default',
-		// 		method: 'GET',
-		// 		custom: {
-		// 			showError: false,
-		// 		},
-		// 	}),
 		default: () =>
 		default: () =>
-			request2({
-				url: 'member/address/get-default',
+			request({
+				url: '/app-api/member/address/get-default',
 				method: 'GET',
 				method: 'GET',
 				custom: {
 				custom: {
 					showError: false,
 					showError: false,
 				},
 				},
 			}),
 			}),
 		list: () =>
 		list: () =>
-			request2({
-				url: 'member/address/list',
+			request({
+				url: '/app-api/member/address/list',
 				method: 'GET',
 				method: 'GET',
 				custom: {},
 				custom: {},
 			}),
 			}),
-		// list: () =>
-		//   request({
-		//     url: 'user/address',
-		//     method: 'GET',
-		//     custom: {},
-		//   }),
 		create: (data) =>
 		create: (data) =>
-			request2({
-				url: 'member/address/create',
+			request({
+				url: '/app-api/member/address/create',
 				method: 'POST',
 				method: 'POST',
 				data,
 				data,
 				custom: {
 				custom: {
 					showSuccess: true,
 					showSuccess: true,
 				},
 				},
 			}),
 			}),
-		// create: (data) =>
-		//   request({
-		//     url: 'user/address',
-		//     method: 'POST',
-		//     data,
-		//     custom: {
-		//       showSuccess: true,
-		//     },
-		//   }),
 		update: (data) =>
 		update: (data) =>
-			request2({
-				url: 'member/address/update',
+			request({
+				url: '/app-api/member/address/update',
 				method: 'PUT',
 				method: 'PUT',
 				data,
 				data,
 				custom: {
 				custom: {
 					showSuccess: true,
 					showSuccess: true,
 				},
 				},
 			}),
 			}),
-		// update: (id, data) =>
-		// 	request({
-		// 		url: 'user/address/' + id,
-		// 		method: 'PUT',
-		// 		data,
-		// 		custom: {
-		// 			showSuccess: true,
-		// 		},
-		// 	}),
 		detail: (id) =>
 		detail: (id) =>
-			request2({
-				url: 'member/address/get?id=' + id,
+			request({
+				url: '/app-api/member/address/get?id=' + id,
 				method: 'GET',
 				method: 'GET',
 			}),
 			}),
-		// detail: (id) =>
-		//   request({
-		//     url: 'user/address/' + id,
-		//     method: 'GET',
-		//   }),
 		delete: (id) =>
 		delete: (id) =>
-			request2({
-				url: 'member/address/delete?id=' + id,
+			request({
+				url: '/app-api/member/address/delete?id=' + id,
 				method: 'DELETE',
 				method: 'DELETE',
 			}),
 			}),
-		// delete: (id) =>
-		// 	request({
-		// 		url: 'user/address/' + id,
-		// 		method: 'DELETE',
-		// 	}),
 	},
 	},
 	invoice: {
 	invoice: {
 		list: () =>
 		list: () =>
@@ -319,17 +257,29 @@ export default {
 	},
 	},
 	favorite: {
 	favorite: {
 		list: (params) =>
 		list: (params) =>
-			request2({
-				url: 'product/favorite/page',
+			request({
+				url: '/app-api/product/favorite/page',
 				method: 'GET',
 				method: 'GET',
 				params,
 				params,
 			}),
 			}),
 		do: (id) =>
 		do: (id) =>
 			request({
 			request({
-				url: 'user/goodsLog/favorite',
+				url: '/app-api/product/favorite/create',
 				method: 'POST',
 				method: 'POST',
 				data: {
 				data: {
-					goods_id: id,
+					spuId: id,
+				},
+				custom: {
+					showSuccess: true,
+					auth: true,
+				},
+			}),
+		dos: (id) =>
+			request({
+				url: '/app-api/product/favorite/delete',
+				method: 'DELETE',
+				data: {
+					spuId: id,
 				},
 				},
 				custom: {
 				custom: {
 					showSuccess: true,
 					showSuccess: true,
@@ -338,8 +288,8 @@ export default {
 			}),
 			}),
 		// 取消收藏
 		// 取消收藏
 		cancel: (id) =>
 		cancel: (id) =>
-			request2({
-				url: 'product/favorite/delete-list',
+			request({
+				url: '/app-api/product/favorite/delete-list',
 				method: 'DELETE',
 				method: 'DELETE',
 				data: {
 				data: {
 					spuIds: id.split(',').map(item => item * 1),
 					spuIds: id.split(',').map(item => item * 1),
@@ -350,18 +300,6 @@ export default {
 					auth: true,
 					auth: true,
 				},
 				},
 			}),
 			}),
-		// cancel: (id) =>
-		//   request({
-		//     url: 'user/goodsLog/favorite',
-		//     method: 'POST',
-		//     data: {
-		//       goods_ids: id,
-		//     },
-		//     custom: {
-		//       showSuccess: true,
-		//       auth: true,
-		//     },
-		//   }),
 	},
 	},
 	view: {
 	view: {
 		list: (params) =>
 		list: (params) =>
@@ -383,28 +321,21 @@ export default {
 	},
 	},
 	wallet: {
 	wallet: {
 		log: (params) =>
 		log: (params) =>
-			request2({
+			request({
 				// url: 'member/point/record/page',
 				// url: 'member/point/record/page',
-				url: 'pay/wallet-transaction/page',
+				url: '/app-api/pay/wallet-transaction/page',
 				method: 'GET',
 				method: 'GET',
 				params,
 				params,
 				custom: {},
 				custom: {},
 			}),
 			}),
 		log2: (params) =>
 		log2: (params) =>
-			request2({
-				url: 'member/point/record/page',
+			request({
+				url: '/app-api/member/point/record/page',
 				// url: 'pay/wallet-transaction/page',
 				// url: 'pay/wallet-transaction/page',
 				method: 'GET',
 				method: 'GET',
 				params,
 				params,
 				custom: {},
 				custom: {},
 			}),
 			}),
-		// log: (params) =>
-		// request({
-		// 	url: '/user/api/walletLog',
-		// 	method: 'GET',
-		// 	params,
-		// 	custom: {},
-		// }),
 	},
 	},
 	account: {
 	account: {
 		info: (params) =>
 		info: (params) =>
@@ -429,18 +360,9 @@ export default {
 			}),
 			}),
 	},
 	},
 	//数量接口
 	//数量接口
-	// data: () =>
-	//   request({
-	//     url: 'user/user/data',
-	//     method: 'GET',
-	//     custom: {
-	//       showLoading: false,
-	//       auth: true,
-	//     },
-	//   }),
 	data: () =>
 	data: () =>
-		request2({
-			url: 'trade/order/get-count',
+		request({
+			url: '/app-api/trade/order/get-count',
 			method: 'GET',
 			method: 'GET',
 			custom: {
 			custom: {
 				showLoading: false,
 				showLoading: false,
@@ -448,8 +370,8 @@ export default {
 			},
 			},
 		}),
 		}),
 	data2: () =>
 	data2: () =>
-		request2({
-			url: 'trade/after-sale/get-applying-count',
+		request({
+			url: '/app-api/trade/after-sale/get-applying-count',
 			method: 'GET',
 			method: 'GET',
 			custom: {
 			custom: {
 				showLoading: false,
 				showLoading: false,

+ 98 - 102
sheep/components/s-coupon-get/s-coupon-get.vue

@@ -1,108 +1,104 @@
 <template>
 <template>
-  <su-popup
-    :show="show"
-    type="bottom"
-    round="20"
-    @close="emits('close')"
-    showClose
-    backgroundColor="#f2f2f2"
-  >
-    <view class="model-box">
-      <view class="title ss-m-t-16 ss-m-l-20 ss-flex">优惠券</view>
-      <scroll-view
-        class="model-content"
-        scroll-y
-        :scroll-with-animation="false"
-        :enable-back-to-top="true"
-      >
-        <view class="subtitle ss-m-l-20">可使用优惠券</view>
-        <view v-for="item in state.couponInfo" :key="item.id">
-          <s-coupon-list :data="item">
-            <template #default>
-              <button
-                class="ss-reset-button card-btn ss-flex ss-row-center ss-col-center"
-                :class="
+	<su-popup :show="show" type="bottom" round="20" @close="emits('close')" showClose backgroundColor="#f2f2f2">
+		<view class="model-box">
+			<view class="title ss-m-t-16 ss-m-l-20 ss-flex">优惠券</view>
+			<scroll-view class="model-content" scroll-y :scroll-with-animation="false" :enable-back-to-top="true">
+				<view class="subtitle ss-m-l-20">可使用优惠券</view>
+				<view v-for="item in state.couponInfo" :key="item.id">
+					<s-coupon-list :data="item">
+						<template #default>
+							<button class="ss-reset-button card-btn ss-flex ss-row-center ss-col-center" :class="
                   item.get_status != 'can_get' && item.get_status != 'can_use' ? 'boder-btn' : ''
                   item.get_status != 'can_get' && item.get_status != 'can_use' ? 'boder-btn' : ''
-                "
-                @click.stop="getBuy(item.id)"
-                :disabled="item.get_status != 'can_get' && item.get_status != 'can_use'"
-              >
-                {{ item.get_status_text }}
-              </button>
-            </template>
-          </s-coupon-list>
-        </view>
-      </scroll-view>
-    </view>
-  </su-popup>
+                " @click.stop="getBuy(item.id)">
+								<!-- 此处对接领取优惠券先将限制解除 -->
+								<!-- :disabled="item.get_status != 'can_get' && item.get_status != 'can_use'" -->
+								{{ item.get_status_text }}
+							</button>
+						</template>
+					</s-coupon-list>
+				</view>
+			</scroll-view>
+		</view>
+	</su-popup>
 </template>
 </template>
 <script setup>
 <script setup>
-  import { computed, reactive } from 'vue';
-  const props = defineProps({
-    modelValue: {
-      type: Object,
-      default() {},
-    },
-    show: {
-      type: Boolean,
-      default: false,
-    },
-  });
-  const emits = defineEmits(['get', 'close']);
-  const state = reactive({
-    couponInfo: computed(() => props.modelValue),
-    currentValue: -1,
-    couponId: '',
-  });
-  const getBuy = (id) => {
-    emits('get', id);
-  };
-  //立即领取
+	import {
+		computed,
+		reactive
+	} from 'vue';
+	const props = defineProps({
+		modelValue: {
+			type: Object,
+			default () {},
+		},
+		show: {
+			type: Boolean,
+			default: false,
+		},
+	});
+	const emits = defineEmits(['get', 'close']);
+	const state = reactive({
+		couponInfo: computed(() => props.modelValue),
+		currentValue: -1,
+		couponId: '',
+	});
+	const getBuy = (id) => {
+		console.log('应该是详情页领取优惠券')
+		emits('get', id);
+	};
+	//立即领取
 </script>
 </script>
 <style lang="scss" scoped>
 <style lang="scss" scoped>
-  .model-box {
-    height: 60vh;
-    .title {
-      font-size: 36rpx;
-      height: 80rpx;
-      font-weight: bold;
-      color: #333333;
-    }
-    .subtitle {
-      font-size: 26rpx;
-      font-weight: 500;
-      color: #333333;
-    }
-  }
-  .model-content {
-    height: 54vh;
-  }
-  .modal-footer {
-    width: 100%;
-    height: 120rpx;
-    background: #fff;
-  }
-  .confirm-btn {
-    width: 710rpx;
-    margin-left: 20rpx;
-    height: 80rpx;
-    background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
-    border-radius: 40rpx;
-    color: #fff;
-  }
-  // 优惠券按钮
-  .card-btn {
-    // width: 144rpx;
-    padding: 0 16rpx;
-    height: 50rpx;
-    border-radius: 40rpx;
-    background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
-    color: #ffffff;
-    font-size: 24rpx;
-    font-weight: 400;
-  }
-  .boder-btn {
-    background: linear-gradient(90deg, var(--ui-BG-Main-opacity-4), var(--ui-BG-Main-light));
-    color: #fff !important;
-  }
-</style>
+	.model-box {
+		height: 60vh;
+
+		.title {
+			font-size: 36rpx;
+			height: 80rpx;
+			font-weight: bold;
+			color: #333333;
+		}
+
+		.subtitle {
+			font-size: 26rpx;
+			font-weight: 500;
+			color: #333333;
+		}
+	}
+
+	.model-content {
+		height: 54vh;
+	}
+
+	.modal-footer {
+		width: 100%;
+		height: 120rpx;
+		background: #fff;
+	}
+
+	.confirm-btn {
+		width: 710rpx;
+		margin-left: 20rpx;
+		height: 80rpx;
+		background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+		border-radius: 40rpx;
+		color: #fff;
+	}
+
+	// 优惠券按钮
+	.card-btn {
+		// width: 144rpx;
+		padding: 0 16rpx;
+		height: 50rpx;
+		border-radius: 40rpx;
+		background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+		color: #ffffff;
+		font-size: 24rpx;
+		font-weight: 400;
+	}
+
+	.boder-btn {
+		background: linear-gradient(90deg, var(--ui-BG-Main-opacity-4), var(--ui-BG-Main-light));
+		color: #fff !important;
+	}
+</style>

+ 2 - 6
sheep/components/s-coupon-list/s-coupon-list.vue

@@ -1,13 +1,9 @@
 <template>
 <template>
 	<view class="ss-m-20" :style="{ opacity: disabled ? '0.5' : '1' }">
 	<view class="ss-m-20" :style="{ opacity: disabled ? '0.5' : '1' }">
 		<view class="content">
 		<view class="content">
-			<!--      <view
-        class="tag ss-flex ss-row-center"
-        :class="
+			<view class="tag ss-flex ss-row-center" :class="
           data.status == 'expired' || data.status == 'used' ? 'disabled-bg-color' : 'info-bg-color'
           data.status == 'expired' || data.status == 'used' ? 'disabled-bg-color' : 'info-bg-color'
-        "
-        >{{ data.type_text }}</view
-      > -->
+        ">{{ data.type_text }}</view>
 			<view class="title ss-m-x-30 ss-p-t-18">
 			<view class="title ss-m-x-30 ss-p-t-18">
 				<view class="ss-flex ss-row-between">
 				<view class="ss-flex ss-row-between">
 					<view class="value-text ss-flex-1 ss-m-r-10" :class="
 					<view class="value-text ss-flex-1 ss-m-r-10" :class="

+ 3 - 1
sheep/request2/index.js

@@ -55,7 +55,8 @@ const http = new Request({
 	method: 'GET',
 	method: 'GET',
 	header: {
 	header: {
 		Accept: 'text/json',
 		Accept: 'text/json',
-		'Content-Type': 'application/json;charset=UTF-8',
+		'Content-Type': 'application/json',
+		// ;charset=UTF-8
 		platform: $platform.name,
 		platform: $platform.name,
 	},
 	},
 	// #ifdef APP-PLUS
 	// #ifdef APP-PLUS
@@ -97,6 +98,7 @@ http.interceptors.request.use(
 			config.header['tenant-id'] = '1';
 			config.header['tenant-id'] = '1';
 			config.header['Authorization'] = 'Bearer test247';
 			config.header['Authorization'] = 'Bearer test247';
 		}
 		}
+		// console.log(config, '看参数')
 		return config;
 		return config;
 	},
 	},
 	(error) => {
 	(error) => {

+ 1 - 1
sheep/store/cart.js

@@ -100,4 +100,4 @@ const cart = defineStore({
   },
   },
 });
 });
 
 
-export default cart;
+export default cart;

+ 191 - 191
uni_modules/mp-html/README.md

@@ -1,191 +1,191 @@
-## 为减小组件包的大小,默认组件包中不包含编辑、latex 公式等扩展功能,需要使用扩展功能的请参考下方的 插件扩展 栏的说明
-
-## 功能介绍
-- 全端支持(含 `v3、NVUE`)
-- 支持丰富的标签(包括 `table`、`video`、`svg` 等)
-- 支持丰富的事件效果(自动预览图片、链接处理等)
-- 支持设置占位图(加载中、出错时、预览时)
-- 支持锚点跳转、长按复制等丰富功能
-- 支持大部分 *html* 实体
-- 丰富的插件(关键词搜索、内容编辑、`latex` 公式等)
-- 效率高、容错性强且轻量化
-
-查看 [功能介绍](https://jin-yufeng.gitee.io/mp-html/#/overview/feature) 了解更多
-
-## 使用方法
-- `uni_modules` 方式  
-  1. 点击右上角的 `使用 HBuilder X 导入插件` 按钮直接导入项目或点击 `下载插件 ZIP` 按钮下载插件包并解压到项目的 `uni_modules/mp-html` 目录下  
-  2. 在需要使用页面的 `(n)vue` 文件中添加  
-     ```html
-     <!-- 不需要引入,可直接使用 -->
-     <mp-html :content="html" />
-     ```
-     ```javascript
-     export default {
-       data() {
-         return {
-           html: '<div>Hello World!</div>'
-         }
-       }
-     }
-     ```
-  3. 需要更新版本时在 `HBuilder X` 中右键 `uni_modules/mp-html` 目录选择 `从插件市场更新` 即可  
-
-- 源码方式  
-  1. 从 [github](https://github.com/jin-yufeng/mp-html/tree/master/dist/uni-app) 或 [gitee](https://gitee.com/jin-yufeng/mp-html/tree/master/dist/uni-app) 下载源码  
-     插件市场的 **非 uni_modules 版本** 无法更新,不建议从插件市场获取  
-  2. 在需要使用页面的 `(n)vue` 文件中添加  
-     ```html
-     <mp-html :content="html" />
-     ```
-     ```javascript
-     import mpHtml from '@/components/mp-html/mp-html'
-     export default {
-       // HBuilderX 2.5.5+ 可以通过 easycom 自动引入
-       components: {
-         mpHtml
-       },
-       data() {
-         return {
-           html: '<div>Hello World!</div>'
-         }
-       }
-     }
-     ```
-
-- npm 方式  
-  1. 在项目根目录下执行  
-     ```bash
-     npm install mp-html
-     ```
-  2. 在需要使用页面的 `(n)vue` 文件中添加  
-     ```html
-     <mp-html :content="html" />
-     ```
-     ```javascript
-     import mpHtml from 'mp-html/dist/uni-app/components/mp-html/mp-html'
-     export default {
-       // 不可省略
-       components: {
-         mpHtml
-       },
-       data() {
-         return {
-           html: '<div>Hello World!</div>'
-         }
-       }
-     }
-     ```
-  3. 需要更新版本时执行以下命令即可  
-     ```bash
-     npm update mp-html
-     ```
-  
-  使用 *cli* 方式运行的项目,通过 *npm* 方式引入时,需要在 *vue.config.js* 中配置 *transpileDependencies*,详情可见 [#330](https://github.com/jin-yufeng/mp-html/issues/330#issuecomment-913617687)  
-  如果在 **nvue** 中使用还要将 `dist/uni-app/static` 目录下的内容拷贝到项目的 `static` 目录下,否则无法运行  
-
-查看 [快速开始](https://jin-yufeng.gitee.io/mp-html/#/overview/quickstart) 了解更多
-
-## 组件属性
-
-| 属性 | 类型 | 默认值 | 说明 |
-|:---:|:---:|:---:|---|
-| container-style | String |  | 容器的样式([2.1.0+](https://jin-yufeng.gitee.io/mp-html/#/changelog/changelog#v210)) |
-| content | String |  | 用于渲染的 html 字符串 |
-| copy-link | Boolean | true | 是否允许外部链接被点击时自动复制 |
-| domain | String |  | 主域名(用于链接拼接) |
-| error-img | String |  | 图片出错时的占位图链接 |
-| lazy-load | Boolean | false | 是否开启图片懒加载 |
-| loading-img | String |  | 图片加载过程中的占位图链接 |
-| pause-video | Boolean | true | 是否在播放一个视频时自动暂停其他视频 |
-| preview-img | Boolean | true | 是否允许图片被点击时自动预览 |
-| scroll-table | Boolean | false | 是否给每个表格添加一个滚动层使其能单独横向滚动 |
-| selectable | Boolean | false | 是否开启文本长按复制 |
-| set-title | Boolean | true | 是否将 title 标签的内容设置到页面标题 |
-| show-img-menu | Boolean | true | 是否允许图片被长按时显示菜单 |
-| tag-style | Object |  | 设置标签的默认样式 |
-| use-anchor | Boolean | false | 是否使用锚点链接 |
-
-查看 [属性](https://jin-yufeng.gitee.io/mp-html/#/basic/prop) 了解更多
-
-## 组件事件
-
-| 名称 | 触发时机 |
-|:---:|---|
-| load | dom 树加载完毕时 |
-| ready | 图片加载完毕时 |
-| error | 发生渲染错误时 |
-| imgtap | 图片被点击时 |
-| linktap | 链接被点击时 |
-| play | 音视频播放时 |
-
-查看 [事件](https://jin-yufeng.gitee.io/mp-html/#/basic/event) 了解更多
-
-## api
-组件实例上提供了一些 `api` 方法可供调用
-
-| 名称 | 作用 |
-|:---:|---|
-| in | 将锚点跳转的范围限定在一个 scroll-view 内 |
-| navigateTo | 锚点跳转 |
-| getText | 获取文本内容 |
-| getRect | 获取富文本内容的位置和大小 |
-| setContent | 设置富文本内容 |
-| imgList | 获取所有图片的数组 |
-| pauseMedia | 暂停播放音视频([2.2.2+](https://jin-yufeng.gitee.io/mp-html/#/changelog/changelog#v222)) |
-| setPlaybackRate | 设置音视频播放速率([2.4.0+](https://jin-yufeng.gitee.io/mp-html/#/changelog/changelog#v240)) |
-
-查看 [api](https://jin-yufeng.gitee.io/mp-html/#/advanced/api) 了解更多
-
-## 插件扩展  
-除基本功能外,本组件还提供了丰富的扩展,可按照需要选用
-
-| 名称 | 作用 |
-|:---:|---|
-| audio | 音乐播放器 |
-| editable | 富文本 **编辑**([示例项目](https://mp-html.oss-cn-hangzhou.aliyuncs.com/editable.zip)) |
-| emoji | 解析 emoji |
-| highlight | 代码块高亮显示 |
-| markdown | 渲染 markdown |
-| search | 关键词搜索 |
-| style | 匹配 style 标签中的样式 |
-| txv-video | 使用腾讯视频 |
-| img-cache | 图片缓存 by [@PentaTea](https://github.com/PentaTea) |
-| latex | 渲染 latex 公式 by [@Zeng-J](https://github.com/Zeng-J) |
-
-从插件市场导入的包中 **不含有** 扩展插件,使用插件需通过微信小程序 `富文本插件` 获取或参考以下方法进行打包:  
-1. 获取完整组件包  
-   ```bash
-   npm install mp-html
-   ```
-2. 编辑 `tools/config.js` 中的 `plugins` 项,选择需要的插件  
-3. 生成新的组件包  
-   在 `node_modules/mp-html` 目录下执行  
-   ```bash
-   npm install
-   npm run build:uni-app
-   ```
-4. 拷贝 `dist/uni-app` 中的内容到项目根目录  
-
-查看 [插件](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin) 了解更多
-
-## 关于 nvue
-`nvue` 使用原生渲染,不支持部分 `css` 样式,为实现和 `html` 相同的效果,组件内部通过 `web-view` 进行渲染,性能上差于原生,根据 `weex` 官方建议,`web` 标签仅应用在非常规的降级场景。因此,如果通过原生的方式(如 `richtext`)能够满足需要,则不建议使用本组件,如果有较多的富文本内容,则可以直接使用 `vue` 页面  
-由于渲染方式与其他端不同,有以下限制:  
-1. 不支持 `lazy-load` 属性
-2. 视频不支持全屏播放
-3. 如果在 `flex-direction: row` 的容器中使用,需要给组件设置宽度或设置 `flex: 1` 占满剩余宽度
-
-纯 `nvue` 模式下,[此问题](https://ask.dcloud.net.cn/question/119678) 修复前,不支持通过 `uni_modules` 引入,需要本地引入(将 [dist/uni-app](https://github.com/jin-yufeng/mp-html/tree/master/dist/uni-app) 中的内容拷贝到项目根目录下)  
-
-## 立即体验
-![富文本插件](https://mp-html.oss-cn-hangzhou.aliyuncs.com/qrcode.jpg)
-
-## 问题反馈
-遇到问题时,请先查阅 [常见问题](https://jin-yufeng.gitee.io/mp-html/#/question/faq) 和 [issue](https://github.com/jin-yufeng/mp-html/issues) 中是否已有相同的问题  
-可通过 [issue](https://github.com/jin-yufeng/mp-html/issues/new/choose) 、插件问答或发送邮件到 [mp_html@126.com](mailto:mp_html@126.com) 提问,不建议在评论区提问(不方便回复)  
-提问请严格按照 [issue 模板](https://github.com/jin-yufeng/mp-html/issues/new/choose) ,描述清楚使用环境、`html` 内容或可复现的 `demo` 项目以及复现方式,对于 **描述不清**、**无法复现** 或重复的问题将不予回复  
-
-欢迎加入 `QQ` 交流群:`699734691`  
-
-查看 [问题反馈](https://jin-yufeng.gitee.io/mp-html/#/question/feedback) 了解更多
+## 为减小组件包的大小,默认组件包中不包含编辑、latex 公式等扩展功能,需要使用扩展功能的请参考下方的 插件扩展 栏的说明
+
+## 功能介绍
+- 全端支持(含 `v3、NVUE`)
+- 支持丰富的标签(包括 `table`、`video`、`svg` 等)
+- 支持丰富的事件效果(自动预览图片、链接处理等)
+- 支持设置占位图(加载中、出错时、预览时)
+- 支持锚点跳转、长按复制等丰富功能
+- 支持大部分 *html* 实体
+- 丰富的插件(关键词搜索、内容编辑、`latex` 公式等)
+- 效率高、容错性强且轻量化
+
+查看 [功能介绍](https://jin-yufeng.gitee.io/mp-html/#/overview/feature) 了解更多
+
+## 使用方法
+- `uni_modules` 方式  
+  1. 点击右上角的 `使用 HBuilder X 导入插件` 按钮直接导入项目或点击 `下载插件 ZIP` 按钮下载插件包并解压到项目的 `uni_modules/mp-html` 目录下  
+  2. 在需要使用页面的 `(n)vue` 文件中添加  
+     ```html
+     <!-- 不需要引入,可直接使用 -->
+     <mp-html :content="html" />
+     ```
+     ```javascript
+     export default {
+       data() {
+         return {
+           html: '<div>Hello World!</div>'
+         }
+       }
+     }
+     ```
+  3. 需要更新版本时在 `HBuilder X` 中右键 `uni_modules/mp-html` 目录选择 `从插件市场更新` 即可  
+
+- 源码方式  
+  1. 从 [github](https://github.com/jin-yufeng/mp-html/tree/master/dist/uni-app) 或 [gitee](https://gitee.com/jin-yufeng/mp-html/tree/master/dist/uni-app) 下载源码  
+     插件市场的 **非 uni_modules 版本** 无法更新,不建议从插件市场获取  
+  2. 在需要使用页面的 `(n)vue` 文件中添加  
+     ```html
+     <mp-html :content="html" />
+     ```
+     ```javascript
+     import mpHtml from '@/components/mp-html/mp-html'
+     export default {
+       // HBuilderX 2.5.5+ 可以通过 easycom 自动引入
+       components: {
+         mpHtml
+       },
+       data() {
+         return {
+           html: '<div>Hello World!</div>'
+         }
+       }
+     }
+     ```
+
+- npm 方式  
+  1. 在项目根目录下执行  
+     ```bash
+     npm install mp-html
+     ```
+  2. 在需要使用页面的 `(n)vue` 文件中添加  
+     ```html
+     <mp-html :content="html" />
+     ```
+     ```javascript
+     import mpHtml from 'mp-html/dist/uni-app/components/mp-html/mp-html'
+     export default {
+       // 不可省略
+       components: {
+         mpHtml
+       },
+       data() {
+         return {
+           html: '<div>Hello World!</div>'
+         }
+       }
+     }
+     ```
+  3. 需要更新版本时执行以下命令即可  
+     ```bash
+     npm update mp-html
+     ```
+  
+  使用 *cli* 方式运行的项目,通过 *npm* 方式引入时,需要在 *vue.config.js* 中配置 *transpileDependencies*,详情可见 [#330](https://github.com/jin-yufeng/mp-html/issues/330#issuecomment-913617687)  
+  如果在 **nvue** 中使用还要将 `dist/uni-app/static` 目录下的内容拷贝到项目的 `static` 目录下,否则无法运行  
+
+查看 [快速开始](https://jin-yufeng.gitee.io/mp-html/#/overview/quickstart) 了解更多
+
+## 组件属性
+
+| 属性 | 类型 | 默认值 | 说明 |
+|:---:|:---:|:---:|---|
+| container-style | String |  | 容器的样式([2.1.0+](https://jin-yufeng.gitee.io/mp-html/#/changelog/changelog#v210)) |
+| content | String |  | 用于渲染的 html 字符串 |
+| copy-link | Boolean | true | 是否允许外部链接被点击时自动复制 |
+| domain | String |  | 主域名(用于链接拼接) |
+| error-img | String |  | 图片出错时的占位图链接 |
+| lazy-load | Boolean | false | 是否开启图片懒加载 |
+| loading-img | String |  | 图片加载过程中的占位图链接 |
+| pause-video | Boolean | true | 是否在播放一个视频时自动暂停其他视频 |
+| preview-img | Boolean | true | 是否允许图片被点击时自动预览 |
+| scroll-table | Boolean | false | 是否给每个表格添加一个滚动层使其能单独横向滚动 |
+| selectable | Boolean | false | 是否开启文本长按复制 |
+| set-title | Boolean | true | 是否将 title 标签的内容设置到页面标题 |
+| show-img-menu | Boolean | true | 是否允许图片被长按时显示菜单 |
+| tag-style | Object |  | 设置标签的默认样式 |
+| use-anchor | Boolean | false | 是否使用锚点链接 |
+
+查看 [属性](https://jin-yufeng.gitee.io/mp-html/#/basic/prop) 了解更多
+
+## 组件事件
+
+| 名称 | 触发时机 |
+|:---:|---|
+| load | dom 树加载完毕时 |
+| ready | 图片加载完毕时 |
+| error | 发生渲染错误时 |
+| imgtap | 图片被点击时 |
+| linktap | 链接被点击时 |
+| play | 音视频播放时 |
+
+查看 [事件](https://jin-yufeng.gitee.io/mp-html/#/basic/event) 了解更多
+
+## api
+组件实例上提供了一些 `api` 方法可供调用
+
+| 名称 | 作用 |
+|:---:|---|
+| in | 将锚点跳转的范围限定在一个 scroll-view 内 |
+| navigateTo | 锚点跳转 |
+| getText | 获取文本内容 |
+| getRect | 获取富文本内容的位置和大小 |
+| setContent | 设置富文本内容 |
+| imgList | 获取所有图片的数组 |
+| pauseMedia | 暂停播放音视频([2.2.2+](https://jin-yufeng.gitee.io/mp-html/#/changelog/changelog#v222)) |
+| setPlaybackRate | 设置音视频播放速率([2.4.0+](https://jin-yufeng.gitee.io/mp-html/#/changelog/changelog#v240)) |
+
+查看 [api](https://jin-yufeng.gitee.io/mp-html/#/advanced/api) 了解更多
+
+## 插件扩展  
+除基本功能外,本组件还提供了丰富的扩展,可按照需要选用
+
+| 名称 | 作用 |
+|:---:|---|
+| audio | 音乐播放器 |
+| editable | 富文本 **编辑**([示例项目](https://mp-html.oss-cn-hangzhou.aliyuncs.com/editable.zip)) |
+| emoji | 解析 emoji |
+| highlight | 代码块高亮显示 |
+| markdown | 渲染 markdown |
+| search | 关键词搜索 |
+| style | 匹配 style 标签中的样式 |
+| txv-video | 使用腾讯视频 |
+| img-cache | 图片缓存 by [@PentaTea](https://github.com/PentaTea) |
+| latex | 渲染 latex 公式 by [@Zeng-J](https://github.com/Zeng-J) |
+
+从插件市场导入的包中 **不含有** 扩展插件,使用插件需通过微信小程序 `富文本插件` 获取或参考以下方法进行打包:  
+1. 获取完整组件包  
+   ```bash
+   npm install mp-html
+   ```
+2. 编辑 `tools/config.js` 中的 `plugins` 项,选择需要的插件  
+3. 生成新的组件包  
+   在 `node_modules/mp-html` 目录下执行  
+   ```bash
+   npm install
+   npm run build:uni-app
+   ```
+4. 拷贝 `dist/uni-app` 中的内容到项目根目录  
+
+查看 [插件](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin) 了解更多
+
+## 关于 nvue
+`nvue` 使用原生渲染,不支持部分 `css` 样式,为实现和 `html` 相同的效果,组件内部通过 `web-view` 进行渲染,性能上差于原生,根据 `weex` 官方建议,`web` 标签仅应用在非常规的降级场景。因此,如果通过原生的方式(如 `richtext`)能够满足需要,则不建议使用本组件,如果有较多的富文本内容,则可以直接使用 `vue` 页面  
+由于渲染方式与其他端不同,有以下限制:  
+1. 不支持 `lazy-load` 属性
+2. 视频不支持全屏播放
+3. 如果在 `flex-direction: row` 的容器中使用,需要给组件设置宽度或设置 `flex: 1` 占满剩余宽度
+
+纯 `nvue` 模式下,[此问题](https://ask.dcloud.net.cn/question/119678) 修复前,不支持通过 `uni_modules` 引入,需要本地引入(将 [dist/uni-app](https://github.com/jin-yufeng/mp-html/tree/master/dist/uni-app) 中的内容拷贝到项目根目录下)  
+
+## 立即体验
+![富文本插件](https://mp-html.oss-cn-hangzhou.aliyuncs.com/qrcode.jpg)
+
+## 问题反馈
+遇到问题时,请先查阅 [常见问题](https://jin-yufeng.gitee.io/mp-html/#/question/faq) 和 [issue](https://github.com/jin-yufeng/mp-html/issues) 中是否已有相同的问题  
+可通过 [issue](https://github.com/jin-yufeng/mp-html/issues/new/choose) 、插件问答或发送邮件到 [mp_html@126.com](mailto:mp_html@126.com) 提问,不建议在评论区提问(不方便回复)  
+提问请严格按照 [issue 模板](https://github.com/jin-yufeng/mp-html/issues/new/choose) ,描述清楚使用环境、`html` 内容或可复现的 `demo` 项目以及复现方式,对于 **描述不清**、**无法复现** 或重复的问题将不予回复  
+
+欢迎加入 `QQ` 交流群:`699734691`  
+
+查看 [问题反馈](https://jin-yufeng.gitee.io/mp-html/#/question/feedback) 了解更多

+ 498 - 498
uni_modules/mp-html/components/mp-html/mp-html.vue

@@ -1,498 +1,498 @@
-<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="/uni_modules/mp-html/static/app-plus/mp-html/local.html" :style="'margin-top:-2px;height:' + height + 'px'" @onPostMessage="_onMessage" />
-    <!-- #endif -->
-  </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: 'mp-html',
-  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)
-    }
-  },
-  beforeDestroy () {
-    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
-    },
-
-    /**
-     * @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
-    },
-
-    /**
-     * @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
-    },
-
-    /**
-     * @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)
-
-      // #ifndef APP-PLUS-NVUE
-      this._videos = []
-      this.$nextTick(() => {
-        this._hook('onLoad')
-        this.$emit('load')
-      })
-
-      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
-    },
-
-    /**
-     * @description 调用插件钩子函数
-     */
-    _hook (name) {
-      for (let i = plugins.length; i--;) {
-        if (this.plugins[i][name]) {
-          this.plugins[i][name]()
-        }
-      }
-    },
-
-    // #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 + ')')
-    },
-
-    /**
-     * @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;
-}
-/* #endif */
-</style>
+<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="/uni_modules/mp-html/static/app-plus/mp-html/local.html" :style="'margin-top:-2px;height:' + height + 'px'" @onPostMessage="_onMessage" />
+    <!-- #endif -->
+  </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: 'mp-html',
+  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)
+    }
+  },
+  beforeDestroy () {
+    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
+    },
+
+    /**
+     * @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
+    },
+
+    /**
+     * @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
+    },
+
+    /**
+     * @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)
+
+      // #ifndef APP-PLUS-NVUE
+      this._videos = []
+      this.$nextTick(() => {
+        this._hook('onLoad')
+        this.$emit('load')
+      })
+
+      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
+    },
+
+    /**
+     * @description 调用插件钩子函数
+     */
+    _hook (name) {
+      for (let i = plugins.length; i--;) {
+        if (this.plugins[i][name]) {
+          this.plugins[i][name]()
+        }
+      }
+    },
+
+    // #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 + ')')
+    },
+
+    /**
+     * @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;
+}
+/* #endif */
+</style>

+ 573 - 573
uni_modules/mp-html/components/mp-html/node/node.vue

@@ -1,576 +1,576 @@
-<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>
+<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>
 <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.getSystemInfoSync().system.includes('iOS')
-      // #endif
-    }
-  },
-  props: {
-    name: String,
-    attrs: {
-      type: Object,
-      default () {
-        return {}
-      }
-    },
-    childs: Array,
-    opts: Array
-  },
+
+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.getSystemInfoSync().system.includes('iOS')
+      // #endif
+    }
+  },
+  props: {
+    name: String,
+    attrs: {
+      type: Object,
+      default () {
+        return {}
+      }
+    },
+    childs: Array,
+    opts: Array
+  },
   components: {
   components: {
-
-    // #ifndef (H5 || APP-PLUS) && VUE3
-    node
-    // #endif
-  },
-  mounted () {
-    this.$nextTick(() => {
-      for (this.root = this.$parent; this.root.$options.name !== 'mp-html'; 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
-  },
-  beforeDestroy () {
-    // #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;
-  word-break: break-all;
-}
-
-/* a 标签点击态效果 */
-._hover {
-  text-decoration: underline;
-  opacity: 0.7;
-}
-
-/* 图片默认效果 */
-._img {
-  max-width: 100%;
-  -webkit-touch-callout: none;
-}
-
-/* 内部样式 */
-
-._block {
-  display: block;
-}
-
-._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 {
-  display: block;
-  font-weight: bold;
-}
-
-._image {
-  height: 1px;
-}
-
-._ins {
-  text-decoration: underline;
-}
-
-._li {
-  display: list-item;
-}
-
-._ol {
-  list-style-type: decimal;
-}
-
-._ol,
-._ul {
-  display: block;
-  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 */
+
+    // #ifndef (H5 || APP-PLUS) && VUE3
+    node
+    // #endif
+  },
+  mounted () {
+    this.$nextTick(() => {
+      for (this.root = this.$parent; this.root.$options.name !== 'mp-html'; 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
+  },
+  beforeDestroy () {
+    // #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;
+  word-break: break-all;
+}
+
+/* a 标签点击态效果 */
+._hover {
+  text-decoration: underline;
+  opacity: 0.7;
+}
+
+/* 图片默认效果 */
+._img {
+  max-width: 100%;
+  -webkit-touch-callout: none;
+}
+
+/* 内部样式 */
+
+._block {
+  display: block;
+}
+
+._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 {
+  display: block;
+  font-weight: bold;
+}
+
+._image {
+  height: 1px;
+}
+
+._ins {
+  text-decoration: underline;
+}
+
+._li {
+  display: list-item;
+}
+
+._ol {
+  list-style-type: decimal;
+}
+
+._ol,
+._ul {
+  display: block;
+  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>
 </style>

+ 1365 - 1333
uni_modules/mp-html/components/mp-html/parser.js

@@ -1,1333 +1,1365 @@
-/**
- * @fileoverview html 解析器
- */
-
-// 配置
-const config = {
-  // 信任的标签(保持标签名不变)
-  trustTags: makeMap('a,abbr,ad,audio,b,blockquote,br,code,col,colgroup,dd,del,dl,dt,div,em,fieldset,h1,h2,h3,h4,h5,h6,hr,i,img,ins,label,legend,li,ol,p,q,ruby,rt,source,span,strong,sub,sup,table,tbody,td,tfoot,th,thead,tr,title,ul,video'),
-
-  // 块级标签(转为 div,其他的非信任标签转为 span)
-  blockTags: makeMap('address,article,aside,body,caption,center,cite,footer,header,html,nav,pre,section'),
-
-  // #ifdef (MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE3
-  // 行内标签
-  inlineTags: makeMap('abbr,b,big,code,del,em,i,ins,label,q,small,span,strong,sub,sup'),
-  // #endif
-
-  // 要移除的标签
-  ignoreTags: makeMap('area,base,canvas,embed,frame,head,iframe,input,link,map,meta,param,rp,script,source,style,textarea,title,track,wbr'),
-
-  // 自闭合的标签
-  voidTags: makeMap('area,base,br,col,circle,ellipse,embed,frame,hr,img,input,line,link,meta,param,path,polygon,rect,source,track,use,wbr'),
-
-  // html 实体
-  entities: {
-    lt: '<',
-    gt: '>',
-    quot: '"',
-    apos: "'",
-    ensp: '\u2002',
-    emsp: '\u2003',
-    nbsp: '\xA0',
-    semi: ';',
-    ndash: '–',
-    mdash: '—',
-    middot: '·',
-    lsquo: '‘',
-    rsquo: '’',
-    ldquo: '“',
-    rdquo: '”',
-    bull: '•',
-    hellip: '…',
-    larr: '←',
-    uarr: '↑',
-    rarr: '→',
-    darr: '↓'
-  },
-
-  // 默认的标签样式
-  tagStyle: {
-    // #ifndef APP-PLUS-NVUE
-    address: 'font-style:italic',
-    big: 'display:inline;font-size:1.2em',
-    caption: 'display:table-caption;text-align:center',
-    center: 'text-align:center',
-    cite: 'font-style:italic',
-    dd: 'margin-left:40px',
-    mark: 'background-color:yellow',
-    pre: 'font-family:monospace;white-space:pre',
-    s: 'text-decoration:line-through',
-    small: 'display:inline;font-size:0.8em',
-    strike: 'text-decoration:line-through',
-    u: 'text-decoration:underline'
-    // #endif
-  },
-
-  // svg 大小写对照表
-  svgDict: {
-    animatetransform: 'animateTransform',
-    lineargradient: 'linearGradient',
-    viewbox: 'viewBox',
-    attributename: 'attributeName',
-    repeatcount: 'repeatCount',
-    repeatdur: 'repeatDur'
-  }
-}
-const tagSelector={}
-const {
-  windowWidth,
-  // #ifdef MP-WEIXIN
-  system
-  // #endif
-} = uni.getSystemInfoSync()
-const blankChar = makeMap(' ,\r,\n,\t,\f')
-let idIndex = 0
-
-// #ifdef H5 || APP-PLUS
-config.ignoreTags.iframe = undefined
-config.trustTags.iframe = true
-config.ignoreTags.embed = undefined
-config.trustTags.embed = true
-// #endif
-// #ifdef APP-PLUS-NVUE
-config.ignoreTags.source = undefined
-config.ignoreTags.style = undefined
-// #endif
-
-/**
- * @description 创建 map
- * @param {String} str 逗号分隔
- */
-function makeMap (str) {
-  const map = Object.create(null)
-  const list = str.split(',')
-  for (let i = list.length; i--;) {
-    map[list[i]] = true
-  }
-  return map
-}
-
-/**
- * @description 解码 html 实体
- * @param {String} str 要解码的字符串
- * @param {Boolean} amp 要不要解码 &amp;
- * @returns {String} 解码后的字符串
- */
-function decodeEntity (str, amp) {
-  let i = str.indexOf('&')
-  while (i !== -1) {
-    const j = str.indexOf(';', i + 3)
-    let code
-    if (j === -1) break
-    if (str[i + 1] === '#') {
-      // &#123; 形式的实体
-      code = parseInt((str[i + 2] === 'x' ? '0' : '') + str.substring(i + 2, j))
-      if (!isNaN(code)) {
-        str = str.substr(0, i) + String.fromCharCode(code) + str.substr(j + 1)
-      }
-    } else {
-      // &nbsp; 形式的实体
-      code = str.substring(i + 1, j)
-      if (config.entities[code] || (code === 'amp' && amp)) {
-        str = str.substr(0, i) + (config.entities[code] || '&') + str.substr(j + 1)
-      }
-    }
-    i = str.indexOf('&', i + 1)
-  }
-  return str
-}
-
-/**
- * @description 合并多个块级标签,加快长内容渲染
- * @param {Array} nodes 要合并的标签数组
- */
-function mergeNodes (nodes) {
-  let i = nodes.length - 1
-  for (let j = i; j >= -1; j--) {
-    if (j === -1 || nodes[j].c || !nodes[j].name || (nodes[j].name !== 'div' && nodes[j].name !== 'p' && nodes[j].name[0] !== 'h') || (nodes[j].attrs.style || '').includes('inline')) {
-      if (i - j >= 5) {
-        nodes.splice(j + 1, i - j, {
-          name: 'div',
-          attrs: {},
-          children: nodes.slice(j + 1, i + 1)
-        })
-      }
-      i = j - 1
-    }
-  }
-}
-
-/**
- * @description html 解析器
- * @param {Object} vm 组件实例
- */
-function Parser (vm) {
-  this.options = vm || {}
-  this.tagStyle = Object.assign({}, config.tagStyle, this.options.tagStyle)
-  this.imgList = vm.imgList || []
-  this.imgList._unloadimgs = 0
-  this.plugins = vm.plugins || []
-  this.attrs = Object.create(null)
-  this.stack = []
-  this.nodes = []
-  this.pre = (this.options.containerStyle || '').includes('white-space') && this.options.containerStyle.includes('pre') ? 2 : 0
-}
-
-/**
- * @description 执行解析
- * @param {String} content 要解析的文本
- */
-Parser.prototype.parse = function (content) {
-  // 插件处理
-  for (let i = this.plugins.length; i--;) {
-    if (this.plugins[i].onUpdate) {
-      content = this.plugins[i].onUpdate(content, config) || content
-    }
-  }
-
-  new Lexer(this).parse(content)
-  // 出栈未闭合的标签
-  while (this.stack.length) {
-    this.popNode()
-  }
-  if (this.nodes.length > 50) {
-    mergeNodes(this.nodes)
-  }
-  return this.nodes
-}
-
-/**
- * @description 将标签暴露出来(不被 rich-text 包含)
- */
-Parser.prototype.expose = function () {
-  // #ifndef APP-PLUS-NVUE
-  for (let i = this.stack.length; i--;) {
-    const item = this.stack[i]
-    if (item.c || item.name === 'a' || item.name === 'video' || item.name === 'audio') return
-    item.c = 1
-  }
-  // #endif
-}
-
-/**
- * @description 处理插件
- * @param {Object} node 要处理的标签
- * @returns {Boolean} 是否要移除此标签
- */
-Parser.prototype.hook = function (node) {
-  for (let i = this.plugins.length; i--;) {
-    if (this.plugins[i].onParse && this.plugins[i].onParse(node, this) === false) {
-      return false
-    }
-  }
-  return true
-}
-
-/**
- * @description 将链接拼接上主域名
- * @param {String} url 需要拼接的链接
- * @returns {String} 拼接后的链接
- */
-Parser.prototype.getUrl = function (url) {
-  const domain = this.options.domain
-  if (url[0] === '/') {
-    if (url[1] === '/') {
-      // // 开头的补充协议名
-      url = (domain ? domain.split('://')[0] : 'http') + ':' + url
-    } else if (domain) {
-      // 否则补充整个域名
-      url = domain + url
-    } /* #ifdef APP-PLUS */ else {
-      url = plus.io.convertLocalFileSystemURL(url)
-    } /* #endif */
-  } else if (!url.includes('data:') && !url.includes('://')) {
-    if (domain) {
-      url = domain + '/' + url
-    } /* #ifdef APP-PLUS */ else {
-      url = plus.io.convertLocalFileSystemURL(url)
-    } /* #endif */
-  }
-  return url
-}
-
-/**
- * @description 解析样式表
- * @param {Object} node 标签
- * @returns {Object}
- */
-Parser.prototype.parseStyle = function (node) {
-  const attrs = node.attrs
-  const list = (this.tagStyle[node.name] || '').split(';').concat((attrs.style || '').split(';'))
-  const styleObj = {}
-  let tmp = ''
-
-  if (attrs.id && !this.xml) {
-    // 暴露锚点
-    if (this.options.useAnchor) {
-      this.expose()
-    } else if (node.name !== 'img' && node.name !== 'a' && node.name !== 'video' && node.name !== 'audio') {
-      attrs.id = undefined
-    }
-  }
-
-  // 转换 width 和 height 属性
-  if (attrs.width) {
-    styleObj.width = parseFloat(attrs.width) + (attrs.width.includes('%') ? '%' : 'px')
-    attrs.width = undefined
-  }
-  if (attrs.height) {
-    styleObj.height = parseFloat(attrs.height) + (attrs.height.includes('%') ? '%' : 'px')
-    attrs.height = undefined
-  }
-
-  for (let i = 0, len = list.length; i < len; i++) {
-    const info = list[i].split(':')
-    if (info.length < 2) continue
-    const key = info.shift().trim().toLowerCase()
-    let value = info.join(':').trim()
-    if ((value[0] === '-' && value.lastIndexOf('-') > 0) || value.includes('safe')) {
-      // 兼容性的 css 不压缩
-      tmp += `;${key}:${value}`
-    } else if (!styleObj[key] || value.includes('import') || !styleObj[key].includes('import')) {
-      // 重复的样式进行覆盖
-      if (value.includes('url')) {
-        // 填充链接
-        let j = value.indexOf('(') + 1
-        if (j) {
-          while (value[j] === '"' || value[j] === "'" || blankChar[value[j]]) {
-            j++
-          }
-          value = value.substr(0, j) + this.getUrl(value.substr(j))
-        }
-      } else if (value.includes('rpx')) {
-        // 转换 rpx(rich-text 内部不支持 rpx)
-        value = value.replace(/[0-9.]+\s*rpx/g, $ => parseFloat($) * windowWidth / 750 + 'px')
-      }
-      styleObj[key] = value
-    }
-  }
-
-  node.attrs.style = tmp
-  return styleObj
-}
-
-/**
- * @description 解析到标签名
- * @param {String} name 标签名
- * @private
- */
-Parser.prototype.onTagName = function (name) {
-  this.tagName = this.xml ? name : name.toLowerCase()
-  if (this.tagName === 'svg') {
-    this.xml = (this.xml || 0) + 1 // svg 标签内大小写敏感
-  }
-}
-
-/**
- * @description 解析到属性名
- * @param {String} name 属性名
- * @private
- */
-Parser.prototype.onAttrName = function (name) {
-  name = this.xml ? name : name.toLowerCase()
-  if (name.substr(0, 5) === 'data-') {
-    if (name === 'data-src' && !this.attrs.src) {
-      // data-src 自动转为 src
-      this.attrName = 'src'
-    } else if (this.tagName === 'img' || this.tagName === 'a') {
-      // a 和 img 标签保留 data- 的属性,可以在 imgtap 和 linktap 事件中使用
-      this.attrName = name
-    } else {
-      // 剩余的移除以减小大小
-      this.attrName = undefined
-    }
-  } else {
-    this.attrName = name
-    this.attrs[name] = 'T' // boolean 型属性缺省设置
-  }
-}
-
-/**
- * @description 解析到属性值
- * @param {String} val 属性值
- * @private
- */
-Parser.prototype.onAttrVal = function (val) {
-  const name = this.attrName || ''
-  if (name === 'style' || name === 'href') {
-    // 部分属性进行实体解码
-    this.attrs[name] = decodeEntity(val, true)
-  } else if (name.includes('src')) {
-    // 拼接主域名
-    this.attrs[name] = this.getUrl(decodeEntity(val, true))
-  } else if (name) {
-    this.attrs[name] = val
-  }
-}
-
-/**
- * @description 解析到标签开始
- * @param {Boolean} selfClose 是否有自闭合标识 />
- * @private
- */
-Parser.prototype.onOpenTag = function (selfClose) {
-  // 拼装 node
-  const node = Object.create(null)
-  node.name = this.tagName
-  node.attrs = this.attrs
-  // 避免因为自动 diff 使得 type 被设置为 null 导致部分内容不显示
-  if (this.options.nodes.length) {
-    node.type = 'node'
-  }
-  this.attrs = Object.create(null)
-
-  const attrs = node.attrs
-  const parent = this.stack[this.stack.length - 1]
-  const siblings = parent ? parent.children : this.nodes
-  const close = this.xml ? selfClose : config.voidTags[node.name]
-
-  // 替换标签名选择器
-  if (tagSelector[node.name]) {
-    attrs.class = tagSelector[node.name] + (attrs.class ? ' ' + attrs.class : '')
-  }
-
-  // 转换 embed 标签
-  if (node.name === 'embed') {
-    // #ifndef H5 || APP-PLUS
-    const src = attrs.src || ''
-    // 按照后缀名和 type 将 embed 转为 video 或 audio
-    if (src.includes('.mp4') || src.includes('.3gp') || src.includes('.m3u8') || (attrs.type || '').includes('video')) {
-      node.name = 'video'
-    } else if (src.includes('.mp3') || src.includes('.wav') || src.includes('.aac') || src.includes('.m4a') || (attrs.type || '').includes('audio')) {
-      node.name = 'audio'
-    }
-    if (attrs.autostart) {
-      attrs.autoplay = 'T'
-    }
-    attrs.controls = 'T'
-    // #endif
-    // #ifdef H5 || APP-PLUS
-    this.expose()
-    // #endif
-  }
-
-  // #ifndef APP-PLUS-NVUE
-  // 处理音视频
-  if (node.name === 'video' || node.name === 'audio') {
-    // 设置 id 以便获取 context
-    if (node.name === 'video' && !attrs.id) {
-      attrs.id = 'v' + idIndex++
-    }
-    // 没有设置 controls 也没有设置 autoplay 的自动设置 controls
-    if (!attrs.controls && !attrs.autoplay) {
-      attrs.controls = 'T'
-    }
-    // 用数组存储所有可用的 source
-    node.src = []
-    if (attrs.src) {
-      node.src.push(attrs.src)
-      attrs.src = undefined
-    }
-    this.expose()
-  }
-  // #endif
-
-  // 处理自闭合标签
-  if (close) {
-    if (!this.hook(node) || config.ignoreTags[node.name]) {
-      // 通过 base 标签设置主域名
-      if (node.name === 'base' && !this.options.domain) {
-        this.options.domain = attrs.href
-      } /* #ifndef APP-PLUS-NVUE */ else if (node.name === 'source' && parent && (parent.name === 'video' || parent.name === 'audio') && attrs.src) {
-        // 设置 source 标签(仅父节点为 video 或 audio 时有效)
-        parent.src.push(attrs.src)
-      } /* #endif */
-      return
-    }
-
-    // 解析 style
-    const styleObj = this.parseStyle(node)
-
-    // 处理图片
-    if (node.name === 'img') {
-      if (attrs.src) {
-        // 标记 webp
-        if (attrs.src.includes('webp')) {
-          node.webp = 'T'
-        }
-        // data url 图片如果没有设置 original-src 默认为不可预览的小图片
-        if (attrs.src.includes('data:') && !attrs['original-src']) {
-          attrs.ignore = 'T'
-        }
-        if (!attrs.ignore || node.webp || attrs.src.includes('cloud://')) {
-          for (let i = this.stack.length; i--;) {
-            const item = this.stack[i]
-            if (item.name === 'a') {
-              node.a = item.attrs
-            }
-            if (item.name === 'table' && !node.webp && !attrs.src.includes('cloud://')) {
-              if (!styleObj.display || styleObj.display.includes('inline')) {
-                node.t = 'inline-block'
-              } else {
-                node.t = styleObj.display
-              }
-              styleObj.display = undefined
-            }
-            // #ifndef H5 || APP-PLUS
-            const style = item.attrs.style || ''
-            if (style.includes('flex:') && !style.includes('flex:0') && !style.includes('flex: 0') && (!styleObj.width || parseInt(styleObj.width) > 100)) {
-              styleObj.width = '100% !important'
-              styleObj.height = ''
-              for (let j = i + 1; j < this.stack.length; j++) {
-                this.stack[j].attrs.style = (this.stack[j].attrs.style || '').replace('inline-', '')
-              }
-            } else if (style.includes('flex') && styleObj.width === '100%') {
-              for (let j = i + 1; j < this.stack.length; j++) {
-                const style = this.stack[j].attrs.style || ''
-                if (!style.includes(';width') && !style.includes(' width') && style.indexOf('width') !== 0) {
-                  styleObj.width = ''
-                  break
-                }
-              }
-            } else if (style.includes('inline-block')) {
-              if (styleObj.width && styleObj.width[styleObj.width.length - 1] === '%') {
-                item.attrs.style += ';max-width:' + styleObj.width
-                styleObj.width = ''
-              } else {
-                item.attrs.style += ';max-width:100%'
-              }
-            }
-            // #endif
-            item.c = 1
-          }
-          attrs.i = this.imgList.length.toString()
-          let src = attrs['original-src'] || attrs.src
-          // #ifndef H5 || MP-ALIPAY || APP-PLUS || MP-360
-          if (this.imgList.includes(src)) {
-            // 如果有重复的链接则对域名进行随机大小写变换避免预览时错位
-            let i = src.indexOf('://')
-            if (i !== -1) {
-              i += 3
-              let newSrc = src.substr(0, i)
-              for (; i < src.length; i++) {
-                if (src[i] === '/') break
-                newSrc += Math.random() > 0.5 ? src[i].toUpperCase() : src[i]
-              }
-              newSrc += src.substr(i)
-              src = newSrc
-            }
-          }
-          // #endif
-          this.imgList.push(src)
-          if (!node.t) {
-            this.imgList._unloadimgs += 1
-          }
-          // #ifdef H5 || APP-PLUS
-          if (this.options.lazyLoad) {
-            attrs['data-src'] = attrs.src
-            attrs.src = undefined
-          }
-          // #endif
-        }
-      }
-      if (styleObj.display === 'inline') {
-        styleObj.display = ''
-      }
-      // #ifndef APP-PLUS-NVUE
-      if (attrs.ignore) {
-        styleObj['max-width'] = styleObj['max-width'] || '100%'
-        attrs.style += ';-webkit-touch-callout:none'
-      }
-      // #endif
-      // 设置的宽度超出屏幕,为避免变形,高度转为自动
-      if (parseInt(styleObj.width) > windowWidth) {
-        styleObj.height = undefined
-      }
-      // 记录是否设置了宽高
-      if (!isNaN(parseInt(styleObj.width))) {
-        node.w = 'T'
-      }
-      if (!isNaN(parseInt(styleObj.height)) && (!styleObj.height.includes('%') || (parent && (parent.attrs.style || '').includes('height')))) {
-        node.h = 'T'
-      }
-    } else if (node.name === 'svg') {
-      siblings.push(node)
-      this.stack.push(node)
-      this.popNode()
-      return
-    }
-    for (const key in styleObj) {
-      if (styleObj[key]) {
-        attrs.style += `;${key}:${styleObj[key].replace(' !important', '')}`
-      }
-    }
-    attrs.style = attrs.style.substr(1) || undefined
-    // #ifdef (MP-WEIXIN || MP-QQ) && VUE3
-    if (!attrs.style) {
-      delete attrs.style
-    }
-    // #endif
-  } else {
-    if ((node.name === 'pre' || ((attrs.style || '').includes('white-space') && attrs.style.includes('pre'))) && this.pre !== 2) {
-      this.pre = node.pre = 1
-    }
-    node.children = []
-    this.stack.push(node)
-  }
-
-  // 加入节点树
-  siblings.push(node)
-}
-
-/**
- * @description 解析到标签结束
- * @param {String} name 标签名
- * @private
- */
-Parser.prototype.onCloseTag = function (name) {
-  // 依次出栈到匹配为止
-  name = this.xml ? name : name.toLowerCase()
-  let i
-  for (i = this.stack.length; i--;) {
-    if (this.stack[i].name === name) break
-  }
-  if (i !== -1) {
-    while (this.stack.length > i) {
-      this.popNode()
-    }
-  } else if (name === 'p' || name === 'br') {
-    const siblings = this.stack.length ? this.stack[this.stack.length - 1].children : this.nodes
-    siblings.push({
-      name,
-      attrs: {
-        class: tagSelector[name] || '',
-        style: this.tagStyle[name] || ''
-      }
-    })
-  }
-}
-
-/**
- * @description 处理标签出栈
- * @private
- */
-Parser.prototype.popNode = function () {
-  const node = this.stack.pop()
-  let attrs = node.attrs
-  const children = node.children
-  const parent = this.stack[this.stack.length - 1]
-  const siblings = parent ? parent.children : this.nodes
-
-  if (!this.hook(node) || config.ignoreTags[node.name]) {
-    // 获取标题
-    if (node.name === 'title' && children.length && children[0].type === 'text' && this.options.setTitle) {
-      uni.setNavigationBarTitle({
-        title: children[0].text
-      })
-    }
-    siblings.pop()
-    return
-  }
-
-  if (node.pre && this.pre !== 2) {
-    // 是否合并空白符标识
-    this.pre = node.pre = undefined
-    for (let i = this.stack.length; i--;) {
-      if (this.stack[i].pre) {
-        this.pre = 1
-      }
-    }
-  }
-
-  const styleObj = {}
-
-  // 转换 svg
-  if (node.name === 'svg') {
-    if (this.xml > 1) {
-      // 多层 svg 嵌套
-      this.xml--
-      return
-    }
-    // #ifdef APP-PLUS-NVUE
-    (function traversal (node) {
-      if (node.name) {
-        // 调整 svg 的大小写
-        node.name = config.svgDict[node.name] || node.name
-        for (const item in node.attrs) {
-          if (config.svgDict[item]) {
-            node.attrs[config.svgDict[item]] = node.attrs[item]
-            node.attrs[item] = undefined
-          }
-        }
-        for (let i = 0; i < (node.children || []).length; i++) {
-          traversal(node.children[i])
-        }
-      }
-    })(node)
-    // #endif
-    // #ifndef APP-PLUS-NVUE
-    let src = ''
-    const style = attrs.style
-    attrs.style = ''
-    attrs.xmlns = 'http://www.w3.org/2000/svg';
-    (function traversal (node) {
-      if (node.type === 'text') {
-        src += node.text
-        return
-      }
-      const name = config.svgDict[node.name] || node.name
-      src += '<' + name
-      for (const item in node.attrs) {
-        const val = node.attrs[item]
-        if (val) {
-          src += ` ${config.svgDict[item] || item}="${val}"`
-        }
-      }
-      if (!node.children) {
-        src += '/>'
-      } else {
-        src += '>'
-        for (let i = 0; i < node.children.length; i++) {
-          traversal(node.children[i])
-        }
-        src += '</' + name + '>'
-      }
-    })(node)
-    node.name = 'img'
-    node.attrs = {
-      src: 'data:image/svg+xml;utf8,' + src.replace(/#/g, '%23'),
-      style,
-      ignore: 'T'
-    }
-    node.children = undefined
-    // #endif
-    this.xml = false
-    return
-  }
-
-  // #ifndef APP-PLUS-NVUE
-  // 转换 align 属性
-  if (attrs.align) {
-    if (node.name === 'table') {
-      if (attrs.align === 'center') {
-        styleObj['margin-inline-start'] = styleObj['margin-inline-end'] = 'auto'
-      } else {
-        styleObj.float = attrs.align
-      }
-    } else {
-      styleObj['text-align'] = attrs.align
-    }
-    attrs.align = undefined
-  }
-
-  // 转换 dir 属性
-  if (attrs.dir) {
-    styleObj.direction = attrs.dir
-    attrs.dir = undefined
-  }
-
-  // 转换 font 标签的属性
-  if (node.name === 'font') {
-    if (attrs.color) {
-      styleObj.color = attrs.color
-      attrs.color = undefined
-    }
-    if (attrs.face) {
-      styleObj['font-family'] = attrs.face
-      attrs.face = undefined
-    }
-    if (attrs.size) {
-      let size = parseInt(attrs.size)
-      if (!isNaN(size)) {
-        if (size < 1) {
-          size = 1
-        } else if (size > 7) {
-          size = 7
-        }
-        styleObj['font-size'] = ['x-small', 'small', 'medium', 'large', 'x-large', 'xx-large', 'xxx-large'][size - 1]
-      }
-      attrs.size = undefined
-    }
-  }
-  // #endif
-
-  // 一些编辑器的自带 class
-  if ((attrs.class || '').includes('align-center')) {
-    styleObj['text-align'] = 'center'
-  }
-
-  Object.assign(styleObj, this.parseStyle(node))
-
-  if (node.name !== 'table' && parseInt(styleObj.width) > windowWidth) {
-    styleObj['max-width'] = '100%'
-    styleObj['box-sizing'] = 'border-box'
-  }
-
-  // #ifndef APP-PLUS-NVUE
-  if (config.blockTags[node.name]) {
-    node.name = 'div'
-  } else if (!config.trustTags[node.name] && !this.xml) {
-    // 未知标签转为 span,避免无法显示
-    node.name = 'span'
-  }
-
-  if (node.name === 'a' || node.name === 'ad'
-    // #ifdef H5 || APP-PLUS
-    || node.name === 'iframe' // eslint-disable-line
-    // #endif
-  ) {
-    this.expose()
-  } else if (node.name === 'video') {
-    if ((styleObj.height || '').includes('auto')) {
-      styleObj.height = undefined
-    }
-    /* #ifdef APP-PLUS */
-    let str = '<video style="width:100%;height:100%"'
-    for (const item in attrs) {
-      if (attrs[item]) {
-        str += ' ' + item + '="' + attrs[item] + '"'
-      }
-    }
-    if (this.options.pauseVideo) {
-      str += ' onplay="this.dispatchEvent(new CustomEvent(\'vplay\',{bubbles:!0}));for(var e=document.getElementsByTagName(\'video\'),t=0;t<e.length;t++)e[t]!=this&&e[t].pause()"'
-    }
-    str += '>'
-    for (let i = 0; i < node.src.length; i++) {
-      str += '<source src="' + node.src[i] + '">'
-    }
-    str += '</video>'
-    node.html = str
-    /* #endif */
-  } else if ((node.name === 'ul' || node.name === 'ol') && node.c) {
-    // 列表处理
-    const types = {
-      a: 'lower-alpha',
-      A: 'upper-alpha',
-      i: 'lower-roman',
-      I: 'upper-roman'
-    }
-    if (types[attrs.type]) {
-      attrs.style += ';list-style-type:' + types[attrs.type]
-      attrs.type = undefined
-    }
-    for (let i = children.length; i--;) {
-      if (children[i].name === 'li') {
-        children[i].c = 1
-      }
-    }
-  } else if (node.name === 'table') {
-    // 表格处理
-    // cellpadding、cellspacing、border 这几个常用表格属性需要通过转换实现
-    let padding = parseFloat(attrs.cellpadding)
-    let spacing = parseFloat(attrs.cellspacing)
-    const border = parseFloat(attrs.border)
-    const bordercolor = styleObj['border-color']
-    const borderstyle = styleObj['border-style']
-    if (node.c) {
-      // padding 和 spacing 默认 2
-      if (isNaN(padding)) {
-        padding = 2
-      }
-      if (isNaN(spacing)) {
-        spacing = 2
-      }
-    }
-    if (border) {
-      attrs.style += `;border:${border}px ${borderstyle || 'solid'} ${bordercolor || 'gray'}`
-    }
-    if (node.flag && node.c) {
-      // 有 colspan 或 rowspan 且含有链接的表格通过 grid 布局实现
-      styleObj.display = 'grid'
-      if (spacing) {
-        styleObj['grid-gap'] = spacing + 'px'
-        styleObj.padding = spacing + 'px'
-      } else if (border) {
-        // 无间隔的情况下避免边框重叠
-        attrs.style += ';border-left:0;border-top:0'
-      }
-
-      const width = [] // 表格的列宽
-      const trList = [] // tr 列表
-      const cells = [] // 保存新的单元格
-      const map = {}; // 被合并单元格占用的格子
-
-      (function traversal (nodes) {
-        for (let i = 0; i < nodes.length; i++) {
-          if (nodes[i].name === 'tr') {
-            trList.push(nodes[i])
-          } else {
-            traversal(nodes[i].children || [])
-          }
-        }
-      })(children)
-
-      for (let row = 1; row <= trList.length; row++) {
-        let col = 1
-        for (let j = 0; j < trList[row - 1].children.length; j++) {
-          const td = trList[row - 1].children[j]
-          if (td.name === 'td' || td.name === 'th') {
-            // 这个格子被上面的单元格占用,则列号++
-            while (map[row + '.' + col]) {
-              col++
-            }
-            let style = td.attrs.style || ''
-            let start = style.indexOf('width') ? style.indexOf(';width') : 0
-            // 提取出 td 的宽度
-            if (start !== -1) {
-              let end = style.indexOf(';', start + 6)
-              if (end === -1) {
-                end = style.length
-              }
-              if (!td.attrs.colspan) {
-                width[col] = style.substring(start ? start + 7 : 6, end)
-              }
-              style = style.substr(0, start) + style.substr(end)
-            }
-            // 设置竖直对齐
-            style += ';display:flex'
-            start = style.indexOf('vertical-align')
-            if (start !== -1) {
-              const val = style.substr(start + 15, 10)
-              if (val.includes('middle')) {
-                style += ';align-items:center'
-              } else if (val.includes('bottom')) {
-                style += ';align-items:flex-end'
-              }
-            } else {
-              style += ';align-items:center'
-            }
-            // 设置水平对齐
-            start = style.indexOf('text-align')
-            if (start !== -1) {
-              const val = style.substr(start + 11, 10)
-              if (val.includes('center')) {
-                style += ';justify-content: center'
-              } else if (val.includes('right')) {
-                style += ';justify-content: right'
-              }
-            }
-            style = (border ? `;border:${border}px ${borderstyle || 'solid'} ${bordercolor || 'gray'}` + (spacing ? '' : ';border-right:0;border-bottom:0') : '') + (padding ? `;padding:${padding}px` : '') + ';' + style
-            // 处理列合并
-            if (td.attrs.colspan) {
-              style += `;grid-column-start:${col};grid-column-end:${col + parseInt(td.attrs.colspan)}`
-              if (!td.attrs.rowspan) {
-                style += `;grid-row-start:${row};grid-row-end:${row + 1}`
-              }
-              col += parseInt(td.attrs.colspan) - 1
-            }
-            // 处理行合并
-            if (td.attrs.rowspan) {
-              style += `;grid-row-start:${row};grid-row-end:${row + parseInt(td.attrs.rowspan)}`
-              if (!td.attrs.colspan) {
-                style += `;grid-column-start:${col};grid-column-end:${col + 1}`
-              }
-              // 记录下方单元格被占用
-              for (let rowspan = 1; rowspan < td.attrs.rowspan; rowspan++) {
-                for (let colspan = 0; colspan < (td.attrs.colspan || 1); colspan++) {
-                  map[(row + rowspan) + '.' + (col - colspan)] = 1
-                }
-              }
-            }
-            if (style) {
-              td.attrs.style = style
-            }
-            cells.push(td)
-            col++
-          }
-        }
-        if (row === 1) {
-          let temp = ''
-          for (let i = 1; i < col; i++) {
-            temp += (width[i] ? width[i] : 'auto') + ' '
-          }
-          styleObj['grid-template-columns'] = temp
-        }
-      }
-      node.children = cells
-    } else {
-      // 没有使用合并单元格的表格通过 table 布局实现
-      if (node.c) {
-        styleObj.display = 'table'
-      }
-      if (!isNaN(spacing)) {
-        styleObj['border-spacing'] = spacing + 'px'
-      }
-      if (border || padding) {
-        // 遍历
-        (function traversal (nodes) {
-          for (let i = 0; i < nodes.length; i++) {
-            const td = nodes[i]
-            if (td.name === 'th' || td.name === 'td') {
-              if (border) {
-                td.attrs.style = `border:${border}px ${borderstyle || 'solid'} ${bordercolor || 'gray'};${td.attrs.style || ''}`
-              }
-              if (padding) {
-                td.attrs.style = `padding:${padding}px;${td.attrs.style || ''}`
-              }
-            } else if (td.children) {
-              traversal(td.children)
-            }
-          }
-        })(children)
-      }
-    }
-    // 给表格添加一个单独的横向滚动层
-    if (this.options.scrollTable && !(attrs.style || '').includes('inline')) {
-      const table = Object.assign({}, node)
-      node.name = 'div'
-      node.attrs = {
-        style: 'overflow:auto'
-      }
-      node.children = [table]
-      attrs = table.attrs
-    }
-  } else if ((node.name === 'td' || node.name === 'th') && (attrs.colspan || attrs.rowspan)) {
-    for (let i = this.stack.length; i--;) {
-      if (this.stack[i].name === 'table') {
-        this.stack[i].flag = 1 // 指示含有合并单元格
-        break
-      }
-    }
-  } else if (node.name === 'ruby') {
-    // 转换 ruby
-    node.name = 'span'
-    for (let i = 0; i < children.length - 1; i++) {
-      if (children[i].type === 'text' && children[i + 1].name === 'rt') {
-        children[i] = {
-          name: 'div',
-          attrs: {
-            style: 'display:inline-block;text-align:center'
-          },
-          children: [{
-            name: 'div',
-            attrs: {
-              style: 'font-size:50%;' + (children[i + 1].attrs.style || '')
-            },
-            children: children[i + 1].children
-          }, children[i]]
-        }
-        children.splice(i + 1, 1)
-      }
-    }
-  } else if (node.c) {
-    (function traversal (node) {
-      node.c = 2
-      for (let i = node.children.length; i--;) {
-        const child = node.children[i]
-        // #ifdef (MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE3
-        if (child.name && (config.inlineTags[child.name] || ((child.attrs.style || '').includes('inline') && child.children)) && !child.c) {
-          traversal(child)
-        }
-        // #endif
-        if (!child.c || child.name === 'table') {
-          node.c = 1
-        }
-      }
-    })(node)
-  }
-
-  if ((styleObj.display || '').includes('flex') && !node.c) {
-    for (let i = children.length; i--;) {
-      const item = children[i]
-      if (item.f) {
-        item.attrs.style = (item.attrs.style || '') + item.f
-        item.f = undefined
-      }
-    }
-  }
-  // flex 布局时部分样式需要提取到 rich-text 外层
-  const flex = parent && ((parent.attrs.style || '').includes('flex') || (parent.attrs.style || '').includes('grid'))
-    // #ifdef MP-WEIXIN
-    // 检查基础库版本 virtualHost 是否可用
-    && !(node.c && wx.getNFCAdapter) // eslint-disable-line
-    // #endif
-    // #ifndef MP-WEIXIN || MP-QQ || MP-BAIDU || MP-TOUTIAO
-    && !node.c // eslint-disable-line
-  // #endif
-  if (flex) {
-    node.f = ';max-width:100%'
-  }
-
-  if (children.length >= 50 && node.c && !(styleObj.display || '').includes('flex')) {
-    mergeNodes(children)
-  }
-  // #endif
-
-  for (const key in styleObj) {
-    if (styleObj[key]) {
-      const val = `;${key}:${styleObj[key].replace(' !important', '')}`
-      /* #ifndef APP-PLUS-NVUE */
-      if (flex && ((key.includes('flex') && key !== 'flex-direction') || key === 'align-self' || key.includes('grid') || styleObj[key][0] === '-' || (key.includes('width') && val.includes('%')))) {
-        node.f += val
-        if (key === 'width') {
-          attrs.style += ';width:100%'
-        }
-      } else /* #endif */ {
-        attrs.style += val
-      }
-    }
-  }
-  attrs.style = attrs.style.substr(1) || undefined
-  // #ifdef (MP-WEIXIN || MP-QQ) && VUE3
-  for (const key in attrs) {
-    if (!attrs[key]) {
-      delete attrs[key]
-    }
-  }
-  // #endif
-}
-
-/**
- * @description 解析到文本
- * @param {String} text 文本内容
- */
-Parser.prototype.onText = function (text) {
-  if (!this.pre) {
-    // 合并空白符
-    let trim = ''
-    let flag
-    for (let i = 0, len = text.length; i < len; i++) {
-      if (!blankChar[text[i]]) {
-        trim += text[i]
-      } else {
-        if (trim[trim.length - 1] !== ' ') {
-          trim += ' '
-        }
-        if (text[i] === '\n' && !flag) {
-          flag = true
-        }
-      }
-    }
-    // 去除含有换行符的空串
-    if (trim === ' ') {
-      if (flag) return
-      // #ifdef VUE3
-      else {
-        const parent = this.stack[this.stack.length - 1]
-        if (parent && parent.name[0] === 't') return
-      }
-      // #endif
-    }
-    text = trim
-  }
-  const node = Object.create(null)
-  node.type = 'text'
-  // #ifdef (MP-BAIDU || MP-ALIPAY || MP-TOUTIAO) && VUE3
-  node.attrs = {}
-  // #endif
-  node.text = decodeEntity(text)
-  if (this.hook(node)) {
-    // #ifdef MP-WEIXIN
-    if (this.options.selectable === 'force' && system.includes('iOS') && !uni.canIUse('rich-text.user-select')) {
-      this.expose()
-    }
-    // #endif
-    const siblings = this.stack.length ? this.stack[this.stack.length - 1].children : this.nodes
-    siblings.push(node)
-  }
-}
-
-/**
- * @description html 词法分析器
- * @param {Object} handler 高层处理器
- */
-function Lexer (handler) {
-  this.handler = handler
-}
-
-/**
- * @description 执行解析
- * @param {String} content 要解析的文本
- */
-Lexer.prototype.parse = function (content) {
-  this.content = content || ''
-  this.i = 0 // 标记解析位置
-  this.start = 0 // 标记一个单词的开始位置
-  this.state = this.text // 当前状态
-  for (let len = this.content.length; this.i !== -1 && this.i < len;) {
-    this.state()
-  }
-}
-
-/**
- * @description 检查标签是否闭合
- * @param {String} method 如果闭合要进行的操作
- * @returns {Boolean} 是否闭合
- * @private
- */
-Lexer.prototype.checkClose = function (method) {
-  const selfClose = this.content[this.i] === '/'
-  if (this.content[this.i] === '>' || (selfClose && this.content[this.i + 1] === '>')) {
-    if (method) {
-      this.handler[method](this.content.substring(this.start, this.i))
-    }
-    this.i += selfClose ? 2 : 1
-    this.start = this.i
-    this.handler.onOpenTag(selfClose)
-    if (this.handler.tagName === 'script') {
-      this.i = this.content.indexOf('</', this.i)
-      if (this.i !== -1) {
-        this.i += 2
-        this.start = this.i
-      }
-      this.state = this.endTag
-    } else {
-      this.state = this.text
-    }
-    return true
-  }
-  return false
-}
-
-/**
- * @description 文本状态
- * @private
- */
-Lexer.prototype.text = function () {
-  this.i = this.content.indexOf('<', this.i) // 查找最近的标签
-  if (this.i === -1) {
-    // 没有标签了
-    if (this.start < this.content.length) {
-      this.handler.onText(this.content.substring(this.start, this.content.length))
-    }
-    return
-  }
-  const c = this.content[this.i + 1]
-  if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
-    // 标签开头
-    if (this.start !== this.i) {
-      this.handler.onText(this.content.substring(this.start, this.i))
-    }
-    this.start = ++this.i
-    this.state = this.tagName
-  } else if (c === '/' || c === '!' || c === '?') {
-    if (this.start !== this.i) {
-      this.handler.onText(this.content.substring(this.start, this.i))
-    }
-    const next = this.content[this.i + 2]
-    if (c === '/' && ((next >= 'a' && next <= 'z') || (next >= 'A' && next <= 'Z'))) {
-      // 标签结尾
-      this.i += 2
-      this.start = this.i
-      this.state = this.endTag
-      return
-    }
-    // 处理注释
-    let end = '-->'
-    if (c !== '!' || this.content[this.i + 2] !== '-' || this.content[this.i + 3] !== '-') {
-      end = '>'
-    }
-    this.i = this.content.indexOf(end, this.i)
-    if (this.i !== -1) {
-      this.i += end.length
-      this.start = this.i
-    }
-  } else {
-    this.i++
-  }
-}
-
-/**
- * @description 标签名状态
- * @private
- */
-Lexer.prototype.tagName = function () {
-  if (blankChar[this.content[this.i]]) {
-    // 解析到标签名
-    this.handler.onTagName(this.content.substring(this.start, this.i))
-    while (blankChar[this.content[++this.i]]);
-    if (this.i < this.content.length && !this.checkClose()) {
-      this.start = this.i
-      this.state = this.attrName
-    }
-  } else if (!this.checkClose('onTagName')) {
-    this.i++
-  }
-}
-
-/**
- * @description 属性名状态
- * @private
- */
-Lexer.prototype.attrName = function () {
-  let c = this.content[this.i]
-  if (blankChar[c] || c === '=') {
-    // 解析到属性名
-    this.handler.onAttrName(this.content.substring(this.start, this.i))
-    let needVal = c === '='
-    const len = this.content.length
-    while (++this.i < len) {
-      c = this.content[this.i]
-      if (!blankChar[c]) {
-        if (this.checkClose()) return
-        if (needVal) {
-          // 等号后遇到第一个非空字符
-          this.start = this.i
-          this.state = this.attrVal
-          return
-        }
-        if (this.content[this.i] === '=') {
-          needVal = true
-        } else {
-          this.start = this.i
-          this.state = this.attrName
-          return
-        }
-      }
-    }
-  } else if (!this.checkClose('onAttrName')) {
-    this.i++
-  }
-}
-
-/**
- * @description 属性值状态
- * @private
- */
-Lexer.prototype.attrVal = function () {
-  const c = this.content[this.i]
-  const len = this.content.length
-  if (c === '"' || c === "'") {
-    // 有冒号的属性
-    this.start = ++this.i
-    this.i = this.content.indexOf(c, this.i)
-    if (this.i === -1) return
-    this.handler.onAttrVal(this.content.substring(this.start, this.i))
-  } else {
-    // 没有冒号的属性
-    for (; this.i < len; this.i++) {
-      if (blankChar[this.content[this.i]]) {
-        this.handler.onAttrVal(this.content.substring(this.start, this.i))
-        break
-      } else if (this.checkClose('onAttrVal')) return
-    }
-  }
-  while (blankChar[this.content[++this.i]]);
-  if (this.i < len && !this.checkClose()) {
-    this.start = this.i
-    this.state = this.attrName
-  }
-}
-
-/**
- * @description 结束标签状态
- * @returns {String} 结束的标签名
- * @private
- */
-Lexer.prototype.endTag = function () {
-  const c = this.content[this.i]
-  if (blankChar[c] || c === '>' || c === '/') {
-    this.handler.onCloseTag(this.content.substring(this.start, this.i))
-    if (c !== '>') {
-      this.i = this.content.indexOf('>', this.i)
-      if (this.i === -1) return
-    }
-    this.start = ++this.i
-    this.state = this.text
-  } else {
-    this.i++
-  }
-}
-
-export default Parser
+/**
+ * @fileoverview html 解析器
+ */
+
+// 配置
+const config = {
+	// 信任的标签(保持标签名不变)
+	trustTags: makeMap(
+		'a,abbr,ad,audio,b,blockquote,br,code,col,colgroup,dd,del,dl,dt,div,em,fieldset,h1,h2,h3,h4,h5,h6,hr,i,img,ins,label,legend,li,ol,p,q,ruby,rt,source,span,strong,sub,sup,table,tbody,td,tfoot,th,thead,tr,title,ul,video'
+	),
+
+	// 块级标签(转为 div,其他的非信任标签转为 span)
+	blockTags: makeMap('address,article,aside,body,caption,center,cite,footer,header,html,nav,pre,section'),
+
+	// #ifdef (MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE3
+	// 行内标签
+	inlineTags: makeMap('abbr,b,big,code,del,em,i,ins,label,q,small,span,strong,sub,sup'),
+	// #endif
+
+	// 要移除的标签
+	ignoreTags: makeMap(
+		'area,base,canvas,embed,frame,head,iframe,input,link,map,meta,param,rp,script,source,style,textarea,title,track,wbr'
+	),
+
+	// 自闭合的标签
+	voidTags: makeMap(
+		'area,base,br,col,circle,ellipse,embed,frame,hr,img,input,line,link,meta,param,path,polygon,rect,source,track,use,wbr'
+	),
+
+	// html 实体
+	entities: {
+		lt: '<',
+		gt: '>',
+		quot: '"',
+		apos: "'",
+		ensp: '\u2002',
+		emsp: '\u2003',
+		nbsp: '\xA0',
+		semi: ';',
+		ndash: '–',
+		mdash: '—',
+		middot: '·',
+		lsquo: '‘',
+		rsquo: '’',
+		ldquo: '“',
+		rdquo: '”',
+		bull: '•',
+		hellip: '…',
+		larr: '←',
+		uarr: '↑',
+		rarr: '→',
+		darr: '↓'
+	},
+
+	// 默认的标签样式
+	tagStyle: {
+		// #ifndef APP-PLUS-NVUE
+		address: 'font-style:italic',
+		big: 'display:inline;font-size:1.2em',
+		caption: 'display:table-caption;text-align:center',
+		center: 'text-align:center',
+		cite: 'font-style:italic',
+		dd: 'margin-left:40px',
+		mark: 'background-color:yellow',
+		pre: 'font-family:monospace;white-space:pre',
+		s: 'text-decoration:line-through',
+		small: 'display:inline;font-size:0.8em',
+		strike: 'text-decoration:line-through',
+		u: 'text-decoration:underline'
+		// #endif
+	},
+
+	// svg 大小写对照表
+	svgDict: {
+		animatetransform: 'animateTransform',
+		lineargradient: 'linearGradient',
+		viewbox: 'viewBox',
+		attributename: 'attributeName',
+		repeatcount: 'repeatCount',
+		repeatdur: 'repeatDur'
+	}
+}
+const tagSelector = {}
+const {
+	windowWidth,
+	// #ifdef MP-WEIXIN
+	system
+	// #endif
+} = uni.getSystemInfoSync()
+const blankChar = makeMap(' ,\r,\n,\t,\f')
+let idIndex = 0
+
+// #ifdef H5 || APP-PLUS
+config.ignoreTags.iframe = undefined
+config.trustTags.iframe = true
+config.ignoreTags.embed = undefined
+config.trustTags.embed = true
+// #endif
+// #ifdef APP-PLUS-NVUE
+config.ignoreTags.source = undefined
+config.ignoreTags.style = undefined
+// #endif
+
+/**
+ * @description 创建 map
+ * @param {String} str 逗号分隔
+ */
+function makeMap(str) {
+	const map = Object.create(null)
+	const list = str.split(',')
+	for (let i = list.length; i--;) {
+		map[list[i]] = true
+	}
+	return map
+}
+
+/**
+ * @description 解码 html 实体
+ * @param {String} str 要解码的字符串
+ * @param {Boolean} amp 要不要解码 &amp;
+ * @returns {String} 解码后的字符串
+ */
+function decodeEntity(str, amp) {
+	let i = str.indexOf('&')
+	while (i !== -1) {
+		const j = str.indexOf(';', i + 3)
+		let code
+		if (j === -1) break
+		if (str[i + 1] === '#') {
+			// &#123; 形式的实体
+			code = parseInt((str[i + 2] === 'x' ? '0' : '') + str.substring(i + 2, j))
+			if (!isNaN(code)) {
+				str = str.substr(0, i) + String.fromCharCode(code) + str.substr(j + 1)
+			}
+		} else {
+			// &nbsp; 形式的实体
+			code = str.substring(i + 1, j)
+			if (config.entities[code] || (code === 'amp' && amp)) {
+				str = str.substr(0, i) + (config.entities[code] || '&') + str.substr(j + 1)
+			}
+		}
+		i = str.indexOf('&', i + 1)
+	}
+	return str
+}
+
+/**
+ * @description 合并多个块级标签,加快长内容渲染
+ * @param {Array} nodes 要合并的标签数组
+ */
+function mergeNodes(nodes) {
+	let i = nodes.length - 1
+	for (let j = i; j >= -1; j--) {
+		if (j === -1 || nodes[j].c || !nodes[j].name || (nodes[j].name !== 'div' && nodes[j].name !== 'p' && nodes[j]
+				.name[0] !== 'h') || (nodes[j].attrs.style || '').includes('inline')) {
+			if (i - j >= 5) {
+				nodes.splice(j + 1, i - j, {
+					name: 'div',
+					attrs: {},
+					children: nodes.slice(j + 1, i + 1)
+				})
+			}
+			i = j - 1
+		}
+	}
+}
+
+/**
+ * @description html 解析器
+ * @param {Object} vm 组件实例
+ */
+function Parser(vm) {
+	this.options = vm || {}
+	this.tagStyle = Object.assign({}, config.tagStyle, this.options.tagStyle)
+	this.imgList = vm.imgList || []
+	this.imgList._unloadimgs = 0
+	this.plugins = vm.plugins || []
+	this.attrs = Object.create(null)
+	this.stack = []
+	this.nodes = []
+	this.pre = (this.options.containerStyle || '').includes('white-space') && this.options.containerStyle.includes(
+		'pre') ? 2 : 0
+}
+
+/**
+ * @description 执行解析
+ * @param {String} content 要解析的文本
+ */
+Parser.prototype.parse = function(content) {
+	// 插件处理
+	for (let i = this.plugins.length; i--;) {
+		if (this.plugins[i].onUpdate) {
+			content = this.plugins[i].onUpdate(content, config) || content
+		}
+	}
+
+	new Lexer(this).parse(content)
+	// 出栈未闭合的标签
+	while (this.stack.length) {
+		this.popNode()
+	}
+	if (this.nodes.length > 50) {
+		mergeNodes(this.nodes)
+	}
+	return this.nodes
+}
+
+/**
+ * @description 将标签暴露出来(不被 rich-text 包含)
+ */
+Parser.prototype.expose = function() {
+	// #ifndef APP-PLUS-NVUE
+	for (let i = this.stack.length; i--;) {
+		const item = this.stack[i]
+		if (item.c || item.name === 'a' || item.name === 'video' || item.name === 'audio') return
+		item.c = 1
+	}
+	// #endif
+}
+
+/**
+ * @description 处理插件
+ * @param {Object} node 要处理的标签
+ * @returns {Boolean} 是否要移除此标签
+ */
+Parser.prototype.hook = function(node) {
+	for (let i = this.plugins.length; i--;) {
+		if (this.plugins[i].onParse && this.plugins[i].onParse(node, this) === false) {
+			return false
+		}
+	}
+	return true
+}
+
+/**
+ * @description 将链接拼接上主域名
+ * @param {String} url 需要拼接的链接
+ * @returns {String} 拼接后的链接
+ */
+Parser.prototype.getUrl = function(url) {
+	const domain = this.options.domain
+	if (url[0] === '/') {
+		if (url[1] === '/') {
+			// // 开头的补充协议名
+			url = (domain ? domain.split('://')[0] : 'http') + ':' + url
+		} else if (domain) {
+			// 否则补充整个域名
+			url = domain + url
+		} /* #ifdef APP-PLUS */
+		else {
+			url = plus.io.convertLocalFileSystemURL(url)
+		} /* #endif */
+	} else if (!url.includes('data:') && !url.includes('://')) {
+		if (domain) {
+			url = domain + '/' + url
+		} /* #ifdef APP-PLUS */
+		else {
+			url = plus.io.convertLocalFileSystemURL(url)
+		} /* #endif */
+	}
+	return url
+}
+
+/**
+ * @description 解析样式表
+ * @param {Object} node 标签
+ * @returns {Object}
+ */
+Parser.prototype.parseStyle = function(node) {
+	const attrs = node.attrs
+	const list = (this.tagStyle[node.name] || '').split(';').concat((attrs.style || '').split(';'))
+	const styleObj = {}
+	let tmp = ''
+
+	if (attrs.id && !this.xml) {
+		// 暴露锚点
+		if (this.options.useAnchor) {
+			this.expose()
+		} else if (node.name !== 'img' && node.name !== 'a' && node.name !== 'video' && node.name !== 'audio') {
+			attrs.id = undefined
+		}
+	}
+
+	// 转换 width 和 height 属性
+	if (attrs.width) {
+		styleObj.width = parseFloat(attrs.width) + (attrs.width.includes('%') ? '%' : 'px')
+		attrs.width = undefined
+	}
+	if (attrs.height) {
+		styleObj.height = parseFloat(attrs.height) + (attrs.height.includes('%') ? '%' : 'px')
+		attrs.height = undefined
+	}
+
+	for (let i = 0, len = list.length; i < len; i++) {
+		const info = list[i].split(':')
+		if (info.length < 2) continue
+		const key = info.shift().trim().toLowerCase()
+		let value = info.join(':').trim()
+		if ((value[0] === '-' && value.lastIndexOf('-') > 0) || value.includes('safe')) {
+			// 兼容性的 css 不压缩
+			tmp += `;${key}:${value}`
+		} else if (!styleObj[key] || value.includes('import') || !styleObj[key].includes('import')) {
+			// 重复的样式进行覆盖
+			if (value.includes('url')) {
+				// 填充链接
+				let j = value.indexOf('(') + 1
+				if (j) {
+					while (value[j] === '"' || value[j] === "'" || blankChar[value[j]]) {
+						j++
+					}
+					value = value.substr(0, j) + this.getUrl(value.substr(j))
+				}
+			} else if (value.includes('rpx')) {
+				// 转换 rpx(rich-text 内部不支持 rpx)
+				value = value.replace(/[0-9.]+\s*rpx/g, $ => parseFloat($) * windowWidth / 750 + 'px')
+			}
+			styleObj[key] = value
+		}
+	}
+
+	node.attrs.style = tmp
+	return styleObj
+}
+
+/**
+ * @description 解析到标签名
+ * @param {String} name 标签名
+ * @private
+ */
+Parser.prototype.onTagName = function(name) {
+	this.tagName = this.xml ? name : name.toLowerCase()
+	if (this.tagName === 'svg') {
+		this.xml = (this.xml || 0) + 1 // svg 标签内大小写敏感
+	}
+}
+
+/**
+ * @description 解析到属性名
+ * @param {String} name 属性名
+ * @private
+ */
+Parser.prototype.onAttrName = function(name) {
+	name = this.xml ? name : name.toLowerCase()
+	if (name.substr(0, 5) === 'data-') {
+		if (name === 'data-src' && !this.attrs.src) {
+			// data-src 自动转为 src
+			this.attrName = 'src'
+		} else if (this.tagName === 'img' || this.tagName === 'a') {
+			// a 和 img 标签保留 data- 的属性,可以在 imgtap 和 linktap 事件中使用
+			this.attrName = name
+		} else {
+			// 剩余的移除以减小大小
+			this.attrName = undefined
+		}
+	} else {
+		this.attrName = name
+		this.attrs[name] = 'T' // boolean 型属性缺省设置
+	}
+}
+
+/**
+ * @description 解析到属性值
+ * @param {String} val 属性值
+ * @private
+ */
+Parser.prototype.onAttrVal = function(val) {
+	// console.log(val, '解码转码')
+	const name = this.attrName || ''
+	if (name === 'style' || name === 'href') {
+		// 部分属性进行实体解码
+		this.attrs[name] = decodeEntity(val, true)
+	} else if (name.includes('src')) {
+		// 拼接主域名
+		this.attrs[name] = this.getUrl(decodeEntity(val, true))
+	} else if (name) {
+		this.attrs[name] = val
+	}
+}
+
+/**
+ * @description 解析到标签开始
+ * @param {Boolean} selfClose 是否有自闭合标识 />
+ * @private
+ */
+Parser.prototype.onOpenTag = function(selfClose) {
+	// 拼装 node
+	const node = Object.create(null)
+	node.name = this.tagName
+	node.attrs = this.attrs
+	// 避免因为自动 diff 使得 type 被设置为 null 导致部分内容不显示
+	if (this.options.nodes.length) {
+		node.type = 'node'
+	}
+	this.attrs = Object.create(null)
+
+	const attrs = node.attrs
+	const parent = this.stack[this.stack.length - 1]
+	const siblings = parent ? parent.children : this.nodes
+	const close = this.xml ? selfClose : config.voidTags[node.name]
+
+	// 替换标签名选择器
+	if (tagSelector[node.name]) {
+		attrs.class = tagSelector[node.name] + (attrs.class ? ' ' + attrs.class : '')
+	}
+
+	// 转换 embed 标签
+	if (node.name === 'embed') {
+		// #ifndef H5 || APP-PLUS
+		const src = attrs.src || ''
+		// 按照后缀名和 type 将 embed 转为 video 或 audio
+		if (src.includes('.mp4') || src.includes('.3gp') || src.includes('.m3u8') || (attrs.type || '').includes(
+				'video')) {
+			node.name = 'video'
+		} else if (src.includes('.mp3') || src.includes('.wav') || src.includes('.aac') || src.includes('.m4a') || (
+				attrs.type || '').includes('audio')) {
+			node.name = 'audio'
+		}
+		if (attrs.autostart) {
+			attrs.autoplay = 'T'
+		}
+		attrs.controls = 'T'
+		// #endif
+		// #ifdef H5 || APP-PLUS
+		this.expose()
+		// #endif
+	}
+
+	// #ifndef APP-PLUS-NVUE
+	// 处理音视频
+	if (node.name === 'video' || node.name === 'audio') {
+		// 设置 id 以便获取 context
+		if (node.name === 'video' && !attrs.id) {
+			attrs.id = 'v' + idIndex++
+		}
+		// 没有设置 controls 也没有设置 autoplay 的自动设置 controls
+		if (!attrs.controls && !attrs.autoplay) {
+			attrs.controls = 'T'
+		}
+		// 用数组存储所有可用的 source
+		node.src = []
+		if (attrs.src) {
+			node.src.push(attrs.src)
+			attrs.src = undefined
+		}
+		this.expose()
+	}
+	// #endif
+
+	// 处理自闭合标签
+	if (close) {
+		if (!this.hook(node) || config.ignoreTags[node.name]) {
+			// 通过 base 标签设置主域名
+			if (node.name === 'base' && !this.options.domain) {
+				this.options.domain = attrs.href
+			} /* #ifndef APP-PLUS-NVUE */
+			else if (node.name === 'source' && parent && (parent.name === 'video' || parent.name === 'audio') &&
+				attrs.src) {
+				// 设置 source 标签(仅父节点为 video 或 audio 时有效)
+				parent.src.push(attrs.src)
+			} /* #endif */
+			return
+		}
+
+		// 解析 style
+		const styleObj = this.parseStyle(node)
+
+		// 处理图片
+		if (node.name === 'img') {
+			if (attrs.src) {
+				// 标记 webp
+				if (attrs.src.includes('webp')) {
+					node.webp = 'T'
+				}
+				// data url 图片如果没有设置 original-src 默认为不可预览的小图片
+				if (attrs.src.includes('data:') && !attrs['original-src']) {
+					attrs.ignore = 'T'
+				}
+				if (!attrs.ignore || node.webp || attrs.src.includes('cloud://')) {
+					for (let i = this.stack.length; i--;) {
+						const item = this.stack[i]
+						if (item.name === 'a') {
+							node.a = item.attrs
+						}
+						if (item.name === 'table' && !node.webp && !attrs.src.includes('cloud://')) {
+							if (!styleObj.display || styleObj.display.includes('inline')) {
+								node.t = 'inline-block'
+							} else {
+								node.t = styleObj.display
+							}
+							styleObj.display = undefined
+						}
+						// #ifndef H5 || APP-PLUS
+						const style = item.attrs.style || ''
+						if (style.includes('flex:') && !style.includes('flex:0') && !style.includes('flex: 0') && (!
+								styleObj.width || parseInt(styleObj.width) > 100)) {
+							styleObj.width = '100% !important'
+							styleObj.height = ''
+							for (let j = i + 1; j < this.stack.length; j++) {
+								this.stack[j].attrs.style = (this.stack[j].attrs.style || '').replace('inline-', '')
+							}
+						} else if (style.includes('flex') && styleObj.width === '100%') {
+							for (let j = i + 1; j < this.stack.length; j++) {
+								const style = this.stack[j].attrs.style || ''
+								if (!style.includes(';width') && !style.includes(' width') && style.indexOf(
+										'width') !== 0) {
+									styleObj.width = ''
+									break
+								}
+							}
+						} else if (style.includes('inline-block')) {
+							if (styleObj.width && styleObj.width[styleObj.width.length - 1] === '%') {
+								item.attrs.style += ';max-width:' + styleObj.width
+								styleObj.width = ''
+							} else {
+								item.attrs.style += ';max-width:100%'
+							}
+						}
+						// #endif
+						item.c = 1
+					}
+					attrs.i = this.imgList.length.toString()
+					let src = attrs['original-src'] || attrs.src
+					// #ifndef H5 || MP-ALIPAY || APP-PLUS || MP-360
+					if (this.imgList.includes(src)) {
+						// 如果有重复的链接则对域名进行随机大小写变换避免预览时错位
+						let i = src.indexOf('://')
+						if (i !== -1) {
+							i += 3
+							let newSrc = src.substr(0, i)
+							for (; i < src.length; i++) {
+								if (src[i] === '/') break
+								newSrc += Math.random() > 0.5 ? src[i].toUpperCase() : src[i]
+							}
+							newSrc += src.substr(i)
+							src = newSrc
+						}
+					}
+					// #endif
+					this.imgList.push(src)
+					if (!node.t) {
+						this.imgList._unloadimgs += 1
+					}
+					// #ifdef H5 || APP-PLUS
+					if (this.options.lazyLoad) {
+						attrs['data-src'] = attrs.src
+						attrs.src = undefined
+					}
+					// #endif
+				}
+			}
+			if (styleObj.display === 'inline') {
+				styleObj.display = ''
+			}
+			// #ifndef APP-PLUS-NVUE
+			if (attrs.ignore) {
+				styleObj['max-width'] = styleObj['max-width'] || '100%'
+				attrs.style += ';-webkit-touch-callout:none'
+			}
+			// #endif
+			// 设置的宽度超出屏幕,为避免变形,高度转为自动
+			if (parseInt(styleObj.width) > windowWidth) {
+				styleObj.height = undefined
+			}
+			// 记录是否设置了宽高
+			if (!isNaN(parseInt(styleObj.width))) {
+				node.w = 'T'
+			}
+			if (!isNaN(parseInt(styleObj.height)) && (!styleObj.height.includes('%') || (parent && (parent.attrs
+					.style || '').includes('height')))) {
+				node.h = 'T'
+			}
+		} else if (node.name === 'svg') {
+			siblings.push(node)
+			this.stack.push(node)
+			this.popNode()
+			return
+		}
+		for (const key in styleObj) {
+			if (styleObj[key]) {
+				attrs.style += `;${key}:${styleObj[key].replace(' !important', '')}`
+			}
+		}
+		attrs.style = attrs.style.substr(1) || undefined
+		// #ifdef (MP-WEIXIN || MP-QQ) && VUE3
+		if (!attrs.style) {
+			delete attrs.style
+		}
+		// #endif
+	} else {
+		if ((node.name === 'pre' || ((attrs.style || '').includes('white-space') && attrs.style.includes('pre'))) &&
+			this.pre !== 2) {
+			this.pre = node.pre = 1
+		}
+		node.children = []
+		this.stack.push(node)
+	}
+
+	// 加入节点树
+	siblings.push(node)
+}
+
+/**
+ * @description 解析到标签结束
+ * @param {String} name 标签名
+ * @private
+ */
+Parser.prototype.onCloseTag = function(name) {
+	// 依次出栈到匹配为止
+	name = this.xml ? name : name.toLowerCase()
+	let i
+	for (i = this.stack.length; i--;) {
+		if (this.stack[i].name === name) break
+	}
+	if (i !== -1) {
+		while (this.stack.length > i) {
+			this.popNode()
+		}
+	} else if (name === 'p' || name === 'br') {
+		const siblings = this.stack.length ? this.stack[this.stack.length - 1].children : this.nodes
+		siblings.push({
+			name,
+			attrs: {
+				class: tagSelector[name] || '',
+				style: this.tagStyle[name] || ''
+			}
+		})
+	}
+}
+
+/**
+ * @description 处理标签出栈
+ * @private
+ */
+Parser.prototype.popNode = function() {
+	const node = this.stack.pop()
+	let attrs = node.attrs
+	const children = node.children
+	const parent = this.stack[this.stack.length - 1]
+	const siblings = parent ? parent.children : this.nodes
+
+	if (!this.hook(node) || config.ignoreTags[node.name]) {
+		// 获取标题
+		if (node.name === 'title' && children.length && children[0].type === 'text' && this.options.setTitle) {
+			uni.setNavigationBarTitle({
+				title: children[0].text
+			})
+		}
+		siblings.pop()
+		return
+	}
+
+	if (node.pre && this.pre !== 2) {
+		// 是否合并空白符标识
+		this.pre = node.pre = undefined
+		for (let i = this.stack.length; i--;) {
+			if (this.stack[i].pre) {
+				this.pre = 1
+			}
+		}
+	}
+
+	const styleObj = {}
+
+	// 转换 svg
+	if (node.name === 'svg') {
+		if (this.xml > 1) {
+			// 多层 svg 嵌套
+			this.xml--
+			return
+		}
+		// #ifdef APP-PLUS-NVUE
+		(function traversal(node) {
+			if (node.name) {
+				// 调整 svg 的大小写
+				node.name = config.svgDict[node.name] || node.name
+				for (const item in node.attrs) {
+					if (config.svgDict[item]) {
+						node.attrs[config.svgDict[item]] = node.attrs[item]
+						node.attrs[item] = undefined
+					}
+				}
+				for (let i = 0; i < (node.children || []).length; i++) {
+					traversal(node.children[i])
+				}
+			}
+		})(node)
+		// #endif
+		// #ifndef APP-PLUS-NVUE
+		let src = ''
+		const style = attrs.style
+		attrs.style = ''
+		attrs.xmlns = 'http://www.w3.org/2000/svg';
+		(function traversal(node) {
+			if (node.type === 'text') {
+				src += node.text
+				return
+			}
+			const name = config.svgDict[node.name] || node.name
+			src += '<' + name
+			for (const item in node.attrs) {
+				const val = node.attrs[item]
+				if (val) {
+					src += ` ${config.svgDict[item] || item}="${val}"`
+				}
+			}
+			if (!node.children) {
+				src += '/>'
+			} else {
+				src += '>'
+				for (let i = 0; i < node.children.length; i++) {
+					traversal(node.children[i])
+				}
+				src += '</' + name + '>'
+			}
+		})(node)
+		node.name = 'img'
+		node.attrs = {
+			src: 'data:image/svg+xml;utf8,' + src.replace(/#/g, '%23'),
+			style,
+			ignore: 'T'
+		}
+		node.children = undefined
+		// #endif
+		this.xml = false
+		return
+	}
+
+	// #ifndef APP-PLUS-NVUE
+	// 转换 align 属性
+	if (attrs.align) {
+		if (node.name === 'table') {
+			if (attrs.align === 'center') {
+				styleObj['margin-inline-start'] = styleObj['margin-inline-end'] = 'auto'
+			} else {
+				styleObj.float = attrs.align
+			}
+		} else {
+			styleObj['text-align'] = attrs.align
+		}
+		attrs.align = undefined
+	}
+
+	// 转换 dir 属性
+	if (attrs.dir) {
+		styleObj.direction = attrs.dir
+		attrs.dir = undefined
+	}
+
+	// 转换 font 标签的属性
+	if (node.name === 'font') {
+		if (attrs.color) {
+			styleObj.color = attrs.color
+			attrs.color = undefined
+		}
+		if (attrs.face) {
+			styleObj['font-family'] = attrs.face
+			attrs.face = undefined
+		}
+		if (attrs.size) {
+			let size = parseInt(attrs.size)
+			if (!isNaN(size)) {
+				if (size < 1) {
+					size = 1
+				} else if (size > 7) {
+					size = 7
+				}
+				styleObj['font-size'] = ['x-small', 'small', 'medium', 'large', 'x-large', 'xx-large', 'xxx-large'][
+					size - 1
+				]
+			}
+			attrs.size = undefined
+		}
+	}
+	// #endif
+
+	// 一些编辑器的自带 class
+	if ((attrs.class || '').includes('align-center')) {
+		styleObj['text-align'] = 'center'
+	}
+
+	Object.assign(styleObj, this.parseStyle(node))
+
+	if (node.name !== 'table' && parseInt(styleObj.width) > windowWidth) {
+		styleObj['max-width'] = '100%'
+		styleObj['box-sizing'] = 'border-box'
+	}
+
+	// #ifndef APP-PLUS-NVUE
+	if (config.blockTags[node.name]) {
+		node.name = 'div'
+	} else if (!config.trustTags[node.name] && !this.xml) {
+		// 未知标签转为 span,避免无法显示
+		node.name = 'span'
+	}
+
+	if (node.name === 'a' || node.name === 'ad'
+		// #ifdef H5 || APP-PLUS
+		||
+		node.name === 'iframe' // eslint-disable-line
+		// #endif
+	) {
+		this.expose()
+	} else if (node.name === 'video') {
+		if ((styleObj.height || '').includes('auto')) {
+			styleObj.height = undefined
+		}
+		/* #ifdef APP-PLUS */
+		let str = '<video style="width:100%;height:100%"'
+		for (const item in attrs) {
+			if (attrs[item]) {
+				str += ' ' + item + '="' + attrs[item] + '"'
+			}
+		}
+		if (this.options.pauseVideo) {
+			str +=
+				' onplay="this.dispatchEvent(new CustomEvent(\'vplay\',{bubbles:!0}));for(var e=document.getElementsByTagName(\'video\'),t=0;t<e.length;t++)e[t]!=this&&e[t].pause()"'
+		}
+		str += '>'
+		for (let i = 0; i < node.src.length; i++) {
+			str += '<source src="' + node.src[i] + '">'
+		}
+		str += '</video>'
+		node.html = str
+		/* #endif */
+	} else if ((node.name === 'ul' || node.name === 'ol') && node.c) {
+		// 列表处理
+		const types = {
+			a: 'lower-alpha',
+			A: 'upper-alpha',
+			i: 'lower-roman',
+			I: 'upper-roman'
+		}
+		if (types[attrs.type]) {
+			attrs.style += ';list-style-type:' + types[attrs.type]
+			attrs.type = undefined
+		}
+		for (let i = children.length; i--;) {
+			if (children[i].name === 'li') {
+				children[i].c = 1
+			}
+		}
+	} else if (node.name === 'table') {
+		// 表格处理
+		// cellpadding、cellspacing、border 这几个常用表格属性需要通过转换实现
+		let padding = parseFloat(attrs.cellpadding)
+		let spacing = parseFloat(attrs.cellspacing)
+		const border = parseFloat(attrs.border)
+		const bordercolor = styleObj['border-color']
+		const borderstyle = styleObj['border-style']
+		if (node.c) {
+			// padding 和 spacing 默认 2
+			if (isNaN(padding)) {
+				padding = 2
+			}
+			if (isNaN(spacing)) {
+				spacing = 2
+			}
+		}
+		if (border) {
+			attrs.style += `;border:${border}px ${borderstyle || 'solid'} ${bordercolor || 'gray'}`
+		}
+		if (node.flag && node.c) {
+			// 有 colspan 或 rowspan 且含有链接的表格通过 grid 布局实现
+			styleObj.display = 'grid'
+			if (spacing) {
+				styleObj['grid-gap'] = spacing + 'px'
+				styleObj.padding = spacing + 'px'
+			} else if (border) {
+				// 无间隔的情况下避免边框重叠
+				attrs.style += ';border-left:0;border-top:0'
+			}
+
+			const width = [] // 表格的列宽
+			const trList = [] // tr 列表
+			const cells = [] // 保存新的单元格
+			const map = {}; // 被合并单元格占用的格子
+
+			(function traversal(nodes) {
+				for (let i = 0; i < nodes.length; i++) {
+					if (nodes[i].name === 'tr') {
+						trList.push(nodes[i])
+					} else {
+						traversal(nodes[i].children || [])
+					}
+				}
+			})(children)
+
+			for (let row = 1; row <= trList.length; row++) {
+				let col = 1
+				for (let j = 0; j < trList[row - 1].children.length; j++) {
+					const td = trList[row - 1].children[j]
+					if (td.name === 'td' || td.name === 'th') {
+						// 这个格子被上面的单元格占用,则列号++
+						while (map[row + '.' + col]) {
+							col++
+						}
+						let style = td.attrs.style || ''
+						let start = style.indexOf('width') ? style.indexOf(';width') : 0
+						// 提取出 td 的宽度
+						if (start !== -1) {
+							let end = style.indexOf(';', start + 6)
+							if (end === -1) {
+								end = style.length
+							}
+							if (!td.attrs.colspan) {
+								width[col] = style.substring(start ? start + 7 : 6, end)
+							}
+							style = style.substr(0, start) + style.substr(end)
+						}
+						// 设置竖直对齐
+						style += ';display:flex'
+						start = style.indexOf('vertical-align')
+						if (start !== -1) {
+							const val = style.substr(start + 15, 10)
+							if (val.includes('middle')) {
+								style += ';align-items:center'
+							} else if (val.includes('bottom')) {
+								style += ';align-items:flex-end'
+							}
+						} else {
+							style += ';align-items:center'
+						}
+						// 设置水平对齐
+						start = style.indexOf('text-align')
+						if (start !== -1) {
+							const val = style.substr(start + 11, 10)
+							if (val.includes('center')) {
+								style += ';justify-content: center'
+							} else if (val.includes('right')) {
+								style += ';justify-content: right'
+							}
+						}
+						style = (border ? `;border:${border}px ${borderstyle || 'solid'} ${bordercolor || 'gray'}` +
+							(spacing ? '' : ';border-right:0;border-bottom:0') : '') + (padding ?
+							`;padding:${padding}px` : '') + ';' + style
+						// 处理列合并
+						if (td.attrs.colspan) {
+							style += `;grid-column-start:${col};grid-column-end:${col + parseInt(td.attrs.colspan)}`
+							if (!td.attrs.rowspan) {
+								style += `;grid-row-start:${row};grid-row-end:${row + 1}`
+							}
+							col += parseInt(td.attrs.colspan) - 1
+						}
+						// 处理行合并
+						if (td.attrs.rowspan) {
+							style += `;grid-row-start:${row};grid-row-end:${row + parseInt(td.attrs.rowspan)}`
+							if (!td.attrs.colspan) {
+								style += `;grid-column-start:${col};grid-column-end:${col + 1}`
+							}
+							// 记录下方单元格被占用
+							for (let rowspan = 1; rowspan < td.attrs.rowspan; rowspan++) {
+								for (let colspan = 0; colspan < (td.attrs.colspan || 1); colspan++) {
+									map[(row + rowspan) + '.' + (col - colspan)] = 1
+								}
+							}
+						}
+						if (style) {
+							td.attrs.style = style
+						}
+						cells.push(td)
+						col++
+					}
+				}
+				if (row === 1) {
+					let temp = ''
+					for (let i = 1; i < col; i++) {
+						temp += (width[i] ? width[i] : 'auto') + ' '
+					}
+					styleObj['grid-template-columns'] = temp
+				}
+			}
+			node.children = cells
+		} else {
+			// 没有使用合并单元格的表格通过 table 布局实现
+			if (node.c) {
+				styleObj.display = 'table'
+			}
+			if (!isNaN(spacing)) {
+				styleObj['border-spacing'] = spacing + 'px'
+			}
+			if (border || padding) {
+				// 遍历
+				(function traversal(nodes) {
+					for (let i = 0; i < nodes.length; i++) {
+						const td = nodes[i]
+						if (td.name === 'th' || td.name === 'td') {
+							if (border) {
+								td.attrs.style =
+									`border:${border}px ${borderstyle || 'solid'} ${bordercolor || 'gray'};${td.attrs.style || ''}`
+							}
+							if (padding) {
+								td.attrs.style = `padding:${padding}px;${td.attrs.style || ''}`
+							}
+						} else if (td.children) {
+							traversal(td.children)
+						}
+					}
+				})(children)
+			}
+		}
+		// 给表格添加一个单独的横向滚动层
+		if (this.options.scrollTable && !(attrs.style || '').includes('inline')) {
+			const table = Object.assign({}, node)
+			node.name = 'div'
+			node.attrs = {
+				style: 'overflow:auto'
+			}
+			node.children = [table]
+			attrs = table.attrs
+		}
+	} else if ((node.name === 'td' || node.name === 'th') && (attrs.colspan || attrs.rowspan)) {
+		for (let i = this.stack.length; i--;) {
+			if (this.stack[i].name === 'table') {
+				this.stack[i].flag = 1 // 指示含有合并单元格
+				break
+			}
+		}
+	} else if (node.name === 'ruby') {
+		// 转换 ruby
+		node.name = 'span'
+		for (let i = 0; i < children.length - 1; i++) {
+			if (children[i].type === 'text' && children[i + 1].name === 'rt') {
+				children[i] = {
+					name: 'div',
+					attrs: {
+						style: 'display:inline-block;text-align:center'
+					},
+					children: [{
+						name: 'div',
+						attrs: {
+							style: 'font-size:50%;' + (children[i + 1].attrs.style || '')
+						},
+						children: children[i + 1].children
+					}, children[i]]
+				}
+				children.splice(i + 1, 1)
+			}
+		}
+	} else if (node.c) {
+		(function traversal(node) {
+			node.c = 2
+			for (let i = node.children.length; i--;) {
+				const child = node.children[i]
+				// #ifdef (MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE3
+				if (child.name && (config.inlineTags[child.name] || ((child.attrs.style || '').includes(
+						'inline') && child.children)) && !child.c) {
+					traversal(child)
+				}
+				// #endif
+				if (!child.c || child.name === 'table') {
+					node.c = 1
+				}
+			}
+		})(node)
+	}
+
+	if ((styleObj.display || '').includes('flex') && !node.c) {
+		for (let i = children.length; i--;) {
+			const item = children[i]
+			if (item.f) {
+				item.attrs.style = (item.attrs.style || '') + item.f
+				item.f = undefined
+			}
+		}
+	}
+	// flex 布局时部分样式需要提取到 rich-text 外层
+	const flex = parent && ((parent.attrs.style || '').includes('flex') || (parent.attrs.style || '').includes(
+			'grid'))
+		// #ifdef MP-WEIXIN
+		// 检查基础库版本 virtualHost 是否可用
+		&&
+		!(node.c && wx.getNFCAdapter) // eslint-disable-line
+		// #endif
+		// #ifndef MP-WEIXIN || MP-QQ || MP-BAIDU || MP-TOUTIAO
+		&&
+		!node.c // eslint-disable-line
+	// #endif
+	if (flex) {
+		node.f = ';max-width:100%'
+	}
+
+	if (children.length >= 50 && node.c && !(styleObj.display || '').includes('flex')) {
+		mergeNodes(children)
+	}
+	// #endif
+
+	for (const key in styleObj) {
+		if (styleObj[key]) {
+			const val = `;${key}:${styleObj[key].replace(' !important', '')}`
+			/* #ifndef APP-PLUS-NVUE */
+			if (flex && ((key.includes('flex') && key !== 'flex-direction') || key === 'align-self' || key.includes(
+					'grid') || styleObj[key][0] === '-' || (key.includes('width') && val.includes('%')))) {
+				node.f += val
+				if (key === 'width') {
+					attrs.style += ';width:100%'
+				}
+			} else /* #endif */ {
+				attrs.style += val
+			}
+		}
+	}
+	attrs.style = attrs.style.substr(1) || undefined
+	// #ifdef (MP-WEIXIN || MP-QQ) && VUE3
+	for (const key in attrs) {
+		if (!attrs[key]) {
+			delete attrs[key]
+		}
+	}
+	// #endif
+}
+
+/**
+ * @description 解析到文本
+ * @param {String} text 文本内容
+ */
+Parser.prototype.onText = function(text) {
+	if (!this.pre) {
+		// 合并空白符
+		let trim = ''
+		let flag
+		for (let i = 0, len = text.length; i < len; i++) {
+			if (!blankChar[text[i]]) {
+				trim += text[i]
+			} else {
+				if (trim[trim.length - 1] !== ' ') {
+					trim += ' '
+				}
+				if (text[i] === '\n' && !flag) {
+					flag = true
+				}
+			}
+		}
+		// 去除含有换行符的空串
+		if (trim === ' ') {
+			if (flag) return
+			// #ifdef VUE3
+			else {
+				const parent = this.stack[this.stack.length - 1]
+				if (parent && parent.name[0] === 't') return
+			}
+			// #endif
+		}
+		text = trim
+	}
+	const node = Object.create(null)
+	node.type = 'text'
+	// #ifdef (MP-BAIDU || MP-ALIPAY || MP-TOUTIAO) && VUE3
+	node.attrs = {}
+	// #endif
+	node.text = decodeEntity(text)
+	if (this.hook(node)) {
+		// #ifdef MP-WEIXIN
+		if (this.options.selectable === 'force' && system.includes('iOS') && !uni.canIUse(
+				'rich-text.user-select')) {
+			this.expose()
+		}
+		// #endif
+		const siblings = this.stack.length ? this.stack[this.stack.length - 1].children : this.nodes
+		siblings.push(node)
+	}
+}
+
+/**
+ * @description html 词法分析器
+ * @param {Object} handler 高层处理器
+ */
+function Lexer(handler) {
+	this.handler = handler
+}
+
+/**
+ * @description 执行解析
+ * @param {String} content 要解析的文本
+ */
+Lexer.prototype.parse = function(content) {
+	this.content = content || ''
+	this.i = 0 // 标记解析位置
+	this.start = 0 // 标记一个单词的开始位置
+	this.state = this.text // 当前状态
+	for (let len = this.content.length; this.i !== -1 && this.i < len;) {
+		this.state()
+	}
+}
+
+/**
+ * @description 检查标签是否闭合
+ * @param {String} method 如果闭合要进行的操作
+ * @returns {Boolean} 是否闭合
+ * @private
+ */
+Lexer.prototype.checkClose = function(method) {
+	const selfClose = this.content[this.i] === '/'
+	if (this.content[this.i] === '>' || (selfClose && this.content[this.i + 1] === '>')) {
+		if (method) {
+			this.handler[method](this.content.substring(this.start, this.i))
+		}
+		this.i += selfClose ? 2 : 1
+		this.start = this.i
+		this.handler.onOpenTag(selfClose)
+		if (this.handler.tagName === 'script') {
+			this.i = this.content.indexOf('</', this.i)
+			if (this.i !== -1) {
+				this.i += 2
+				this.start = this.i
+			}
+			this.state = this.endTag
+		} else {
+			this.state = this.text
+		}
+		return true
+	}
+	return false
+}
+
+/**
+ * @description 文本状态
+ * @private
+ */
+Lexer.prototype.text = function() {
+	this.i = this.content.indexOf('<', this.i) // 查找最近的标签
+	if (this.i === -1) {
+		// 没有标签了
+		if (this.start < this.content.length) {
+			this.handler.onText(this.content.substring(this.start, this.content.length))
+		}
+		return
+	}
+	const c = this.content[this.i + 1]
+	if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
+		// 标签开头
+		if (this.start !== this.i) {
+			this.handler.onText(this.content.substring(this.start, this.i))
+		}
+		this.start = ++this.i
+		this.state = this.tagName
+	} else if (c === '/' || c === '!' || c === '?') {
+		if (this.start !== this.i) {
+			this.handler.onText(this.content.substring(this.start, this.i))
+		}
+		const next = this.content[this.i + 2]
+		if (c === '/' && ((next >= 'a' && next <= 'z') || (next >= 'A' && next <= 'Z'))) {
+			// 标签结尾
+			this.i += 2
+			this.start = this.i
+			this.state = this.endTag
+			return
+		}
+		// 处理注释
+		let end = '-->'
+		if (c !== '!' || this.content[this.i + 2] !== '-' || this.content[this.i + 3] !== '-') {
+			end = '>'
+		}
+		this.i = this.content.indexOf(end, this.i)
+		if (this.i !== -1) {
+			this.i += end.length
+			this.start = this.i
+		}
+	} else {
+		this.i++
+	}
+}
+
+/**
+ * @description 标签名状态
+ * @private
+ */
+Lexer.prototype.tagName = function() {
+	if (blankChar[this.content[this.i]]) {
+		// 解析到标签名
+		this.handler.onTagName(this.content.substring(this.start, this.i))
+		while (blankChar[this.content[++this.i]]);
+		if (this.i < this.content.length && !this.checkClose()) {
+			this.start = this.i
+			this.state = this.attrName
+		}
+	} else if (!this.checkClose('onTagName')) {
+		this.i++
+	}
+}
+
+/**
+ * @description 属性名状态
+ * @private
+ */
+Lexer.prototype.attrName = function() {
+	let c = this.content[this.i]
+	if (blankChar[c] || c === '=') {
+		// 解析到属性名
+		this.handler.onAttrName(this.content.substring(this.start, this.i))
+		let needVal = c === '='
+		const len = this.content.length
+		while (++this.i < len) {
+			c = this.content[this.i]
+			if (!blankChar[c]) {
+				if (this.checkClose()) return
+				if (needVal) {
+					// 等号后遇到第一个非空字符
+					this.start = this.i
+					this.state = this.attrVal
+					return
+				}
+				if (this.content[this.i] === '=') {
+					needVal = true
+				} else {
+					this.start = this.i
+					this.state = this.attrName
+					return
+				}
+			}
+		}
+	} else if (!this.checkClose('onAttrName')) {
+		this.i++
+	}
+}
+
+/**
+ * @description 属性值状态
+ * @private
+ */
+Lexer.prototype.attrVal = function() {
+	const c = this.content[this.i]
+	const len = this.content.length
+	if (c === '"' || c === "'") {
+		// 有冒号的属性
+		this.start = ++this.i
+		this.i = this.content.indexOf(c, this.i)
+		if (this.i === -1) return
+		this.handler.onAttrVal(this.content.substring(this.start, this.i))
+	} else {
+		// 没有冒号的属性
+		for (; this.i < len; this.i++) {
+			if (blankChar[this.content[this.i]]) {
+				this.handler.onAttrVal(this.content.substring(this.start, this.i))
+				break
+			} else if (this.checkClose('onAttrVal')) return
+		}
+	}
+	while (blankChar[this.content[++this.i]]);
+	if (this.i < len && !this.checkClose()) {
+		this.start = this.i
+		this.state = this.attrName
+	}
+}
+
+/**
+ * @description 结束标签状态
+ * @returns {String} 结束的标签名
+ * @private
+ */
+Lexer.prototype.endTag = function() {
+	const c = this.content[this.i]
+	if (blankChar[c] || c === '>' || c === '/') {
+		this.handler.onCloseTag(this.content.substring(this.start, this.i))
+		if (c !== '>') {
+			this.i = this.content.indexOf('>', this.i)
+			if (this.i === -1) return
+		}
+		this.start = ++this.i
+		this.state = this.text
+	} else {
+		this.i++
+	}
+}
+
+export default Parser

+ 75 - 75
uni_modules/mp-html/package.json

@@ -1,76 +1,76 @@
-{
-    "id": "mp-html",
-    "displayName": "mp-html 富文本组件【全端支持,支持编辑、latex等扩展】",
-    "version": "v2.4.1",
-    "description": "一个强大的富文本组件,高效轻量,功能丰富",
-    "keywords": [
-        "富文本",
-        "编辑器",
-        "html",
-        "rich-text",
-        "editor"
-    ],
-    "repository": "https://github.com/jin-yufeng/mp-html",
-    "dcloudext": {
-        "sale": {
-            "regular": {
-                "price": "0.00"
-            },
-            "sourcecode": {
-                "price": "0.00"
-            }
-        },
-        "contact": {
-            "qq": ""
-        },
-        "declaration": {
-            "ads": "无",
-            "data": "无",
-            "permissions": "无"
-        },
-        "npmurl": "https://www.npmjs.com/package/mp-html",
-        "type": "component-vue"
-    },
-    "uni_modules": {
-        "platforms": {
-            "cloud": {
-                "tcb": "y",
-                "aliyun": "y"
-            },
-            "client": {
-                "App": {
-                    "app-vue": "y",
-                    "app-nvue": "y"
-                },
-                "H5-mobile": {
-                    "Safari": "y",
-                    "Android Browser": "y",
-                    "微信浏览器(Android)": "y",
-                    "QQ浏览器(Android)": "y"
-                },
-                "H5-pc": {
-                    "Chrome": "y",
-                    "IE": "u",
-                    "Edge": "y",
-                    "Firefox": "y",
-                    "Safari": "y"
-                },
-                "小程序": {
-                    "微信": "y",
-                    "阿里": "y",
-                    "百度": "y",
-                    "字节跳动": "y",
-                    "QQ": "y"
-                },
-                "快应用": {
-                    "华为": "y",
-                    "联盟": "y"
-                },
-                "Vue": {
-                    "vue2": "y",
-                    "vue3": "y"
-                }
-            }
-        }
-    }
+{
+    "id": "mp-html",
+    "displayName": "mp-html 富文本组件【全端支持,支持编辑、latex等扩展】",
+    "version": "v2.4.1",
+    "description": "一个强大的富文本组件,高效轻量,功能丰富",
+    "keywords": [
+        "富文本",
+        "编辑器",
+        "html",
+        "rich-text",
+        "editor"
+    ],
+    "repository": "https://github.com/jin-yufeng/mp-html",
+    "dcloudext": {
+        "sale": {
+            "regular": {
+                "price": "0.00"
+            },
+            "sourcecode": {
+                "price": "0.00"
+            }
+        },
+        "contact": {
+            "qq": ""
+        },
+        "declaration": {
+            "ads": "无",
+            "data": "无",
+            "permissions": "无"
+        },
+        "npmurl": "https://www.npmjs.com/package/mp-html",
+        "type": "component-vue"
+    },
+    "uni_modules": {
+        "platforms": {
+            "cloud": {
+                "tcb": "y",
+                "aliyun": "y"
+            },
+            "client": {
+                "App": {
+                    "app-vue": "y",
+                    "app-nvue": "y"
+                },
+                "H5-mobile": {
+                    "Safari": "y",
+                    "Android Browser": "y",
+                    "微信浏览器(Android)": "y",
+                    "QQ浏览器(Android)": "y"
+                },
+                "H5-pc": {
+                    "Chrome": "y",
+                    "IE": "u",
+                    "Edge": "y",
+                    "Firefox": "y",
+                    "Safari": "y"
+                },
+                "小程序": {
+                    "微信": "y",
+                    "阿里": "y",
+                    "百度": "y",
+                    "字节跳动": "y",
+                    "QQ": "y"
+                },
+                "快应用": {
+                    "华为": "y",
+                    "联盟": "y"
+                },
+                "Vue": {
+                    "vue2": "y",
+                    "vue3": "y"
+                }
+            }
+        }
+    }
 }
 }

+ 4 - 2
vite.config.js

@@ -1,4 +1,6 @@
-import { loadEnv } from 'vite';
+import {
+	loadEnv
+} from 'vite';
 import uni from '@dcloudio/vite-plugin-uni';
 import uni from '@dcloudio/vite-plugin-uni';
 import path from 'path';
 import path from 'path';
 // import viteCompression from 'vite-plugin-compression';
 // import viteCompression from 'vite-plugin-compression';
@@ -31,4 +33,4 @@ export default (command, mode) => {
 			},
 			},
 		},
 		},
 	};
 	};
-};
+};