jwyu пре 3 година
родитељ
комит
2b97a4f662
46 измењених фајлова са 5101 додато и 42 уклоњено
  1. 19 0
      api/buy.js
  2. 101 0
      api/report.js
  3. 9 1
      mixin/index.js
  4. 207 0
      pages-buy/detail.vue
  5. 36 0
      pages-report/chapterDetail.vue
  6. 97 0
      pages-report/classify.vue
  7. 226 0
      pages-report/components/audioBox.vue
  8. 115 0
      pages-report/components/searchBox.vue
  9. 33 0
      pages-report/ficcService.vue
  10. 35 0
      pages-report/reportDetail.vue
  11. 315 0
      pages-report/reportList.vue
  12. 158 0
      pages-report/search.vue
  13. 334 0
      pages-report/specialColumn/detail.vue
  14. 114 0
      pages-report/specialColumn/list.vue
  15. BIN
      pages-report/static/audio-change-grey.png
  16. BIN
      pages-report/static/audio-change.png
  17. BIN
      pages-report/static/audio-pause.png
  18. BIN
      pages-report/static/audio-play.png
  19. BIN
      pages-report/static/audio-s.png
  20. BIN
      pages-report/static/audio.png
  21. BIN
      pages-report/static/bg-1.png
  22. BIN
      pages-report/static/filter-icon.png
  23. BIN
      pages-report/static/search-del.png
  24. BIN
      pages-report/static/search-icon-s.png
  25. BIN
      pages-report/static/search-icon.png
  26. BIN
      pages-report/static/tag.png
  27. 88 18
      pages.json
  28. 9 3
      pages/activity/activity.vue
  29. 119 4
      pages/buy/buy.vue
  30. 2 2
      pages/login.vue
  31. 431 3
      pages/report/report.vue
  32. BIN
      static/report-menu.png
  33. 3 1
      store/index.js
  34. 34 0
      store/modules/report.js
  35. 12 0
      style/common.scss
  36. 182 0
      uni_modules/mp-html/README.md
  37. 75 0
      uni_modules/mp-html/changelog.md
  38. 458 0
      uni_modules/mp-html/components/mp-html/mp-html.vue
  39. 536 0
      uni_modules/mp-html/components/mp-html/node/node.vue
  40. 1260 0
      uni_modules/mp-html/components/mp-html/parser.js
  41. 79 0
      uni_modules/mp-html/package.json
  42. 0 0
      uni_modules/mp-html/static/app-plus/mp-html/js/handler.js
  43. 0 0
      uni_modules/mp-html/static/app-plus/mp-html/js/uni.webview.min.js
  44. 1 0
      uni_modules/mp-html/static/app-plus/mp-html/local.html
  45. 12 10
      utils/config.js
  46. 1 0
      utils/request.js

+ 19 - 0
api/buy.js

@@ -0,0 +1,19 @@
+// 已购模块
+
+import { httpGet, httpPost } from "@/utils/request.js";
+
+/** 
+ * 已购列表
+ */
+export const apiBuyList=()=>{
+    return httpGet('/purchase/list',{})
+}
+
+/** 
+ * 已购详情消息列表
+ * @param {classify_id_first} 一级分类id
+ * @param {activity_id} 活动id
+ */
+export const apiBuyDetail=params=>{
+    return httpGet('/purchase/detail',params)
+}

+ 101 - 0
api/report.js

@@ -0,0 +1,101 @@
+// 报告模块
+
+import { httpGet, httpPost } from "@/utils/request.js";
+
+/**
+ * 研报详情
+ * @param report_id 
+ */
+export const apiReportDetail=params=>{
+    return httpGet('/report/detail',params)
+}
+
+/**
+ * 章节详情
+ * @param report_chapter_id
+ */
+export const apiChapterDetail=params=>{
+    return httpGet('/report/chapter/detail',params)
+}
+
+/**
+ * 研报首页顶部权限菜单
+ */
+export const apiReportIndexPageAuthList=()=>{
+    return httpGet('/company/getPermissionList',{})
+}
+
+/**
+ * 研报首页报告列表
+ * @param chart_permission_id
+ */
+export const apiReportIndexPageList=params=>{
+    return httpGet('/report/collect',params)
+}
+
+/**
+ * 研报搜索
+ * @param key_word
+ */
+export const apiReportSearch=params=>{
+    return httpGet('/report/search',params)
+}
+
+/**
+ * 分类数据
+ */
+export const apiReportClassify=()=>{
+    return httpGet('/classify/ficc',{})
+}
+
+/**
+ * 研报列表
+ * @param classify_id_first
+ * @param key_word
+ */
+export const apiReportList=params=>{
+    return httpGet('/report/list',params)
+}
+
+/**
+ * 二级分类列表
+ * @param classify_id_first
+ * @param classify_id_second
+ */
+export const apiSubClassifyList=params=>{
+    return httpGet('/classify/simple/list',params)
+}
+
+/**
+ * 专栏列表
+ * @param classify_id_first
+ */
+export const apiSpecialColumnList=params=>{
+    return httpGet('/classify/list',params)
+}
+
+/**
+ * 专栏详情
+ * @param classify_id_second
+ */
+export const apiSpecialColumnDetail=params=>{
+    return httpGet('/classify/detail',params)
+}
+
+/**
+ * 专栏详情中目录
+ * @param classify_id_second
+ * @param current_index
+ * @param page_size
+ */
+export const apiSpecialColumnReportList=params=>{
+    return httpGet('/classify/detail/reports',params)
+}
+
+/**
+ * 章节详情中指标数据
+ * @param report_chapter_id
+ */
+export const apiChapterTickerValue=params=>{
+    return httpGet('/report/chapter/ticker',params)
+}

+ 9 - 1
mixin/index.js

@@ -42,9 +42,17 @@ module.exports = {
      */
     formatVoiceTime(e){
       let minus=parseInt(e/60)
-      let sec=e%60
+      let sec=parseInt(e%60)
       return `${minus>9?minus:'0'+minus}分${sec>9?sec:'0'+sec}秒`
+    },
+
+    /**
+     * 报告时间格式化
+     */
+    formatReportTime(e){
+      return moment(e).format('YYYY.MM.DD HH:mm')
     }
+
   },
   data() {
     return {

+ 207 - 0
pages-buy/detail.vue

@@ -0,0 +1,207 @@
+<template>
+    <page-meta :page-style="loading? 'overflow: hidden;' : ''" />
+    <!-- <scroll-view scroll-y="true" :scroll-into-view="scrollViewId" upper-threshold="10" @scrolltoupper="onScrollTop" style="height: 100vh;"> -->
+        <view class="list">
+            <view class="item" v-for="item in list" :key="item.report_id" :id="`msg${item.report_id}${item.activity_id}`" >
+                <view class="msg-time">{{formatMsgTime(item.time)}}</view>
+                <view class="content" @click="goDetail(item)">
+                    <view class="top-img" :style="'background-image:url('+item.img_url+')'">
+                        <text class="name">{{item.top_name}}</text>
+                    </view>
+                    <view class="title">{{item.title}}</view>
+                    <view class="intro">{{item.content}}</view>
+                </view>
+            </view>
+        </view>
+        <view class="report-empty-box" v-if="finished&&list.length==0">
+            <image :src="globalImgUrls.chartEmpty" mode="widthFix" />
+            <view>暂无数据</view>
+        </view>
+    <!-- </scroll-view> -->
+</template>
+
+<script>
+const moment=require('@/utils/moment-with-locales.min')
+moment.locale('zh-cn');
+import {apiBuyDetail} from '@/api/buy'
+export default {
+    data () {
+        return {
+            scrollViewId:'',
+            classify_id_first:0,
+            activity_id:0,
+            page:1,
+            pageSize:20,
+            list:[],
+            finished:false,
+            loading:false
+        }
+    },
+    onLoad(options) {
+        this.classify_id_first=options.classify_id_first
+        this.activity_id=options.activity_id
+        uni.setNavigationBarTitle({
+			title: options.activity_id!=0?'电话会':options.name
+		})
+        this.getList()
+    },
+    methods: {
+        async getList(){
+            this.loading=true
+            const res=await apiBuyDetail({
+                classify_id_first:Number(this.classify_id_first),
+                activity_id:Number(this.activity_id),
+                current_index:this.page,
+                page_size:this.pageSize
+            })
+            setTimeout(() => {
+                this.loading=false
+            }, 500);
+            if(res.code===200){
+                let arr=res.data.list||[]
+                
+                let temarr=JSON.parse(JSON.stringify(arr))
+                this.list=[...temarr.reverse(),...this.list]
+                
+                if(res.data.paging.is_end){
+                    this.finished=true
+                }
+
+                this.$nextTick(()=>{
+                    if(arr[0]){
+                        this.scrollViewId=`msg${res.data.list[0].report_id}${res.data.list[0].activity_id}`
+                        uni.pageScrollTo({ selector:'#'+this.scrollViewId,duration:0 })
+                    }
+				})
+                
+            }
+        },
+
+        // onScrollTop(){
+        //     if(this.finished) return
+		// 	this.page++
+		// 	this.getList()
+        // },
+
+        // 格式化消息时间
+        formatMsgTime(e){
+            const hour=moment(e).hour()//获取小时
+            const dayTime=moment(e).format('hh:mm')
+            let dayStr=''
+            if(hour>=0&&hour<5){
+                dayStr=`凌晨 ${moment(e).format('HH:mm')}`
+            }else if(hour>=5&&hour<7){
+                dayStr=`清晨 ${dayTime}`
+            }else if(hour>=7&&hour<9){
+                dayStr=`早上 ${dayTime}`
+            }else if(hour>=9&&hour<11){
+                dayStr=`上午 ${dayTime}`
+            }else if(hour>=11&&hour<13){
+                dayStr=`中午 ${dayTime}`
+            }else if(hour>=13&&hour<17){
+                dayStr=`下午 ${dayTime}`
+            }else if(hour>=17&&hour<19){
+                dayStr=`傍晚 ${dayTime}`
+            }else if(hour>=19&&hour<23){
+                dayStr=`晚上 ${dayTime}`
+            }else if(hour>=23&&hour<24){
+                dayStr=`午夜 ${dayTime}`
+            }
+
+            // 判断是否为今天
+            if(moment(new Date()).isSame(e, 'day')){
+                return dayStr
+            }
+            // 判断昨天
+            if(moment(moment().subtract(1, 'day')).isSame(e,'day')){
+                return `昨天 ${dayStr}`
+            }
+            // 前天
+            if(moment(moment().subtract(2, 'day')).isSame(e,'day')){
+                return `前天 ${dayStr}`
+            }
+            // 周内
+            if(moment(new Date()).isSame(e, 'week')){
+                return `${moment(e).format('ddd')} ${dayStr}`
+            }
+            //年内
+            if(moment(new Date()).isSame(e, 'year')){
+                return `${moment(e).format('MM月DD日')} ${dayStr}`
+            }
+
+            return `${moment(e).format('YYYY年MM月DD日')} ${dayStr}`
+
+
+        },
+
+        goDetail(item){
+            if(item.activity_id){
+                uni.navigateTo({url: '/pages-activity/detail?id=' + item.activity_id })
+            }else{
+                uni.navigateTo({url:'/pages-report/reportDetail?reportId='+item.report_id})
+            }
+        },
+
+        onPageScroll({ scrollTop }) {
+            if(this.loading||this.finished) return
+            if(scrollTop<20){
+                this.page++
+                this.getList()
+            }
+        }
+    }
+}
+</script>
+
+<style>
+page{
+    min-height: 100vh;
+    padding-bottom: 0;
+    background-color: #F5F6FA;
+}
+</style>
+<style lang="scss" scoped>
+.list{
+    .item{
+        padding: 34rpx;
+        .msg-time{
+            text-align: center;
+            padding: 20rpx;
+        }
+        .content{
+            background: #FFFFFF;
+            box-shadow: 3rpx 3rpx 12rpx 0px rgba(161,161,161,0.14);
+            border-radius: 16rpx;
+            overflow: hidden;
+            .top-img{
+                width: 100%;
+                height: 230rpx;
+                background-color: #a1a1a1;
+                background-size: cover;
+                position: relative;
+                .name{
+                    display: block;
+                    width: 90%;
+                    text-align: center;
+                    color: #CFC09F;
+                    font-size: 50rpx;
+                    font-weight: 600;
+                    position: absolute;
+                    top: 50%;
+                    left: 50%;
+                    transform: translate(-50%,-50%);
+                }
+            }
+            .title{
+                padding: 20rpx 30rpx 10rpx 30rpx;
+                font-weight: bold;
+                font-size: 32rpx;
+            }
+            .intro{
+                padding: 0 30rpx 24rpx 30rpx;
+                color: $global-text-color-grey;
+            }
+        }
+    }
+}
+</style>

+ 36 - 0
pages-report/chapterDetail.vue

@@ -0,0 +1,36 @@
+<template>
+  <web-view :src="url" @message="handleGetMessage"/>
+</template>
+
+<script>
+import {h5BaseUrl} from '@/utils/config'
+export default {
+  data() {
+    return {
+      url:'',
+      msgObj:{}
+    };
+  },
+  onLoad(options) {
+    let chapterId=options.chapterId
+    let fromPage=options.fromPage||''
+    const timestamp=new Date().getTime()
+    const token=this.$store.state.user.token
+    this.url=`${h5BaseUrl}/hzyb/report/chapterdetail?chapterId=${chapterId}&fromPage=${fromPage}&token=${token}&timestamp=${timestamp}#wechat_redirect`
+  },
+  onShareAppMessage() {
+    return {
+      title:this.msgObj.title,
+      path:`/pages-report/chapterDetail?chapterId=${this.msgObj.chapterId}`
+    }
+  },
+  methods: {
+    handleGetMessage(e){
+      const data=e.detail.data[e.detail.data.length-1]
+      console.log('h5传来的数据',data);
+      this.msgObj=data
+    }
+  }
+};
+</script>
+

+ 97 - 0
pages-report/classify.vue

@@ -0,0 +1,97 @@
+<template>
+  <view class="classify-page">
+    <swiper class="swiper" autoplay interval>
+      <swiper-item>
+        <image @click="goFiccService" :src="globalImgUrls.ficcServiceImg" mode="aspectFill"/>
+      </swiper-item>
+    </swiper>
+    <view class="list">
+      <view class="item" @click="goDetail(item)" v-for="item in list" :key="item.classify_name_first">
+        <image :src="item.icon_img_url" mode="aspectFill"/>
+        <view>{{item.classify_name_first}}</view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import {apiReportClassify} from '@/api/report'
+export default {
+  data () {
+    return {
+      list:[]
+    }
+  },
+  onLoad(options) {
+    this.getClassifyList()
+  },
+  onPullDownRefresh() {
+    this.getClassifyList()
+    setTimeout(() => {
+      uni.stopPullDownRefresh()
+    }, 1500);
+  },
+  onShareAppMessage() {
+    return {
+      title:"FICC研报分类"
+    }
+  },
+  methods: {
+    async getClassifyList(){
+      const res=await apiReportClassify()
+      if(res.code===200){
+        this.list=res.data
+      }
+    },
+
+    goDetail(item){
+      //redirect_type : 跳转页面类型:1,专栏列表,2报告列表,3专栏详情
+      if(item.redirect_type==1){
+        uni.navigateTo({ url: `/pages-report/specialColumn/list?classifyId=${item.classify_id_first}&classifyName=${item.classify_name_first}` })
+      }
+      if(item.redirect_type==2){
+        uni.navigateTo({url:`/pages-report/reportList?classifyId=${item.classify_id_first}&classifyName=${item.classify_name_first}`})
+      }
+      if(item.redirect_type==3){
+        uni.navigateTo({url:'/pages-report/specialColumn/detail?columnId='+item.classify_id_second})
+      }
+    },
+
+    goFiccService(){
+      uni.navigateTo({url:'/pages-report/ficcService'})
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.classify-page {
+  padding: 34rpx;
+  .swiper{
+    height: 300rpx;
+    overflow: hidden;
+    image{
+      width: 100%;
+      height: 100%;
+      display: block;
+      border-radius: 8rpx;
+    }
+  }
+  .list{
+    margin-top: 40rpx;
+    display: flex;
+    flex-wrap: wrap;
+    .item{
+      width: 25%;
+      padding: 0 20rpx;
+      margin-bottom: 30rpx;
+      text-align: center;
+      image{
+        width: 128rpx;
+        height: 128rpx;
+        // background-color: #f5f5f5;
+      }
+    }
+  }
+}
+</style>

+ 226 - 0
pages-report/components/audioBox.vue

@@ -0,0 +1,226 @@
+<template>
+    <view class="pop-audio-box">
+        <view class="name">{{title}}</view>
+        <view class="flex">
+            <text>{{currentTime|formatVoiceTime}}</text>
+            <slider
+                activeColor="#e3b377"
+                :max="audioTime" 
+                :value="currentTime" 
+                @change="handleAudioSliderChange($event)"
+                block-size="16"
+                class="slider"
+            />
+            <text>{{audioTime|formatVoiceTime}}</text>
+            <image 
+                class="small-img"  
+                :src="play?'../static/audio-play.png':'../static/audio-pause.png'" 
+                mode="aspectFill" 
+                v-if="audioData.list.length==1"
+                @click="handleChangePlayStatus"
+            />
+        </view>
+        <view class="btn" v-if="audioData.list.length>1">
+            <image 
+                :src="audioData.index==0?'../static/audio-change-grey.png':'../static/audio-change.png'" 
+                mode="aspectFill" 
+                class="aside"
+                @click="handleAudioChange('before')"
+            />
+            <image 
+                :src="play?'../static/audio-play.png':'../static/audio-pause.png'" 
+                mode="aspectFill" 
+                class="center"
+                @click="handleChangePlayStatus"
+            />
+            <image 
+                :src="audioData.index==audioData.list.length-1?'../static/audio-change-grey.png':'../static/audio-change.png'" 
+                mode="aspectFill" 
+                class="aside" 
+                style="transform: rotate(180deg)"
+                @click="handleAudioChange('next')"
+            />
+        </view>
+    </view>
+</template>
+
+<script>
+export default {
+    computed: {
+        audioData(){
+            return this.$store.state.report.audioData
+        }
+    },
+    watch: {
+        'audioData.list':{
+            handler(nval,old){
+                console.log('watch',nval,old);
+                
+                if(nval.length>0){
+                    if(old){
+                        this.init('change')
+                    }else{
+                        this.init()
+                    }
+                }
+            },
+            deep:true,
+            immediate:true
+        }
+    },
+    data () {
+        return {
+            title:'',
+            audioTime:0,
+            currentTime:0,
+            play:false
+        }
+    },
+    methods: {
+        init(type){
+            console.log('init',this.$store.state.report.audioData);
+            let curAudio=this.$store.state.report.audioData.list[this.$store.state.report.audioData.index]
+            if(this.globalBgMusic.src){
+                console.log(this.globalBgMusic);
+                console.log(type);
+                if(type==='change'){
+                    this.globalBgMusic.src=curAudio.video_url
+                    this.globalBgMusic.title=curAudio.video_name
+                }else{
+                    this.currentTime=parseInt(this.globalBgMusic.currentTime)
+                    this.play=!this.globalBgMusic.paused
+                }
+            }else{
+                this.globalBgMusic.src=curAudio.video_url
+                this.globalBgMusic.title=curAudio.video_name
+            }
+            this.title=curAudio.video_name
+            this.audioTime=curAudio.video_play_seconds
+            this.handleAudioFun()
+        },
+
+        // 音频事件
+        handleAudioFun(){
+            this.globalBgMusic.onPlay(()=>{
+                this.play=true
+                this.$store.commit('updateAudioPause',false)
+            })
+            this.globalBgMusic.onPause(()=>{
+                this.play=false
+                this.$store.commit('updateAudioPause',true)
+            })
+            this.globalBgMusic.onStop(()=>{
+                this.$store.commit('removeAudio')
+            })
+            this.globalBgMusic.onEnded(()=>{
+                console.log('onEnded');
+                // this.play=false
+                let index=this.$store.state.report.audioData.index
+                if(index==this.$store.state.report.audioData.list.length-1){
+                    this.$store.commit('removeAudio')
+                }else{
+                    this.handleAudioChange('next')
+                }
+            })
+            this.globalBgMusic.onError((e)=>{
+                console.log('onError',e);
+                uni.showToast({
+                    title: '音频播放错误',
+                    icon: 'none'
+                })
+            })
+            this.globalBgMusic.onTimeUpdate(()=>{
+                // console.log('时间更新');
+                this.currentTime=parseInt(this.globalBgMusic.currentTime)
+            })
+        },
+
+        //音频点击暂停播放
+        handleChangePlayStatus(){
+            if(this.play){
+                this.globalBgMusic.pause()
+            }else{
+                this.globalBgMusic.play()
+            }
+        },
+
+        //音频切换
+        handleAudioChange(type){
+            let temIndex=this.$store.state.report.audioData.index
+            if(type=='before'){
+                if(temIndex>0){
+                    let index=temIndex-1
+                    this.$store.commit('updateAudioIndex', index)
+                    this.init('change')
+                }
+            }else{
+                if(temIndex<this.$store.state.report.audioData.list.length-1){
+                    let index=temIndex+1
+                    this.$store.commit('updateAudioIndex', index)
+                    this.init('change')
+                }
+            }
+        },
+
+        //拖动进度条
+        handleAudioSliderChange(e){
+            const value=e.detail.value
+            this.globalBgMusic.seek(value)
+        }
+    }
+}
+</script>
+
+<style lang="scss" scoped>
+.pop-audio-box{
+    position: fixed;
+    width: 90vw;
+    min-height: 50rpx;
+    left: 5vw;
+    bottom: 30rpx;
+    z-index: 99;
+    background-color: #fff;
+    border: 2px solid rgba(240, 234, 226, 0.32);
+    border-radius: 8rpx;
+    padding: 10rpx 15rpx;
+    .name{
+        text-align: center;
+        font-size: 24rpx;
+    }
+    .flex{
+        align-items: center;
+        text{
+            flex-shrink: 0;
+            font-size: 20rpx;
+        }
+        .slider{
+            flex: 1;
+            margin: 0 20rpx;
+        }
+        .small-img{
+            width: 36rpx;
+            height: 36rpx;
+            display: block;
+            margin-left: 10rpx;
+            flex-shrink: 0;
+        }
+    }
+    .btn{
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        image{
+            display: block;
+        }
+        .aside{
+            width: 36rpx;
+            height: 36rpx;
+        }
+        .center{
+            width: 44rpx;
+            height: 44rpx;
+            margin: 0 20rpx;
+        }
+    }
+}
+</style>

+ 115 - 0
pages-report/components/searchBox.vue

@@ -0,0 +1,115 @@
+<template>
+    <view :class="['flex search-box',isFocus||searchVal?'search-focus':'']">
+        <image 
+            class="search-icon" 
+            :src="isFocus||searchVal?'../static/search-icon-s.png':'../static/search-icon.png'" 
+            mode="aspectFill"
+        />
+        <input 
+            type="text" 
+            :maxlength="-1"
+            v-model="searchVal" 
+            :placeholder="placeholder" 
+            :focus="isFocus" 
+            @focus="handelFocus"
+            @blur="handelBlur"
+            @confirm="handleSearch"
+            @input="handleInput"
+            confirm-type="search"
+        />
+        <image 
+            class="search-clear" 
+            src="../static/search-del.png" 
+            mode="aspectFill"
+            v-if="searchVal"
+            @click="handleClearSearch"
+        />
+        <view class="right-btn" v-if="hasRightBtn&&searchVal" @click="handleSearch">搜索</view>
+    </view>
+</template>
+
+<script>
+export default {
+    props: {
+        hasRightBtn:{
+            type:Boolean,
+            default:true
+        },
+        placeholder:{
+            type:String,
+            default:'请输入搜索关键词'
+        },
+        focus:{
+            type:Boolean,
+            default:false
+        }
+    },
+    data () {
+        return {
+            searchVal:'',
+            isFocus:this.focus
+        }
+    },
+    methods: {
+        handleClearSearch(){
+            this.searchVal=''
+            this.$emit('change',this.searchVal)
+            this.$emit('search')
+        },
+
+        handleInput(){
+            this.$emit('change',this.searchVal)
+        },
+
+        handelFocus(){
+            this.isFocus=true
+        },
+
+        handelBlur(){
+            this.isFocus=false
+        },
+
+        handleSearch(){
+            this.$emit('search')
+        }
+    }
+}
+</script>
+
+<style lang="scss" scoped>
+.search-box{
+    border: 1px solid #E5E5E5;
+    align-items: center;
+    background-color: #F6F6F6;
+    border-radius: 43rpx;
+    padding: 0 30rpx;
+    height: 70rpx;
+    .search-icon{
+        width: 48rpx;
+        height: 48rpx;
+        flex-shrink: 0;
+        margin-right: 5rpx;
+    }
+    input{
+        flex: 1;
+    }
+    .search-clear{
+        width: 40rpx;
+        height: 40rpx;
+        flex-shrink: 0;
+        margin-left: 5rpx;
+        margin-right: 20rpx;
+    }
+    .right-btn{
+        font-size: 28rpx;
+        color: #E3B377;
+        flex-shrink: 0;
+        border-left: 1px solid #999999;
+        padding-left: 20rpx;
+    }
+}
+.search-focus{
+    border-color: #E3B377;
+    background-color: #fff;
+}
+</style>

+ 33 - 0
pages-report/ficcService.vue

@@ -0,0 +1,33 @@
+<template>
+    <view class="ficc-service-page">
+        <image
+            :src="globalImgUrls.ficcServicePage+'?t='+t"
+            mode="widthFix"
+        />
+    </view>
+</template>
+
+<script>
+export default {
+    data () {
+        return {
+            t:new Date().getTime()
+        }
+    }
+}
+</script>
+
+<style>
+page{
+    padding-bottom: 0;
+}
+</style>
+<style lang="scss" scoped>
+
+.ficc-service-page{
+    image{
+        width: 100%;
+        display: block;
+    }
+}
+</style>

+ 35 - 0
pages-report/reportDetail.vue

@@ -0,0 +1,35 @@
+<template>
+    <web-view :src="url" @message="handleGetMessage"/></web-view>
+</template>
+
+<script>
+import {h5BaseUrl} from '@/utils/config'
+export default {
+    data () {
+        return {
+            url:'',
+            msgObj:{}
+        }
+    },
+    onLoad(options) {
+        let reportId=options.reportId
+        const timestamp=new Date().getTime()
+        const token=this.$store.state.user.token
+        this.url=`${h5BaseUrl}/hzyb/report/detail?reportId=${reportId}&token=${token}&timestamp=${timestamp}#wechat_redirect`
+    },
+    onShareAppMessage() {
+        return {
+            title:this.msgObj.title,
+            path:`/pages-report/reportDetail?reportId=${this.msgObj.reportId}`
+        }
+    },
+    methods: {
+        handleGetMessage(e){
+            const data=e.detail.data[e.detail.data.length-1]
+            console.log('h5传来的数据',data);
+            this.msgObj=data
+        }
+    }
+    
+}
+</script>

+ 315 - 0
pages-report/reportList.vue

@@ -0,0 +1,315 @@
+<template>
+  <view class="report-list-page">
+    <van-sticky style="background: #fff">
+      <view class="flex search-wrap">
+        <searchBox style="flex: 1" :focus="focus" placeholder="请输入报告标题或关键字" :hasRightBtn="false" @change="onChange" @search="onSearch"></searchBox>
+        <view class="filter-box" @click="showFilter=true" v-if="classifyList.length>0">
+          <image src="./static/filter-icon.png" mode="aspectFill"></image>
+          <text>筛选</text>
+        </view>
+      </view>
+    </van-sticky>
+    <view class="report-empty-box" v-if="finished&&list.length==0">
+      <image :src="globalImgUrls.chartEmpty" mode="widthFix" />
+      <view>{{searchVal?'找不到对应报告,试试别的搜索词吧':'暂无报告'}}</view>
+    </view>
+    <view class="report-list-wrap" :style="{paddingBottom:showAudioPop&&'90px'}" v-else>
+      <view class="flex item" v-for="item in list" :key="item.report_id" @click="goReportDetail(item)">
+        <image class="img" :src="item.report_img_url" mode="aspectFill" />
+        <view class="con">
+          <view class="title-info" v-html="item.title_info"></view>
+          <view class="title" v-html="item.title"></view>
+          <view class="tips" v-html="item.abstract"></view>
+          <view class="time">{{item.publish_time|formatReportTime}}</view>
+          <view :class="['audio-box',!item.auth_ok&&'grey-audio-box']" @click.stop="handleClickAudio(item)">
+            <image :src="curAudioReportId==item.report_id&&!curAudioPaused?'./static/audio-s.png':'./static/audio.png'" mode="aspectFill"/>
+            <text>{{curAudioReportId==item.report_id&&!curAudioPaused?'暂停':'播放'}}</text>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 筛选 -->
+    <van-popup :show="showFilter" position="bottom" :safe-area-inset-bottom="false" round @close="showFilter=false">
+        <view class="filter-wrap">
+            <view class="flex top">
+                <text style="color:#000">筛选</text>
+                <text style="color:#E3B377" @click="showFilter=false">取消</text>
+            </view>
+            <view class="list-box">
+                <van-row gutter="10">
+                    <van-col span="8" v-for="item in classifyList" :key="item.classify_id_second">
+                        <view 
+                          :class="['item',item.classify_id_second==selectSecClassifyId&&'active']" 
+                          @click="handleSelectSecClassifyId(item)"
+                        >{{item.classify_second_simple}}</view>
+                    </van-col>
+                </van-row>
+            </view>
+        </view>
+    </van-popup>
+
+    <!-- 音频弹窗 -->
+    <audioBox v-if="showAudioPop"></audioBox>
+  </view>
+</template>
+
+<script>
+import searchBox from "./components/searchBox.vue";
+import audioBox from './components/audioBox.vue'
+import {apiReportList,apiSubClassifyList} from '@/api/report'
+export default {
+  computed: {
+    showAudioPop(){//是否显示音频弹窗
+      return this.$store.state.report.audioData.show 
+    },
+    curAudioReportId(){//当前播放的音频所属报告
+      return this.$store.state.report.audioData.reportId
+    },
+    curAudioPaused(){//当前音频是否暂停状态
+      return this.$store.state.report.audioData.paused
+    }
+  },
+  components: {
+    searchBox,
+    audioBox
+  },
+  data() {
+    return {
+      classifyId:0,
+      classifyName:'',
+
+      classifyList:[],
+      selectSecClassifyId:0,
+
+      searchVal: "",
+      showFilter:false,
+
+      list:[],
+      finished:false,
+      page:1,
+      pageSize:20
+    };
+  },
+  onLoad(options) {
+    this.classifyId=options.classifyId
+    this.classifyName=options.classifyName
+    // 设置title
+    uni.setNavigationBarTitle({ title: decodeURIComponent(options.classifyName)+'列表' })
+    this.getList()
+    this.getClassifyList()
+  },
+  onPullDownRefresh() {
+    this.page=1
+    this.list=[]
+    this.finished=false
+    this.getList()
+    this.getClassifyList()
+    setTimeout(() => {
+      uni.stopPullDownRefresh()
+    }, 1500);
+  },
+  onReachBottom() {
+    if(this.finished) return
+    this.page++
+    this.getList()
+  },
+  onShareAppMessage() {
+    return {
+      title:this.classifyName+'列表'
+    }
+  },
+  methods: {
+    //获取研报列表
+    async getList(){
+      const res=await apiReportList({
+        classify_id_first:Number(this.classifyId),
+        classify_id_second:Number(this.selectSecClassifyId),
+        key_word:this.searchVal,
+        current_index:this.page,
+        page_size:this.pageSize
+      })
+      if(res.code===200){
+        let arr=res.data.list||[]
+        this.list=[...this.list,...arr]
+        if(res.data.paging.is_end){
+          this.finished=true
+        }
+      }
+    },
+
+    //分类数据
+    async getClassifyList(){
+      const res=await apiSubClassifyList({classify_id_first:Number(this.classifyId)})
+      if(res.code===200){
+        this.classifyList=res.data
+      }
+    },
+
+    handleSelectSecClassifyId(item){
+      if(this.selectSecClassifyId==item.classify_id_second){
+        this.selectSecClassifyId=''
+      }else{
+        this.selectSecClassifyId=item.classify_id_second
+      }
+      this.showFilter=false
+      this.page=1
+      this.finished=false
+      this.list=[]
+      this.getList()
+    },
+
+    onChange(e) {
+      this.searchVal = e;
+    },
+
+    onSearch() {
+      console.log("搜索", this.searchVal);
+      this.page=1
+      this.finished=false
+      this.list=[]
+      this.getList()
+    },
+
+    goReportDetail(item){
+      uni.navigateTo({ url: '/pages-report/reportDetail?reportId='+item.report_id })
+    },
+
+    handleClickAudio(item){
+      if(!item.auth_ok) return
+      if(!item.video_list||item.video_list.length==0){
+        uni.showToast({
+          title: '暂无音频',
+          icon: 'none'
+        })
+        return
+      }
+      // 判断是否为同一个音频
+      if(this.$store.state.report.audioData.reportId==item.report_id){
+        if(this.globalBgMusic.paused){
+          this.globalBgMusic.play()
+        }else{
+          this.globalBgMusic.pause()
+        }
+      }else{
+        this.$store.commit('addAudio', {list:item.video_list,reportId:item.report_id})
+      }
+    }
+
+  },
+};
+</script>
+
+<style>
+page{
+  padding-bottom: 0;
+}
+</style>
+<style lang="scss" scoped>
+.search-wrap {
+  background-color: #fff;
+  padding: 30rpx 34rpx;
+  align-items: center;
+  .filter-box {
+    margin-left: 20rpx;
+    image {
+      width: 52rpx;
+      height: 52rpx;
+      vertical-align: middle;
+    }
+    text {
+      vertical-align: middle;
+      color: #e3b377;
+      font-size: 32rpx;
+    }
+  }
+}
+.report-list-wrap {
+    padding: 0 34rpx;
+  .item {
+    margin-bottom: 30rpx;
+    .img {
+      width: 120rpx;
+      height: 160rpx;
+      border-radius: 16rpx;
+      background-color: #f5f5f5;
+      flex-shrink: 0;
+      margin-right: 20rpx;
+    }
+    .con {
+      flex: 1;
+      position: relative;
+      overflow: hidden;
+      .title {
+        font-size: 32rpx;
+        font-weight: bold;
+        margin-bottom: 8rpx;
+      }
+      .title-info{
+        font-size: 28rpx;
+      }
+      .tips {
+        color: #666666;
+        margin-bottom: 10rpx;
+        min-height: 34rpx;
+      }
+      .time {
+        color: #666666;
+      }
+      .audio-box {
+        position: absolute;
+        bottom: 0;
+        right: 0;
+        width: 99rpx;
+        height: 39rpx;
+        background: linear-gradient(100deg, #e3b377 0%, #ffddb1 100%);
+        border-radius: 20rpx;
+        color: #fff;
+        font-size: 24rpx;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        image{
+          width: 30rpx;
+          height: 30rpx;
+          margin-right: 4rpx;
+        }
+      }
+      .grey-audio-box {
+        background: linear-gradient(114deg, #b0b0b0 0%, #e5e2e2 100%);
+      }
+    }
+  }
+}
+.filter-wrap{
+    background-color: #fff;
+    padding-top: 53rpx;
+    .top{
+        font-size: 32rpx;
+        justify-content: space-between;
+        margin-bottom: 40rpx;
+        padding: 0 34rpx;
+    }
+    .list-box{
+        min-height: 30vh;
+        max-height: 60vh;
+        overflow-y: auto;
+        padding: 0 34rpx;
+        .item{
+            background-color: #F6F6F6;
+            color: #000000;
+            // text-align: center;
+            height: 76rpx;
+            overflow: hidden;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            // line-height: 76rpx;
+            margin-bottom: 20rpx;
+        }
+        .active{
+            background-color: #FAEEDE;
+        }
+    }
+}
+</style>

+ 158 - 0
pages-report/search.vue

@@ -0,0 +1,158 @@
+<template>
+    <view class="report-search-page">
+        <van-sticky style="background: #fff">
+            <view style="padding:30rpx 34rpx">
+            <searchBox 
+                :focus="focus" 
+                placeholder="请输入报告标题或关键字" 
+                @change="onChange"
+                @search="onSearch"
+            ></searchBox>
+            </view>
+        </van-sticky>
+        <view class="empty-box" v-if="finished&&list.length==0">
+            <image :src="globalImgUrls.chartEmpty" mode="widthFix" />
+            <view>找不到对应报告,试试别的搜索词吧</view>
+        </view>
+        <view class="list-wrap" v-else>
+            <view class="item" v-for="item in list" :key="item.report_id" @click="goReportDetail(item)">
+                <view class="title" v-html="item.title"></view>
+                <view class="desc" v-html="item.content_sub"></view>
+                <view class="flex bot">
+                    <view class="tags">
+                        <text v-if="item.classify_name_first">#{{item.classify_name_first}}</text>
+                        <text v-if="item.classify_name_second">#{{item.classify_name_second}}</text>
+                    </view>
+                    <view class="time">{{getReportListTime(item.publish_time)}}</view>
+                </view>
+            </view>
+        </view>
+    </view>
+</template>
+
+<script>
+const moment=require('@/utils/moment-with-locales.min')
+moment.locale('zh-cn');
+import searchBox from './components/searchBox.vue'
+import {apiReportSearch} from '@/api/report'
+export default {
+    components: {
+        searchBox
+    },
+    data () {
+        return {
+            searchVal:'',
+            focus:true,
+            list:[],
+            finished:false,
+            page:1,
+        }
+    },
+    onReachBottom() {
+        if(this.finished) return
+        this.page++
+        this.getList()
+    },
+    methods: {
+        onChange(e){
+            this.searchVal=e
+        },
+
+        async getList(){
+            const res=await apiReportSearch({
+                key_word:this.searchVal,
+                current_index:this.page,
+                page_size:20,
+            })
+            if(res.code===200){
+                if(res.data.paging.is_end){
+                    this.finished=true
+                }
+                let arr=res.data.list||[]
+                this.list=[...this.list,...arr]
+            }
+        },
+
+        async onSearch(){
+            this.finished=false
+            this.page=1
+            this.list=[]
+            if(!this.searchVal){
+                this.finished=true
+                return
+            }
+            this.getList()
+        },
+
+        goReportDetail(item){
+            if(['晨报','周报'].includes(item.classify_name_first)){
+                uni.navigateTo({url: `/pages-report/chapterDetail?chapterId=${item.report_chapter_id}&fromPage=home`})
+            }else{
+                uni.navigateTo({url:'/pages-report/reportDetail?reportId='+item.report_id})
+            }
+        },
+
+        getReportListTime(e){
+            return moment(e).format('YYYY年MM月DD日 HH:mm')
+        }
+    }
+}
+</script>
+
+<style>
+.van-sticky-wrap--fixed{
+  background-color: #fff;
+}
+page{
+    padding-bottom: 0;
+}
+</style>
+<style lang="scss" scoped>
+.empty-box{
+    text-align: center;
+    font-size: 32rpx;
+    color: #999;
+    padding-top: 150rpx;
+    image{
+        width: 346rpx;
+        margin-bottom: 57rpx;
+    }
+}
+.list-wrap{
+    padding: 34rpx;
+    .item{
+        padding: 30rpx;
+        background: #FFFFFF;
+        box-shadow: 3rpx 3rpx 12rpx 1rpx rgba(184, 184, 184, 0.16);
+        border-radius: 16rpx;
+        border: 1px solid #E5E5E5;
+        margin-bottom: 20rpx;
+        .title{
+            font-size: 32rpx;
+            font-weight: bold;
+        }
+        .desc{
+            color: #666666;
+            line-height: 1.5;
+            margin: 10rpx 0 20rpx 0;
+        }
+        .bot{
+            justify-content: space-between;
+            align-items: flex-end;
+            .time{
+                flex-shrink: 0;
+                color: #999999;
+                font-size: 24rpx;
+            }
+            .tags{
+                font-size: 28rpx;
+                color: #E3B377;
+                text{
+                    display: inline-block;
+                    margin-right: 30rpx;
+                }
+            }
+        }
+    }
+}
+</style>

+ 334 - 0
pages-report/specialColumn/detail.vue

@@ -0,0 +1,334 @@
+<template>
+    <view class="special-column-info-page">
+        <image class="top-bg" src="../static/bg-1.png" mode="aspectFill"/>
+        <view class="user-wrap" v-if="info">
+            <image :src="info.avatar_img_url" mode="aspectFill" class="avatar"/>
+            <view class="user-name">
+                <text>{{info.report_author}}</text>
+                <text class="tag" v-if="info.vip_title">{{info.vip_title}}</text>
+            </view>
+            <view class="user-intro">{{info.author_descript}}</view>
+        </view>
+        <van-sticky>
+        <view class="flex tab-box">
+            <view :class="['item',tabActive=='专栏介绍'&&'tab-acitve']" @click="tabChange('专栏介绍')">专栏介绍</view>
+            <view :class="['item',tabActive=='报告目录'&&'tab-acitve']" @click="tabChange('报告目录')">报告目录</view>
+        </view>
+        </van-sticky>
+        <view class="section column-intro-wrap" v-if="tabActive=='专栏介绍'">
+            <view v-html="info.abstract"></view>
+        </view>
+        <view class="section report-list-wrap" v-if="tabActive=='报告目录'">
+            <view class="report-empty-box" v-if="finished&&list.length==0">
+                <image :src="globalImgUrls.chartEmpty" mode="widthFix" />
+                <view>暂无报告</view>
+            </view>
+            <block v-else>
+            <view class="flex item" v-for="item in list" :key="item" @click="goDetail(item)">
+                <image class="img" :src="item.report_img_url" mode="aspectFill" lazy-load/>
+                <view class="con">
+                    <view class="title">【第{{item.stage}}期|FICC】{{item.classify_name_second}}</view>
+                    <view class="tips" v-html="item.abstract"></view>
+                    <view class="time">{{item.publish_time|formatReportTime}}</view>
+                    <view :class="['audio-box',!info.auth_ok&&'grey-audio-box']" @click.stop="handleClickAudio(item)">
+                        <image :src="curAudioReportId==item.report_id&&!curAudioPaused?'../static/audio-s.png':'../static/audio.png'" mode="aspectFill"/>
+                        <text>{{curAudioReportId==item.report_id&&!curAudioPaused?'暂停':'播放'}}</text>
+                    </view>
+                </view>
+            </view>
+            </block>
+            
+        </view>
+        <view class="contact-box" v-if="info&&!info.auth_ok&&!info.permission_check.mobile" @click="handleContact">联系我们</view>
+
+        <!-- 音频弹窗 -->
+        <audioBox v-if="showAudioPop"></audioBox>
+    </view>
+</template>
+
+<script>
+import {apiSpecialColumnDetail,apiSpecialColumnReportList} from '@/api/report'
+import audioBox from '../components/audioBox.vue'
+export default {
+    computed: {
+        showAudioPop(){//是否显示音频弹窗
+            return this.$store.state.report.audioData.show 
+        },
+        curAudioReportId(){//当前播放的音频所属报告
+            return this.$store.state.report.audioData.reportId
+        },
+        curAudioPaused(){//当前音频是否暂停状态
+            return this.$store.state.report.audioData.paused
+        }
+    },
+    components: {
+        audioBox
+    },
+    data () {
+        return {
+            tabActive:'专栏介绍',
+            columnId:0,
+            info:null,
+
+            list:[],
+            page:1,
+            pageSize:20,
+            finished:false
+        }
+    },
+    onLoad(options) {
+        this.columnId=options.columnId
+        this.getDetail()
+        this.getList()
+    },
+    onPullDownRefresh() {
+        this.getDetail()
+        this.list=[]
+        this.page=1
+        this.finished=false
+        this.getList()
+        setTimeout(()=>{
+            uni.stopPullDownRefresh()
+        },1500)
+    },
+    onReachBottom() {
+        if(!this.finished&&this.tabActive=='报告目录'){
+            this.page++
+            this.getList()
+        }
+    },
+    onShareAppMessage() {
+        return {
+            title:this.info.classify_name_second
+        }
+    },
+    methods: {
+        async getDetail(){
+            const res=await apiSpecialColumnDetail({classify_id_second:Number(this.columnId)})
+            if(res.code===200){
+                this.info=res.data
+                uni.setNavigationBarTitle({ title: res.data.classify_name_second })
+            }
+        },
+
+        async getList(){
+            const res=await apiSpecialColumnReportList({
+                classify_id_second:Number(this.columnId),
+                current_index:this.page,
+                page_size:this.pageSize
+            })
+            if(res.code===200){
+                let arr=res.data.list||[]
+                this.list=[...this.list,...arr]
+                if(res.data.paging.is_end){
+                    this.finished=true
+                }
+            }
+        },
+
+        tabChange(e){
+            this.tabActive=e
+        },
+
+        goDetail(item){
+            uni.navigateTo({ url: '/pages-report/reportDetail?reportId='+item.report_id })
+        },
+
+        handleContact(){
+            uni.makePhoneCall({
+                phoneNumber: this.info.permission_check.hz_phone
+            })
+        },
+
+        handleClickAudio(item){
+            if(!this.info.auth_ok) return
+            if(!item.video_url){
+                uni.showToast({
+                title: '暂无音频',
+                icon: 'none'
+                })
+                return
+            }
+            // 判断是否为同一个音频
+            if(this.$store.state.report.audioData.reportId==item.report_id){
+                if(this.globalBgMusic.paused){
+                    this.globalBgMusic.play()
+                }else{
+                    this.globalBgMusic.pause()
+                }
+            }else{
+                this.$store.commit('addAudio', {
+                    list:[{video_url:item.video_url,video_name:item.video_name,video_play_seconds:item.video_play_seconds}],
+                    reportId:item.report_id
+                })
+            }
+            
+        }
+    }
+}
+</script>
+
+<style lang="scss">
+page{
+    padding-bottom: 0;
+}
+
+</style>
+
+<style lang="scss" scoped>
+.special-column-info-page{
+    .top-bg{
+        display: block;
+        width: 100%;
+        height: 188rpx;
+    }
+    .user-wrap{
+        padding: 0 34rpx;
+        margin-bottom: 34rpx;
+        .avatar{
+            width: 140rpx;
+            height: 140rpx;
+            background-color: #f5f5f5;
+            display: block;
+            box-sizing: border-box;
+            border: 4rpx solid #fff;
+            border-radius: 50%;
+            margin-top: -70rpx;
+        }
+        .user-name{
+            margin-top: 20rpx;
+            text:first-child{
+                font-size: 32rpx;
+                font-weight: bold;
+            }
+            .tag{
+                background-color: #FAF7EE;
+                margin-left: 20rpx;
+                padding: 6rpx 16rpx;
+                border-radius: 30rpx;
+                font-size: 24rpx;
+                display: inline-block;
+                &::before{
+                    content: '';
+                    display: inline-block;
+                    width: 30rpx;
+                    height: 30rpx;
+                    background-image: url('../static/tag.png');
+                    background-size: cover;
+                    position: relative;
+                    top: 4rpx;
+                }
+            }
+        }
+        .user-title{
+            color: #666666;
+            margin: 20rpx 0;
+        }
+        .user-intro{
+            margin-top: 20rpx;
+            font-size: 24rpx;
+            color: #666666;
+            line-height: 1.7;
+        }
+    }
+    .tab-box{
+        align-items: center;
+        height: 78rpx;
+        background: #FFFFFF;
+        position: relative;
+        box-shadow: 0px 4rpx 4rpx 1px rgba(198, 198, 198, 0.16);
+        .item{
+            flex: 1;
+            text-align: center;
+            font-size: 32rpx;
+        }
+        &::after{
+            content: '';
+            position: absolute;
+            display: inline-block;
+            width: 1rpx;
+            height: 78rpx;
+            top: 0;
+            left: 50%;
+            background-color: #F6F6F6;
+        }
+        .tab-acitve{
+            color: #E3B377;
+        }
+    }
+    .section{
+        padding: 34rpx;
+        line-height: 1.7;
+        color: #666666;
+        padding-bottom: 90rpx;
+    }
+
+    .report-list-wrap{
+        .item{
+            margin-bottom: 30rpx;
+            .img{
+                width: 120rpx;
+                height: 160rpx;
+                border-radius: 16rpx;
+                background-color: #f5f5f5;
+                flex-shrink: 0;
+                margin-right: 20rpx;
+            }
+            .con{
+                flex: 1;
+                position: relative;
+                overflow: hidden;
+                .title{
+                    font-size: 32rpx;
+                    font-weight: bold;
+                    margin-bottom: 8rpx;
+                }
+                .tips{
+                    color: #666666;
+                    margin-bottom: 10rpx;
+                    min-height: 38rpx;
+                }
+                .time{
+                    color: #666666;
+                }
+                .audio-box{
+                    position: absolute;
+                    bottom: 0;
+                    right: 0;
+                    width: 99rpx;
+                    height: 39rpx;
+                    background: linear-gradient(100deg, #E3B377 0%, #FFDDB1 100%);
+                    border-radius: 20rpx;
+                    color: #fff;
+                    font-size: 24rpx;
+                    display: flex;
+                    justify-content: center;
+                    align-items: center;
+                    image{
+                        width: 30rpx;
+                        height: 30rpx;
+                        margin-right: 4rpx;
+                    }
+                }
+                .grey-audio-box{
+                    background: linear-gradient(114deg, #B0B0B0 0%, #E5E2E2 100%);
+                }
+            }
+        }
+    }
+
+    .contact-box{
+        position: fixed;
+        bottom: 0;
+        left: 0;
+        right: 0;
+        height: 80rpx;
+        background: #E6B77D;
+        line-height: 80rpx;
+        text-align: center;
+        color: #FFFFFF;
+        font-size: 32rpx;
+        z-index: 10;
+    }
+}
+</style>

+ 114 - 0
pages-report/specialColumn/list.vue

@@ -0,0 +1,114 @@
+<template>
+    <view class="special-column-list-page">
+      <view class="report-empty-box" v-if="finished&&list.length==0">
+        <image :src="globalImgUrls.chartEmpty" mode="widthFix" />
+        <view>暂无数据</view>
+      </view>
+      <view class="list" v-else>
+        <view class="flex item" v-for="item in list" :key="item.classify_id_second" @click="goDetail(item)">
+          <image class="avatar" :src="item.home_img_url" mode="aspectFill" lazyload/>
+          <view class="content">
+            <view class="name">{{item.classify_name_second}}</view>
+            <view class="author">主讲人:{{item.report_author}}</view>
+            <view class="van-ellipsis job">{{item.author_descript}}</view>
+            <view class="num">第{{item.stage}}期 | {{item.product_name}}</view>
+          </view>
+        </view>
+      </view>
+      
+    </view>
+</template>
+
+<script>
+import {apiSpecialColumnList} from '@/api/report'
+export default {
+  data () {
+    return {
+      classifyId:0,
+      classifyName:'',
+
+      list:[],
+      finished:false
+    }
+  },
+  onLoad(options) {
+    this.classifyId=options.classifyId
+    // 设置title
+    this.classifyName=decodeURIComponent(options.classifyName)
+    uni.setNavigationBarTitle({ title: decodeURIComponent(options.classifyName) })
+    this.getList()
+  },
+  onPullDownRefresh() {
+    this.list=[]
+    this.finished=false
+    this.getList()
+    setTimeout(() => {
+      uni.stopPullDownRefresh()
+    }, 1500);
+  },
+  onShareAppMessage() {
+    return {
+      title:this.classifyName
+    }
+  },
+  methods: {
+    async getList(){
+      this.finished=false
+      const res=await apiSpecialColumnList({classify_id_first:Number(this.classifyId)})
+      if(res.code===200){
+        this.list=res.data||[]
+        this.finished=true
+      }
+    },
+
+
+    goDetail(item){
+      uni.navigateTo({ url: '/pages-report/specialColumn/detail?columnId='+item.classify_id_second })
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+page{
+    padding-bottom: 0;
+}
+</style>
+
+<style lang="scss" scoped>
+.special-column-list-page{
+  padding: 34rpx;
+  .list{
+    .item{
+      margin-bottom: 30rpx;
+      .avatar{
+        flex-shrink: 0;
+        margin-right: 30rpx;
+        width: 160rpx;
+        height: 214rpx;
+        border-radius: 16rpx;
+        background-color: #f5f5f5;
+      }
+      .content{
+        flex: 1;
+        overflow: hidden;
+        .name{
+          font-size: 32rpx;
+          font-weight: bold;
+        }
+        .author{
+          color: #666;
+          margin: 10rpx 0;
+        }
+        .job{
+          color: #666;
+        }
+        .num{
+          margin-top: 20rpx;
+          color: #E3B377;
+        }
+      }
+    }
+  }
+}
+</style>

BIN
pages-report/static/audio-change-grey.png


BIN
pages-report/static/audio-change.png


BIN
pages-report/static/audio-pause.png


BIN
pages-report/static/audio-play.png


BIN
pages-report/static/audio-s.png


BIN
pages-report/static/audio.png


BIN
pages-report/static/bg-1.png


BIN
pages-report/static/filter-icon.png


BIN
pages-report/static/search-del.png


BIN
pages-report/static/search-icon-s.png


BIN
pages-report/static/search-icon.png


BIN
pages-report/static/tag.png


+ 88 - 18
pages.json

@@ -1,28 +1,31 @@
 {
 	"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
 		{
-			"path": "pages/activity/activity",
+			"path": "pages/report/report",
 			"style": {
-				"navigationBarTitleText": "报告及活动",
+				"navigationBarTitleText": "报告",
+				"navigationStyle": "custom",
 				"enablePullDownRefresh": true
 			}
 		},
 		{
-			"path": "pages/report/report",
+			"path": "pages/buy/buy",
 			"style": {
-				"navigationBarTitleText": "报告"
+				"navigationBarTitleText": "已购",
+				"enablePullDownRefresh": true
 			}
 		},
 		{
-			"path": "pages/buy/buy",
+			"path": "pages/chart/chart",
 			"style": {
-				"navigationBarTitleText": "已购"
+				"navigationBarTitleText": "ETA图库",
+				"enablePullDownRefresh": true
 			}
 		},
 		{
-			"path": "pages/chart/chart",
+			"path": "pages/activity/activity",
 			"style": {
-				"navigationBarTitleText": "ETA图库",
+				"navigationBarTitleText": "活动",
 				"enablePullDownRefresh": true
 			}
 		},
@@ -53,13 +56,13 @@
 				{
 					"path":"detail",
 					"style":{
-						"navigationBarTitleText":"报告及活动详情"
+						"navigationBarTitleText":"活动详情"
 					}
 				},
 				{
 					"path":"noAuthority",
 					"style":{
-						"navigationBarTitleText":"报告及活动详情"
+						"navigationBarTitleText":"活动详情"
 					}
 				},
 				{
@@ -123,6 +126,70 @@
 					}
 				}
 			]
+		},
+		//已购模块
+		{
+			"root": "pages-buy",
+			"pages": [
+				{
+					"path":"detail",
+					"style":{
+						"navigationBarTitleText":""
+					}
+				}
+			]
+		},
+		// 研报模块
+		{
+			"root": "pages-report",
+			"pages": [
+				{
+					"path":"classify",
+					"style":{
+						"navigationBarTitleText":"FICC研报分类",
+						"enablePullDownRefresh": true
+					}
+				},
+				{
+					"path":"search",
+					"style":{
+						"navigationBarTitleText":"FICC研报"
+					}
+				},
+				// 专栏列表 classifyId 
+				{
+					"path": "specialColumn/list",
+					"style":{
+						"enablePullDownRefresh": true
+					}
+				},
+				// 专栏详情 columnId
+				{
+					"path": "specialColumn/detail",
+					"style":{
+						"enablePullDownRefresh": true
+					}
+				},
+				// 报告列表 classifyId
+				{
+					"path": "reportList",
+					"style":{
+						"enablePullDownRefresh": true
+					}
+				},
+				//报告详情
+				{
+					"path": "reportDetail"
+				},
+				// 章节详情
+				{
+					"path": "chapterDetail"
+				},
+				//ficc介绍页面
+				{
+					"path": "ficcService"
+				}
+			]
 		}
 	],
 		
@@ -131,18 +198,18 @@
 		"color": "#1F243A",
 		"selectedColor": "#E3B377",
 		"list": [
-			{
-				"pagePath": "pages/buy/buy",
-				"text": "已购",
-				"iconPath": "./static/tabbar/buy.png",
-				"selectedIconPath": "./static/tabbar/buy-s.png"
-			},
 			{
 				"pagePath": "pages/report/report",
 				"text": "报告",
 				"iconPath": "./static/tabbar/report.png",
 				"selectedIconPath": "./static/tabbar/report-s.png"
 			},
+			{
+				"pagePath": "pages/buy/buy",
+				"text": "已购",
+				"iconPath": "./static/tabbar/buy.png",
+				"selectedIconPath": "./static/tabbar/buy-s.png"
+			},
 			{
 				"pagePath": "pages/chart/chart",
 				"text": "图库",
@@ -151,7 +218,7 @@
 			},
 			{
 				"pagePath": "pages/activity/activity",
-				"text": "报告及活动",
+				"text": "活动",
 				"iconPath": "./static/tabbar/activity.png",
 				"selectedIconPath": "./static/tabbar/activity-s.png"
 			},
@@ -187,7 +254,10 @@
 			"van-checkbox": "/wxcomponents/vant/checkbox/index",
 			"van-transition": "/wxcomponents/vant/transition/index",
 			"van-collapse": "/wxcomponents/vant/collapse/index",
-  		"van-collapse-item": "/wxcomponents/vant/collapse-item/index"
+  		"van-collapse-item": "/wxcomponents/vant/collapse-item/index",
+			"van-tag": "/wxcomponents/vant/tag/index",
+			"van-row": "/wxcomponents/vant/row/index",
+  		"van-col": "/wxcomponents/vant/col/index"
 		}
 	}
 }

+ 9 - 3
pages/activity/activity.vue

@@ -16,7 +16,7 @@
                 <view
                     :class="['tab-item', tabActive === '1' && 'tab-active']"
                     data-type="1"
-                    >报告及线上会议</view>
+                    >线上会议</view>
                 <view
                     :class="['tab-item', tabActive === '3' && 'tab-active']"
                     data-type="3"
@@ -236,6 +236,7 @@ export default {
             pageSize: 20,
             list: [],
             finished: false,
+            isInit:false,//是否是初始化获取数据
 
             pupData:{
                 show:false,
@@ -259,12 +260,14 @@ export default {
         }
     },
     onLoad() {
+        this.isInit=true
         this.init()
         this.addEventListenerRemind()
         this.addEventListenerRegister()
     },
     onShow() {
         if(this.$store.state.activity.activityListPageRefresh){
+            if(this.isInit) return
             this.allRefresh()
         }
         this.initAudio()
@@ -511,7 +514,7 @@ export default {
             if(userInfoRes.status=='试用'&&userInfoRes.is_suspend==0){
                 this.onlySeeAuth=true
             }
-            this.getList()
+            this.getList('init')
         },
 
         handleAuthChange(e){
@@ -606,7 +609,7 @@ export default {
             });
         },
 
-        async getList() {
+        async getList(e) {
             const res = await apiActivityList({
                 title:this.searchVal,
                 active_state: Number(this.statusActive),
@@ -615,6 +618,9 @@ export default {
                 page: this.page,
                 limit: this.pageSize
             })
+            if(e=='init'){
+                this.isInit=false
+            }
             if (res.code === 200) {
                 if(res.data){
                   this.list = [...this.list, ...res.data]

+ 119 - 4
pages/buy/buy.vue

@@ -1,14 +1,129 @@
 <template>
-  <view>buy</view>
+  <view class="buy-page">
+    <view class="list-wrap">
+      <view class="flex item" v-for="item in list" :key="item.report_id" @click="goDetail(item)">
+        <view class="img" :style="'background-image:url('+item.img_url+')'">
+          <text class="num" v-if="item.unread">{{item.unread}}</text>
+        </view>
+        <view class="content-box">
+          <view class="van-ellipsis title">{{item.title}}</view>
+          <view :class="['van-ellipsis intro',item.content.substr(0,1)=='【'?'text-indent':'']">{{item.content}}</view>
+        </view>
+        <view class="time">{{item.time|showTime}}</view>
+      </view>
+      <view style="color:#999;margin:40rpx 0;text-align:center">- 已经到底了 -</view>
+    </view>
+  </view>
 </template>
 
 <script>
+const moment=require('@/utils/moment-with-locales.min')
+moment.locale('zh-cn');
+import {apiBuyList} from '@/api/buy.js'
 export default {
-  onLoad(){
+  filters: {
+    showTime(e){
+      // 判断是否为今年
+      const isYear=moment(e).isBefore(new Date(), 'year');
+      if(isYear){
+        return moment(e).format("YYYY年MM月DD日");
+      }else{
+        const isDay=moment(e).isBefore(new Date(), 'day');
+        if(isDay){
+          return moment(e).format("MM月DD日");
+        }else{
+          const hour=moment(e).hour()
+          return hour<12?`上午 ${moment(e).format('hh:mm')}`:`下午 ${moment(e).format('hh:mm')}`
+        }
+      }
+    }
+  },
+  data () {
+    return {
+      list:[]
+    }
+  },
+  onShow() {
+    this.getList()
+  },
+  onPullDownRefresh() {
+    this.getList()
+    setTimeout(() => {
+      uni.stopPullDownRefresh()
+    }, 1500);
+  },
+  methods: {
+    async getList(){
+      const res=await apiBuyList()
+      if(res.code===200){
+        this.list=res.data
+      }
+    },
+
+    goDetail(item){
+      uni.navigateTo({ url: `/pages-buy/detail?classify_id_first=${item.classify_id_first}&activity_id=${item.activity_id}&name=${item.classify_name_first}` })
+    }
   }
 }
 </script>
 
-<style>
-
+<style lang="scss" scoped>
+.buy-page{
+  .list-wrap{
+    padding: 30rpx 34rpx;
+    .item{
+      padding: 30rpx 0;
+      border-bottom: 1px solid $global-border-color;
+      align-items: center;
+      .img{
+        flex-shrink: 0;
+        margin-right: 28rpx;
+        width: 120rpx;
+        height: 120rpx;
+        background-color: $global-bg-color-grey;
+        background-size: cover;
+        position: relative;
+        border-radius: 14rpx;
+        .num{
+          position: absolute;
+          top: 0;
+          right: 0;
+          transform: translate(50%,-50%);
+          display: inline-block;
+          box-sizing: border-box;
+          min-width: 28rpx;
+          padding: var(--van-badge-padding);
+          color: #fff;
+          font-size: $global-font-size-mini;
+          line-height: 1.2;
+          text-align: center;
+          background: #ee0a24;
+          border-radius: 28rpx;
+          padding: 2rpx 4rpx;
+        }
+      }
+      .content-box{
+        flex: 1;
+        overflow: hidden;
+        .title{
+          font-size: 14px;
+          font-weight: bold;
+          margin-bottom: 18rpx;
+        }
+        .intro{
+          color: $global-text-color-grey;
+        }
+        .text-indent{
+          margin-left: -12rpx;
+        }
+      }
+      .time{
+        font-size: $global-font-size-sm;
+        color: $global-text-color-grey;
+        flex-shrink: 0;
+        margin-left: 20rpx;
+      }
+    }
+  }
+}
 </style>

+ 2 - 2
pages/login.vue

@@ -133,7 +133,7 @@ export default {
             })
             if(res.code===200){
                 uni.switchTab({
-                    url: '/pages/activity/activity'
+                    url: '/pages/report/report'
                 })
                 this.$store.dispatch('getUserInfo')
                 this.$store.dispatch('getTabBar')
@@ -232,7 +232,7 @@ export default {
             const res = await apiUserLogin(params)
             if (res.code === 200) {
                 uni.switchTab({
-                    url: '/pages/activity/activity'
+                    url: '/pages/report/report'
                 })
                 this.$store.dispatch('getUserInfo')
                 this.$store.dispatch('getTabBar')

+ 431 - 3
pages/report/report.vue

@@ -1,14 +1,442 @@
 <template>
-  <view>report</view>
+  <view class="report-page">
+    <view class="top-sticky" style="background: #fff">
+    <!-- 导航 -->
+    <view class="nav-bar-wrap" :style="{height:navBarStyle.height,paddingTop:navBarStyle.paddingTop,paddingBottom:navBarStyle.paddingBottom}">
+      <view class="content">
+        <van-icon custom-class="search-icon" name="search" size="24px" @click="goSearch" />
+        <view class="text">FICC研报</view>
+      </view>
+    </view>
+    <!-- 分类 -->
+    <view class="type-wrap">
+      <view class="flex first-type-box">
+        <view class="item" v-for="(item,index) in topFirstList" :key="item.classify_name" @click="handleClickTopFirst(item,index)">
+          <image :src="selectTopFirstId==item.classify_name?item.select_icon_url:item.icon_url" mode="aspectFill"/>
+          <view>{{item.classify_name}}</view>
+        </view>
+        <!-- <view class="item" @click="goClassify">
+          <image src="@/static/report-menu.png" mode="aspectFill"/>
+          <view>查看更多</view>
+        </view> -->
+      </view>
+      <view class="flex sub-type-box">
+        <view 
+          :class="['item',item.chart_permission_id==selectTopSubId&&'active']" 
+          v-for="item in topSubList" :key="item.chart_permission_id"
+          @click="handleClickTopSub(item)"
+        >{{item.chart_permission_name}}</view>
+      </view>
+    </view>
+    </view>
+    <view class="report-empty-box" v-if="finished&&list.length==0">
+      <image :src="globalImgUrls.chartEmpty" mode="widthFix" />
+      <view>暂无报告</view>
+    </view>
+    <view class="list-wrap" v-else>
+      <view class="list-item" v-for="item in list" :key="item.date">
+        <view class="time">{{getReportListDate(item.date)}}</view>
+        <view class="content-list">
+          <view class="content-item" v-for="citem in item.sub_list" :key="citem.report_id">
+            <view class="content-box" @click="goDetail(citem)">
+              <view class="all-btn">全部</view>
+              <view class="c-time">{{citem.publish_time|getListTime}}</view>
+              <view class="c-title">{{citem.title}}</view>
+              <view class="desc" v-html="citem.content_sub"></view>
+              <view class="tags">
+                <text style="margin-right:15px" v-if="citem.classify_name_first">#{{citem.classify_name_first}}</text>
+                <text style="margin-right:15px" v-if="citem.classify_name_second">#{{citem.classify_name_second}}</text>
+              </view>
+            </view>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 联系销售弹窗 -->
+    <van-popup :show="authData.show" @close="authData.show=false" :close-on-click-overlay="false">
+      <view class="global-pup">
+          <view class="content">
+            <view>您暂无该品种权限,若想查看请联系对口销售--{{authData.contactInfo.name}}:{{authData.contactInfo.mobile}}
+            </view>
+          </view>
+          <view class="flex bot">
+            <view @click="authData.show=false" style="color:#A9AFB8">取消</view>
+            <view @click="handleCallPhone(authData.contactInfo.mobile)">拨号</view>
+          </view>    
+      </view>
+    </van-popup>
+  </view>
 </template>
 
 <script>
+const moment=require('@/utils/moment-with-locales.min')
+moment.locale('zh-cn');
+import {apiReportIndexPageAuthList,apiReportIndexPageList} from '@/api/report'
 export default {
-  onLoad(){
+  filters: {
+    getListTime(e){
+      return moment(e).format('HH:mm:ss')
+    }
+  },
+  data () {
+    return {
+      navBarStyle:{
+        height:60+'px',
+        paddingTop:40+'px',
+        paddingBottom:'4px'
+      },
+
+      authData:{
+        show:false,
+        isBuy:false,//是否为已购客户
+        contactInfo:''
+      },//如果是已购客户 并且点击了没有开通的品种则弹窗联系销售
+      topFirstList:[],
+      selectTopFirstId:0,
+      topSubList:[],
+      selectTopSubId:0,
+      
+      list:[],
+      dateArr:[],
+      page:1,
+      pageSize:20,
+      finished:false
+    }
+  },
+  onLoad(){ 
+    this.initNavBar()
+    this.getTopAuthList()
+  },
+  onShow() {
+    uni.getSystemInfo({
+			success: function (res) {
+				if (res.windowWidth > 700) {
+					console.log('跳转启动页判断进入pc');
+					uni.reLaunch({
+						url: "/pages/pc",
+					});
+				}
+			},
+		})
+  },
+  onPullDownRefresh() {
+    this.getTopAuthList()
+    setTimeout(() => {
+      uni.stopPullDownRefresh()
+    }, 1500);
+  },
+  onReachBottom() {
+    if(this.finished) return
+    this.page++
+    this.getReportList()
+  },
+  onShareAppMessage() {
+    return {
+      title:"FICC研报"
+    }
+  },
+  methods: {
+    initNavBar(){
+      let menuButtonInfo = uni.getMenuButtonBoundingClientRect()
+      this.navBarStyle={
+        height:(menuButtonInfo.height+menuButtonInfo.top+8)+'px',
+        paddingTop:(menuButtonInfo.top-4)+'px',
+        paddingBottom:'4px'
+      }
+    },
+
+    //顶部权限数据
+    async getTopAuthList(){
+      const res=await apiReportIndexPageAuthList()
+      if(res.code===200){
+        this.authData.isBuy=res.data.check_flag
+        this.authData.contactInfo=res.data.contact_info
+        this.topFirstList=res.data.permission_list
+        this.handleClickTopFirst(this.topFirstList[0],0)
+      }
+    },
+
+    //点击顶部一级分类
+    handleClickTopFirst(item,index){
+      if(item.sort==100000){
+        this.goClassify()
+        return
+      }
+      this.selectTopFirstId=item.classify_name
+      this.topSubList=this.topFirstList[index].list
+      this.handleClickTopSub(this.topSubList[0])
+      this.handleShowAuthData(this.topSubList[0])
+    },
+
+    //点击顶部二级分类
+    handleClickTopSub(item){
+      this.selectTopSubId=item.chart_permission_id
+      this.list=[]
+      this.dateArr=[]
+      this.page=1
+      this.finished=false
+      this.getReportList()
+      this.handleShowAuthData(item)
+    },
+
+    // 判断是否要为已购用户且点击的品种没有权限
+    handleShowAuthData(e){
+      if(this.authData.isBuy){
+        if(!e.auth_ok){
+          this.authData.show=true
+        }
+      }
+    },
+
+    //获取报告列表
+    async getReportList(){
+      const res=await apiReportIndexPageList({
+        chart_permission_id:this.selectTopSubId,
+        current_index:this.page,
+        page_size:this.pageSize
+      })
+      if(res.code===200){
+        if(res.data.list){
+          if(this.list.length==0){
+            this.list=res.data.list
+            res.data.list.forEach(item=>{
+              this.dateArr.push(item.date)
+            })
+          }else{
+            //判断是否前面已经有相同日期数据 有的话添加合并
+            let arr=[]
+            let temTimearr=[]
+            res.data.list.forEach(item => {
+              if(this.dateArr.includes(item.date)){
+                this.list.forEach(_item=>{
+                  if(item.date===_item.date){
+                    _item.sub_list=[..._item.sub_list,...item.sub_list]
+                  }
+                })
+              }else{
+                arr.push(item)
+                temTimearr.push(item.date)
+              }
+            });
+            this.list=[...this.list,...arr]
+            this.dateArr=[...this.dateArr,...temTimearr]
+          }
+        }
+        if(res.data.paging.is_end){
+          this.finished=true
+        }
+      }
+    },
+
+    // 跳转分类
+    goClassify(){
+      uni.navigateTo({ url: '/pages-report/classify' })
+    },
+
+    //跳转搜索
+    goSearch(){
+      uni.navigateTo({url:'/pages-report/search'})
+    },
+
+    //跳转报告详情
+    goDetail(item){
+      if(['晨报','周报'].includes(item.classify_name_first)){
+        uni.navigateTo({url: `/pages-report/chapterDetail?chapterId=${item.report_chapter_id}&fromPage=home`})
+      }else{
+        uni.navigateTo({url:'/pages-report/reportDetail?reportId='+item.report_id})
+      }
+    },
+
+    // 拨打电话
+    handleCallPhone(tel){
+      uni.makePhoneCall({
+        phoneNumber: tel,
+        success:()=>{
+          this.authData.show=false
+        }
+      });
+    },
+
+    //设置列表日期显示
+    getReportListDate(e){
+      const isSameYear=moment(e).isSame(new Date(), 'year');
+      if(isSameYear){//今年
+        return moment(e).format('MM.DD')+' '+ moment(e).format('ddd')
+      }else{
+        return moment(e).format('YY.MM.DD')+' '+moment(e).format('ddd')
+      }
+    }
   }
 }
 </script>
 
-<style>
+<style lang="scss">
+.van-sticky-wrap--fixed{
+  background-color: #fff;
+}
+.nav-bar-wrap{
+  .content{
+    position: relative;
+    height: 100%;
+    .search-icon{
+      position: absolute;
+      left: 34rpx;
+      top: 50%;
+      transform: translateY(-50%);
+    }
+  }
+  
+}
+</style>
+
+<style lang="scss" scoped>
+.top-sticky{
+  position: sticky;
+  top: 0;
+  left: 0;
+  z-index: 99;
+}
+.nav-bar-wrap{
+  border-bottom: 1px solid $global-border-color;
+  .content{
+    .text{
+      text-align: center;
+      width: 50vw;
+      position: absolute;
+      left: 50%;
+      top: 50%;
+      transform: translate(-50%,-50%);
+      font-weight: bold;
+      font-size: 16px;
+    }
+  }
+  
+}
+.type-wrap{
+  // border-bottom: 1px solid $global-border-color;
+  padding: 20rpx 34rpx 0 34rpx;
+  box-shadow: 0px 4rpx 4rpx 1px rgba(198, 198, 198, 0.25);
+  .first-type-box{
+    justify-content: space-between;
+    .item{
+      flex-shrink: 0;
+      text-align: center;
+      font-size: $global-font-size-sm;
+      image{
+        width: 100rpx;
+        height: 100rpx;
+        display: block;
+        margin: 0 auto 15rpx auto;
+      }
+    }
+  }
+  .sub-type-box{
+    margin-top: 40rpx;
+    font-size: 28rpx;
+    color: #666666;
+    overflow-x: auto;
+    &::-webkit-scrollbar{
+      width: 0;
+      height: 0;
+      display: none;
+    }
+    .item{
+      padding-bottom: 16rpx;
+      margin-right: 50rpx;
+      flex-shrink: 0;
+    }
+    .active{
+      color: #E3B377;
+      border-bottom: 2px solid #E3B377;
+    }
+  }
+}
+
+.list-wrap{
+  padding: 20rpx 34rpx;
+  .list-item{
+    margin-bottom: 40rpx;
+    .time{
+      margin-bottom: 32rpx;
+    }
+  }
+  .content-list{
+    .content-box{
+      padding: 0 0rpx 20rpx 20rpx;
+      position: relative;
+      .all-btn{
+        position: absolute;
+        right: 20rpx;
+        bottom: 20rpx;
+        width: 95rpx;
+        height: 34rpx;
+        background: linear-gradient(100deg, #E3B377 0%, #FFDDB1 100%);
+        border-radius: 17rpx;
+        color: #fff;
+        font-size: 24rpx;
+        text-align: center;
+      }
+    }
+    .content-item{
+      padding:0 0 50rpx 20rpx;
+      border-left: 1px solid #F4E1C9;
+      position: relative;
+      &:last-child{
+        border-bottom: none;
+        padding-bottom: 0rpx;
+      }
+      &::before{
+        content: '';
+        display: block;
+        box-sizing: border-box;
+        width: 24rpx;
+        height: 24rpx;
+        border-radius: 50%;
+        border: 6rpx solid #F4E1C9;
+        position: absolute;
+        left: 0;
+        top: 0;
+        background: #E3B377;
+        transform: translate(-50%,-50%);
+        z-index: 2;
+      }
+      &::after{
+        content:'';
+        display: block;
+        width: 34rpx;
+        height: 1rpx;
+        background-color: #F4E1C9;
+        position: absolute;
+        top: 0;
+        left: 0;
+        z-index: 1;
+      }
+      .c-time{
+        position: relative;
+        top: -17rpx;
+        font-size: 24rpx;
+        color: #333333;
+      }
+      .c-title{
+        font-size: $global-font-size-lg;
+        font-weight: bold;
+        word-wrap: break-word;
+        white-space: normal;
+        word-break: break-all;
+      }
+      .desc{
+        line-height: 1.5;
+        margin-top: 10rpx;
+        color: #666666;
+      }
+      .tags{
+        margin-top: 20rpx;
+        color: #E3B377;
+        min-height: 40rpx;
+      }
+    }
+  }
+}
 
 </style>

BIN
static/report-menu.png


+ 3 - 1
store/index.js

@@ -2,6 +2,7 @@ import Vue from 'vue'
 import Vuex from 'vuex'
 import user from './modules/user.js'
 import activity from './modules/activity'
+import report from './modules/report'
 Vue.use(Vuex);//vue的插件机制
 
 //Vuex.Store 构造器选项
@@ -11,7 +12,8 @@ const store = new Vuex.Store({
 	actions:{},
 	modules:{
 		user,
-		activity
+		activity,
+		report
 	}
 })
 export default store

+ 34 - 0
store/modules/report.js

@@ -0,0 +1,34 @@
+const reportModules={
+    state:{
+        audioData:{
+            show:false,//是否显示音频弹窗
+            list:[],
+            index:0,//当前是播放第几个
+            reportId:0,//当前是哪个报告的音频
+            paused:true,//当前是否音频正在播放 true暂停状态
+        }
+    },
+    mutations: {
+        addAudio(state,e){
+            state.audioData.show=true
+            state.audioData.list=e.list
+            state.audioData.index=0
+            state.audioData.reportId=e.reportId
+        },
+        updateAudioIndex(state,e){
+            state.audioData.index=e
+        },
+        updateAudioPause(state,e){
+            state.audioData.paused=e
+        },
+        removeAudio(state,e){
+            state.audioData.show=false
+            state.audioData.list=[]
+            state.audioData.index=0
+            state.audioData.reportId=0
+            state.audioData.paused=true
+        }
+    }
+}
+
+export default reportModules;

+ 12 - 0
style/common.scss

@@ -79,4 +79,16 @@ view{
             border: none;
         }
     }
+}
+
+/* 报告模块公共空白页 */
+.report-empty-box{
+    text-align: center;
+    font-size: 32rpx;
+    color: #999;
+    padding-top: 150rpx;
+    image{
+        width: 346rpx;
+        margin-bottom: 57rpx;
+    }
 }

+ 182 - 0
uni_modules/mp-html/README.md

@@ -0,0 +1,182 @@
+## 功能介绍
+- 全端支持(含 `v3、NVUE`)
+- 支持丰富的标签(包括 `table`、`video`、`svg` 等)
+- 支持丰富的事件效果(自动预览图片、链接处理等)
+- 支持设置占位图(加载中、出错时、预览时)
+- 支持锚点跳转、长按复制等丰富功能
+- 支持大部分 *html* 实体
+- 丰富的插件(关键词搜索、内容 **编辑** 等)
+- 效率高、容错性强且轻量化
+
+查看 [功能介绍](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 | 链接被点击时 |
+
+查看 [事件](https://jin-yufeng.gitee.io/mp-html/#/basic/event) 了解更多
+
+## api
+组件实例上提供了一些 `api` 方法可供调用
+
+| 名称 | 作用 |
+|:---:|---|
+| in | 将锚点跳转的范围限定在一个 scroll-view 内 |
+| navigateTo | 锚点跳转 |
+| getText | 获取文本内容 |
+| getRect | 获取富文本内容的位置和大小 |
+| setContent | 设置富文本内容 |
+| imgList | 获取所有图片的数组 |
+
+查看 [api](https://jin-yufeng.gitee.io/mp-html/#/advanced/api) 了解更多
+
+## 插件扩展  
+除基本功能外,本组件还提供了丰富的扩展,可按照需要选用
+
+| 名称 | 作用 |
+|:---:|---|
+| audio | 音乐播放器 |
+| editable | 富文本 **编辑**([示例项目](https://6874-html-foe72-1259071903.tcb.qcloud.la/editable.zip?sign=cc0017be203fb3dbca62d33a0c15792e&t=1608447445)) |
+| emoji | 解析 emoji |
+| highlight | 代码块高亮显示 |
+| markdown | 渲染 markdown |
+| search | 关键词搜索 |
+| style | 匹配 style 标签中的样式 |
+| txv-video | 使用腾讯视频 |
+| img-cache | 图片缓存 by [@PentaTea](https://github.com/PentaTea) |
+
+从插件市场导入的包中 **不含有** 扩展插件,需要使用插件参考以下方法:  
+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) 了解更多
+
+## 示例体验
+![富文本插件](https://gitee.com/jin-yufeng/mp-html/raw/master/docs/assets/case/富文本插件.jpg)
+
+## 关于 nvue
+`nvue` 使用原生渲染,不支持部分 `css` 样式,为实现和 `html` 相同的效果,组件内部通过 `web-view` 进行渲染,性能上差于原生,根据 `weex` 官方建议,`web` 标签仅应用在非常规的降级场景。因此,如果通过原生的方式(如 `richtext`)能够满足需要,则不建议使用本组件,如果有较多的富文本内容,则可以直接使用 `vue` 页面  
+由于渲染方式与其他端不同,有以下限制:  
+1. 不支持 `lazy-load` 属性
+2. 视频不支持全屏播放
+
+纯 `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://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` 项目以及复现方式,对于 **描述不清**、**无法复现** 或重复的问题将不予回复  
+
+查看 [问题反馈](https://jin-yufeng.gitee.io/mp-html/#/question/feedback) 了解更多

+ 75 - 0
uni_modules/mp-html/changelog.md

@@ -0,0 +1,75 @@
+## v2.2.2(2022-02-26)
+1. `A` 增加了 [pauseMedia](https://jin-yufeng.gitee.io/mp-html/#/advanced/api#pauseMedia) 的 `api`,可用于暂停播放音视频 [详细](https://github.com/jin-yufeng/mp-html/issues/317)
+2. `U` 优化了长内容的加载速度  
+3. `U` 适配 `vue3` [#389](https://github.com/jin-yufeng/mp-html/issues/389)、[#398](https://github.com/jin-yufeng/mp-html/pull/398) by [@zhouhuafei](https://github.com/zhouhuafei)、[#400](https://github.com/jin-yufeng/mp-html/issues/400)
+4. `F` 修复了小程序端图片高度设置为百分比时可能不显示的问题
+5. `F` 修复了 `highlight` 插件部分情况下可能显示不完整的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/403)
+## v2.2.1(2021-12-24)
+1. `A` `editable` 插件增加上下移动标签功能
+2. `U` `editable` 插件支持在文本中间光标处插入内容
+3. `F` 修复了 `nvue` 端设置 `margin` 后可能导致高度不正确的问题
+4. `F` 修复了 `highlight` 插件使用压缩版的 `prism.css` 可能导致背景失效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/367)
+5. `F` 修复了编辑状态下使用 `emoji` 插件内容为空时可能报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/371)
+6. `F` 修复了使用 `editable` 插件后将 `selectable` 属性设置为 `force` 不生效的问题
+## v2.2.0(2021-10-12)
+1. `A` 增加 `customElements` 配置项,便于添加自定义功能性标签 [详细](https://github.com/jin-yufeng/mp-html/issues/350)
+2. `A` `editable` 插件增加切换音视频自动播放状态的功能 [详细](https://github.com/jin-yufeng/mp-html/pull/341) by [@leeseett](https://github.com/leeseett)
+3. `A` `editable` 插件删除媒体标签时触发 `remove` 事件,便于删除已上传的文件
+4. `U` `editable` 插件 `insertImg` 方法支持同时插入多张图片 [详细](https://github.com/jin-yufeng/mp-html/issues/342)
+5. `U` `editable` 插入图片和音视频时支持拼接 `domian` 主域名
+6. `F` 修复了内部链接参数中包含 `://` 时被认为是外部链接的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/356)
+7. `F` 修复了部分 `svg` 标签名或属性名大小写不正确时不生效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/351)
+8. `F` 修复了 `nvue` 页面运行到非 `app` 平台时可能样式错误的问题
+## v2.1.5(2021-08-13)
+1. `A` 增加支持标签的 `dir` 属性
+2. `F` 修复了 `ruby` 标签文字与拼音没有居中对齐的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/325)
+3. `F` 修复了音视频标签内有 `a` 标签时可能无法播放的问题
+4. `F` 修复了 `externStyle` 中的 `class` 名包含下划线或数字时可能失效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/326)
+5. `F` 修复了 `h5` 端引入 `externStyle` 可能不生效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/326)
+## v2.1.4(2021-07-14)
+1. `F` 修复了 `rt` 标签无法设置样式的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/318)
+2. `F` 修复了表格中有单元格同时合并行和列时可能显示不正确的问题
+3. `F` 修复了 `app` 端无法关闭图片长按菜单的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/322)
+4. `F` 修复了 `editable` 插件只能添加图片链接不能修改的问题 [详细](https://github.com/jin-yufeng/mp-html/pull/312) by [@leeseett](https://github.com/leeseett)
+## v2.1.3(2021-06-12)
+1. `A` `editable` 插件增加 `insertTable` 方法
+2. `U` `editable` 插件支持编辑表格中的空白单元格 [详细](https://github.com/jin-yufeng/mp-html/issues/310)
+3. `F` 修复了 `externStyle` 中使用伪类可能失效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/298)
+4. `F` 修复了多个组件同时使用时 `tag-style` 属性时可能互相影响的问题 [详细](https://github.com/jin-yufeng/mp-html/pull/305) by [@woodguoyu](https://github.com/woodguoyu)
+5. `F` 修复了包含 `linearGradient` 的 `svg` 可能无法显示的问题
+6. `F` 修复了编译到头条小程序时可能报错的问题
+7. `F` 修复了 `nvue` 端不触发 `click` 事件的问题
+8. `F` 修复了 `editable` 插件尾部插入时无法撤销的问题
+9. `F` 修复了 `editable` 插件的 `insertHtml` 方法只能在末尾插入的问题
+10. `F` 修复了 `editable` 插件插入音频不显示的问题
+## v2.1.2(2021-04-24)
+1. `A` 增加了 [img-cache](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#img-cache) 插件,可以在 `app` 端缓存图片 [详细](https://github.com/jin-yufeng/mp-html/issues/292) by [@PentaTea](https://github.com/PentaTea)
+2. `U` 支持通过 `container-style` 属性设置 `white-space` 来保留连续空格和换行符 [详细](https://jin-yufeng.gitee.io/mp-html/#/question/faq#space)
+3. `U` 代码风格符合 [standard](https://standardjs.com) 标准
+4. `U` `editable` 插件编辑状态下支持预览视频 [详细](https://github.com/jin-yufeng/mp-html/issues/286)
+5. `F` 修复了 `svg` 标签内嵌 `svg` 时无法显示的问题
+6. `F` 修复了编译到支付宝和头条小程序时部分区域不可复制的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/291)
+## v2.1.1(2021-04-09)
+1. 修复了对 `p` 标签设置 `tag-style` 可能不生效的问题
+2. 修复了 `svg` 标签中的文本无法显示的问题
+3. 修复了使用 `editable` 插件编辑表格时可能报错的问题
+4. 修复了使用 `highlight` 插件运行到头条小程序时可能没有样式的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/280)
+5. 修复了使用 `editable` 插件 `editable` 属性为 `false` 时会报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/284)
+6. 修复了 `style` 插件连续子选择器失效的问题
+7. 修复了 `editable` 插件无法修改图片和字体大小的问题
+## v2.1.0.2(2021-03-21)
+修复了 `nvue` 端使用可能报错的问题
+## v2.1.0(2021-03-20)
+1. `A` 增加了 [container-style](https://jin-yufeng.gitee.io/mp-html/#/basic/prop#container-style) 属性 [详细](https://gitee.com/jin-yufeng/mp-html/pulls/1)
+2. `A` 增加支持 `strike` 标签
+3. `A` `editable` 插件增加 `placeholder` 属性 [详细](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#editable)
+4. `A` `editable` 插件增加 `insertHtml` 方法 [详细](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#editable)
+5. `U` 外部样式支持标签名选择器 [详细](https://jin-yufeng.gitee.io/mp-html/#/overview/quickstart#setting)
+6. `F` 修复了 `nvue` 端部分情况下可能不显示的问题
+## v2.0.5(2021-03-12)
+1. `U` [linktap](https://jin-yufeng.gitee.io/mp-html/#/basic/event#linktap) 事件增加返回内部文本内容 `innerText` [详细](https://github.com/jin-yufeng/mp-html/issues/271)
+2. `U` [selectable](https://jin-yufeng.gitee.io/mp-html/#/basic/prop#selectable) 属性设置为 `force` 时能够在微信 `iOS` 端生效(文本块会变成 `inline-block`) [详细](https://github.com/jin-yufeng/mp-html/issues/267)
+3. `F` 修复了部分情况下竖向无法滚动的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/182)
+4. `F` 修复了多次修改富文本数据时部分内容可能不显示的问题
+5. `F` 修复了 [腾讯视频](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#txv-video) 插件可能无法播放的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/265)
+6. `F` 修复了 [highlight](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#highlight) 插件没有设置高亮语言时没有应用默认样式的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/276) by [@fuzui](https://github.com/fuzui)

+ 458 - 0
uni_modules/mp-html/components/mp-html/mp-html.vue

@@ -0,0 +1,458 @@
+<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]" 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.2.2
+ * @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} 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', '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')
+    clearInterval(this._timer)
+  },
+  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 {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')
+      })
+
+      // 等待图片加载完毕
+      let height
+      clearInterval(this._timer)
+      this._timer = setInterval(() => {
+        this.getRect().then(rect => {
+          // 350ms 总高度无变化就触发 ready 事件
+          if (rect.height === height) {
+            this.$emit('ready', rect)
+            clearInterval(this._timer)
+          }
+          height = rect.height
+        }).catch(() => { })
+      }, 350)
+      // #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) + ',' + 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(() => { })
+          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 '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>

+ 536 - 0
uni_modules/mp-html/components/mp-html/node/node.vue

@@ -0,0 +1,536 @@
+<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'&&((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 -->
+      <!-- #ifdef APP-PLUS && VUE3 -->
+      <image v-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'" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
+      <!-- #endif -->
+      <!-- #ifndef H5 || APP-PLUS -->
+      <image v-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'" :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 -->
+      <!-- 文本 -->
+      <!-- #ifndef MP-BAIDU || MP-ALIPAY || MP-TOUTIAO -->
+      <text v-else-if="n.text" :user-select="n.us" 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" />
+      <!-- #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" :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="handler.use(n)" :id="n.attrs.id" :style="n.f" :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" :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 是否使用 rich-text 显示剩余内容
+ */
+module.exports = {
+  use: function (item) {
+    if (item.c) return false
+    // 微信和 QQ 的 rich-text inline 布局无效
+    return !inlineTags[item.name] && (item.attrs.style || '').indexOf('display:inline') == -1
+  }
+}
+</script>
+<script>
+
+import node from './node'
+export default {
+  name: 'node',
+  options: {
+    // #ifdef MP-WEIXIN
+    virtualHost: true,
+    // #endif
+    // #ifdef MP-TOUTIAO
+    addGlobalClass: false
+    // #endif
+  },
+  data () {
+    return {
+      ctrl: {}
+    }
+  },
+  props: {
+    name: String,
+    attrs: {
+      type: Object,
+      default () {
+        return {}
+      }
+    },
+    childs: Array,
+    opts: Array
+  },
+  // #ifndef H5 && VUE3
+  components: {
+
+    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 () { },
+    // #endif
+    /**
+     * @description 播放视频事件
+     * @param {Event} e
+     */
+    play (e) {
+      // #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
+          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({
+          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)
+      }
+    },
+
+    /**
+     * @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)
+        }
+      }
+      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>

+ 1260 - 0
uni_modules/mp-html/components/mp-html/parser.js

@@ -0,0 +1,1260 @@
+/**
+ * @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: '…'
+  },
+
+  // 默认的标签样式
+  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 html 解析器
+ * @param {Object} vm 组件实例
+ */
+function Parser (vm) {
+  this.options = vm || {}
+  this.tagStyle = Object.assign({}, config.tagStyle, this.options.tagStyle)
+  this.imgList = vm.imgList || []
+  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()
+  }
+  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
+    }
+  } else if (domain && !url.includes('data:') && !url.includes('://')) {
+    url = domain + '/' + url
+  }
+  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
+              break
+            }
+            // #ifndef H5 || APP-PLUS
+            const style = item.attrs.style || ''
+            if (style.includes('flex:') && !style.includes('flex:0') && !style.includes('flex: 0') && (!styleObj.width || !styleObj.width.includes('%'))) {
+              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)
+          // #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 (styleObj.width) {
+        if (styleObj.width.includes('auto')) {
+          styleObj.width = ''
+        } else {
+          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
+  } 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()
+  } /* #ifdef APP-PLUS */ else if (node.name === 'video') {
+    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="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)
+    if (node.c) {
+      // padding 和 spacing 默认 2
+      if (isNaN(padding)) {
+        padding = 2
+      }
+      if (isNaN(spacing)) {
+        spacing = 2
+      }
+    }
+    if (border) {
+      attrs.style += ';border:' + border + 'px solid 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++, col++) {
+          const td = trList[row - 1].children[j]
+          if (td.name === 'td' || td.name === 'th') {
+            // 这个格子被上面的单元格占用,则列号++
+            while (map[row + '.' + col]) {
+              col++
+            }
+            let style = td.attrs.style || ''
+            const 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 += (border ? `;border:${border}px solid gray` + (spacing ? '' : ';border-right:0;border-bottom:0') : '') + (padding ? `;padding:${padding}px` : '')
+            // 处理列合并
+            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)
+          }
+        }
+        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 solid 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) {
+    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.c = 1
+      }
+      // #endif
+      if (!child.c || child.name === 'table') {
+        node.c = 1
+      }
+    }
+  }
+
+  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')
+    // #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')) {
+    let i = children.length - 1
+    for (let j = i; j >= -1; j--) {
+      // 合并多个块级标签
+      if (j === -1 || children[j].c || !children[j].name || (children[j].name !== 'div' && children[j].name !== 'p' && children[j].name[0] !== 'h') || (children[j].attrs.style || '').includes('inline')) {
+        if (i - j >= 5) {
+          children.splice(j + 1, i - j, {
+            name: 'div',
+            attrs: {},
+            children: node.children.slice(j + 1, i + 1)
+          })
+        }
+        i = j - 1
+      }
+    }
+  }
+  // #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' || styleObj[key][0] === '-' || (key === '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
+  if (!attrs.style) {
+    delete attrs.style
+  }
+  // #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 === ' ' && flag) return
+    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')) {
+      this.expose()
+      node.us = 'T'
+    }
+    // #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

+ 79 - 0
uni_modules/mp-html/package.json

@@ -0,0 +1,79 @@
+{
+    "id": "mp-html",
+    "displayName": "mp-html 富文本组件【全端支持,可编辑】",
+    "version": "v2.2.2",
+    "description": "一个强大的富文本组件,高效轻量,功能丰富",
+    "keywords": [
+        "富文本",
+        "编辑器",
+        "html",
+        "rich-text",
+        "editor"
+    ],
+    "repository": "https://github.com/jin-yufeng/mp-html",
+    "dcloudext": {
+        "category": [
+            "前端组件",
+            "通用组件"
+        ],
+        "sale": {
+            "regular": {
+                "price": "0.00"
+            },
+            "sourcecode": {
+                "price": "0.00"
+            }
+        },
+        "contact": {
+            "qq": ""
+        },
+        "declaration": {
+            "ads": "无",
+            "data": "无",
+            "permissions": "无"
+        },
+        "npmurl": "https://www.npmjs.com/package/mp-html"
+    },
+    "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"
+                }
+            }
+        }
+    }
+}

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
uni_modules/mp-html/static/app-plus/mp-html/js/handler.js


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
uni_modules/mp-html/static/app-plus/mp-html/js/uni.webview.min.js


+ 1 - 0
uni_modules/mp-html/static/app-plus/mp-html/local.html

@@ -0,0 +1 @@
+<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"><style>body,html{width:100%;height:100%;overflow-x:scroll;overflow-y:hidden}body{margin:0}video{width:300px;height:225px}img{max-width:100%;-webkit-touch-callout:none}</style></head><body><div id="content" style="overflow:hidden"></div><script type="text/javascript" src="./js/uni.webview.min.js"></script><script type="text/javascript" src="./js/handler.js"></script></body>

+ 12 - 10
utils/config.js

@@ -7,11 +7,11 @@ let h5BaseUrl=''// h5页面根路径
 let pcBaseUrl=''//pc页面根路径
 if(env.envVersion==='develop'){//开发
     baseApiUrl='http://8.136.199.33:8612/api'
-    h5BaseUrl='http://advisoryadmin.brilliantstart.cn/xcx_h5'
+    h5BaseUrl='http://xcxh5test.hzinsights.com/xcx_h5'
     pcBaseUrl='https://ybpctest.hzinsights.com'
 }else if(env.envVersion==='trial'){//体验版
     baseApiUrl='http://8.136.199.33:8612/api'
-    h5BaseUrl='http://advisoryadmin.brilliantstart.cn/xcx_h5'
+    h5BaseUrl='http://xcxh5test.hzinsights.com/xcx_h5'
     pcBaseUrl='https://ybpctest.hzinsights.com'
 }else if(env.envVersion==='release'){//正式版
     baseApiUrl='https://yanbao.hzinsights.com/api'
@@ -27,19 +27,14 @@ const globalImgUrls={
     activityNoAuth:'https://hzstatic.hzinsights.com/static/icon/hzyb/activity_no_auth.png',
     imgSuccess:'https://hzstatic.hzinsights.com/static/icon/hzyb/success_icon.png',
     loginTop:'https://hzstatic.hzinsights.com/static/icon/hzyb/login_top_img.png',
-    idCardExp:'https://hzstatic.hzinsights.com/static/icon/hzyb/idcard_exp.jpg',
+    idCardExp:'https://hzstatic.hzinsights.com/static/icon/hzyb/idcard_exp.jpg',//名片示例图
     defaultAvatar:'https://hzstatic.hzinsights.com/static/icon/hzyb/default_avatar.png',
+    ficcServiceImg:'https://hzstatic.hzinsights.com/static/icon/hzyb/ficc_service.png',//ficc服务banner图
+    ficcServicePage:'https://hzstatic.hzinsights.com/static/icon/hzyb/ficc_service_page.png',//ficc服务介绍图
 }
 
 // 默认tabbar数据
 const defaultTabBarListConfig=[
-    {
-        key: "activity",
-        pagePath: "pages/activity/activity",
-        text: "报告及活动",
-        iconPath: "../static/tabbar/activity.png",
-        selectedIconPath: "../static/tabbar/activity-s.png",
-    },
     {
         key: "report",
         pagePath: "pages/report/report",
@@ -61,6 +56,13 @@ const defaultTabBarListConfig=[
         iconPath: "../static/tabbar/chart.png",
         selectedIconPath: "../static/tabbar/chart-s.png",
     },
+    {
+        key: "activity",
+        pagePath: "pages/activity/activity",
+        text: "活动",
+        iconPath: "../static/tabbar/activity.png",
+        selectedIconPath: "../static/tabbar/activity-s.png",
+    },
     {
         key: "user",
         pagePath: "pages/user/user",

+ 1 - 0
utils/request.js

@@ -92,6 +92,7 @@ const http=(url,params,method)=>{
 			method:method,
 			header:{
 				Authorization:store.state.user.token,
+				version:'yb3.0'
 			},
 			success(e) {
 				// 接口404

Неке датотеке нису приказане због велике количине промена