refactor: 替换rich-text为uv-parse组件以优化富文本解析 fix: 修复图片加载错误处理和黑名单用户限制 style: 调整页面样式和布局 docs: 更新uv-parse组件文档和配置 chore: 添加uv-parse组件依赖和静态资源v1
| @ -0,0 +1,23 @@ | |||
| const api = { | |||
| // 获取回收去向 | |||
| getRecyclingDestination: { | |||
| url: '/recycle-admin/applet/index/getRecyclingDestination', | |||
| method: 'GET', | |||
| auth : false, | |||
| }, | |||
| // 获取回收去向详情 | |||
| getRecyclingDestinationDetail: { | |||
| url: '/recycle-admin/applet/index/getRecyclingDestinationDetail', | |||
| method: 'GET', | |||
| auth : false, | |||
| }, | |||
| // 联系客服问题相关详情 | |||
| getQuestionListDetail: { | |||
| url: '/recycle-admin/applet/index/getQuestionListDetail', | |||
| method: 'GET', | |||
| auth : false, | |||
| }, | |||
| } | |||
| export default api | |||
| @ -0,0 +1,63 @@ | |||
| <template> | |||
| <view class="float-button"> | |||
| <!-- <button type="default" open-type="contact" class="kf-btn"> | |||
| <img src="../../static/images/details/kefu.svg" style="width: 26px;height: 36px;" alt="kefu" | |||
| srcset=""> | |||
| </button> --> | |||
| <button open-type="contact" class="kf-btn"> | |||
| <img src="@/static/image/base/kefu.svg" style="width: 26px;height: 36px;" alt="kefu" srcset=""> | |||
| </button> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| data() { | |||
| return { | |||
| }; | |||
| }, | |||
| methods: { | |||
| // openCustomerService() { | |||
| // uni.openCustomerServiceChat({ | |||
| // //企业微信的企业id | |||
| // corpId: 'wwccd9a21f09fed62d', | |||
| // extInfo: { | |||
| // //客服链接 | |||
| // url: 'https://work.weixin.qq.com/kfid/kfc09f128696578f66d' | |||
| // }, | |||
| // success: (e) => { | |||
| // console.log('e', e) | |||
| // }, | |||
| // fail: (err) => { | |||
| // console.log('err', err) | |||
| // } | |||
| // }) | |||
| // } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss"> | |||
| .float-button { | |||
| position: fixed; | |||
| bottom: 180px; | |||
| /* 距离底部的距离 */ | |||
| right: 10px; | |||
| /* 距离右侧的距离 */ | |||
| width: 50px; | |||
| /* 按钮的宽度 */ | |||
| height: 50px; | |||
| /* 按钮的高度 */ | |||
| /* 其他样式 */ | |||
| .kf-btn { | |||
| background-color: rgba(255, 255, 255, 1); | |||
| height: 52px; | |||
| width: 52px; | |||
| border-radius: 50%; | |||
| display: flex; | |||
| align-items: center; | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,143 @@ | |||
| <template> | |||
| <!-- <view class="navbar" | |||
| :style="{backgroundColor : bgColor}"> --> | |||
| <view class="title" | |||
| :style="{backgroundColor : bgColor,color}"> | |||
| <view class="left"> | |||
| <uv-icon name="home" | |||
| v-if="leftClick && length == 1" | |||
| @click="toHome" | |||
| :color="color" size="46rpx"></uv-icon> | |||
| <uv-icon name="arrow-left" | |||
| v-else-if="leftClick" | |||
| @click="$emit('leftClick')" | |||
| :color="color" size="46rpx"></uv-icon> | |||
| </view> | |||
| <view>{{ title }}</view> | |||
| <view class="icon"> | |||
| <uv-icon name="search" | |||
| v-if="isSearch" | |||
| :color="color" size="58rpx"></uv-icon> | |||
| <uv-icon name="plus-circle" :color="color" | |||
| v-if="isPlus" | |||
| @click="plusCircleShow = true" | |||
| size="46rpx" style="margin-left: 30rpx;"></uv-icon> | |||
| <view v-if="moreClick" style="margin-left: 30rpx;"> | |||
| <uv-icon name="more-dot-fill" :color="color" | |||
| v-if="!moreText" | |||
| @click="moreClick()" | |||
| size="46rpx"></uv-icon> | |||
| <view v-else @click="moreClick" | |||
| style="font-weight: 400;font-size: 30rpx;"> | |||
| {{ moreText }} | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <!-- </view> --> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name:"navbar", | |||
| props : { | |||
| title : { | |||
| type : String, | |||
| default : '' | |||
| }, | |||
| leftClick : { | |||
| type : Boolean, | |||
| }, | |||
| moreClick : { | |||
| type : Function, | |||
| }, | |||
| isSearch : { | |||
| type : Boolean, | |||
| default : false, | |||
| }, | |||
| isPlus : { | |||
| type : Boolean, | |||
| default : false, | |||
| }, | |||
| moreText : { | |||
| }, | |||
| bgColor : { | |||
| default : '#fff' | |||
| }, | |||
| color : { | |||
| default : '#333' | |||
| } | |||
| }, | |||
| created() { | |||
| }, | |||
| beforeDestroy() { | |||
| }, | |||
| data() { | |||
| return { | |||
| length : getCurrentPages().length | |||
| }; | |||
| }, | |||
| methods : { | |||
| toHome(){ | |||
| if(this.length != 1){ | |||
| return | |||
| } | |||
| uni.reLaunch({ | |||
| url: '/pages/index/index' | |||
| }) | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| // .navbar{ | |||
| // width: 100%; | |||
| // height: 120rpx; | |||
| // padding-top: var(--status-bar-height); | |||
| // } | |||
| .title{ | |||
| position: sticky; | |||
| top: 0; | |||
| left: 0; | |||
| padding-top: calc(var(--status-bar-height) + 20rpx); | |||
| width: 100%; | |||
| height: 100rpx; | |||
| background-color: #fff; | |||
| display: flex; | |||
| justify-content: center; | |||
| font-size: 32rpx; | |||
| align-items: center; | |||
| z-index: 999; | |||
| .left{ | |||
| position: absolute; | |||
| left: 40rpx; | |||
| display: flex; | |||
| justify-content: flex-start; | |||
| } | |||
| .icon{ | |||
| position: absolute; | |||
| right: 40rpx; | |||
| display: flex; | |||
| justify-content: flex-end; | |||
| } | |||
| } | |||
| @keyframes fade-in { | |||
| 0% { | |||
| opacity: 0; | |||
| } | |||
| 100% { | |||
| opacity: 1; | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,134 @@ | |||
| <template> | |||
| <view class="tabbar-box"> | |||
| <view class="tabbar"> | |||
| <view :class="{ 'tabbar-active' : select == item.key}" v-for="(item, index) in list" :key="index" | |||
| v-if="!item.isNotShop || !userShop" @click="toPath(item, index)" class="tabbar-item"> | |||
| <view class="tabbar-icon"> | |||
| <image :src="select == item.key ? | |||
| item.selectedIconPath : | |||
| item.iconPath" class="tabbar-icon-image" mode="aspectFill"></image> | |||
| </view> | |||
| <view class="tabbar-title"> | |||
| {{ item.title }} | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import { | |||
| mapGetters | |||
| } from 'vuex' | |||
| export default { | |||
| name: "tabbar", | |||
| props: ['select'], | |||
| computed: { | |||
| ...mapGetters(['userShop']), | |||
| }, | |||
| data() { | |||
| return { | |||
| list: [{ | |||
| "selectedIconPath": "/static/image/tabbar/home-active.png", | |||
| "iconPath": "/static/image/tabbar/home.png", | |||
| "pagePath": "/pages/index/index", | |||
| "title": "首页", | |||
| key: 'home', | |||
| }, | |||
| { | |||
| "selectedIconPath": "/static/image/tabbar/product-list-active.png", | |||
| "iconPath": "/static/image/tabbar/product-list.png", | |||
| "pagePath": "/pages/index/category", | |||
| "title": "商品列表", | |||
| key: 'category', | |||
| }, | |||
| { | |||
| "selectedIconPath": "/static/image/tabbar/order-active.png", | |||
| "iconPath": "/static/image/tabbar/order.png", | |||
| "pagePath": "/pages/index/order", | |||
| "title": "订单", | |||
| key: 'order', | |||
| }, | |||
| { | |||
| "selectedIconPath": "/static/image/tabbar/cart-active.png", | |||
| "iconPath": "/static/image/tabbar/cart.png", | |||
| "pagePath": "/pages/index/cart", | |||
| "title": "购物车", | |||
| key: 'cart', | |||
| }, | |||
| { | |||
| "selectedIconPath": "/static/image/tabbar/user-center-active.png", | |||
| "iconPath": "/static/image/tabbar/user-center.png", | |||
| "pagePath": "/pages/index/center", | |||
| "title": "我的", | |||
| key: 'center', | |||
| } | |||
| ] | |||
| }; | |||
| }, | |||
| methods: { | |||
| toPath(item, index) { | |||
| if (item.key == this.select) { | |||
| return | |||
| } | |||
| uni.reLaunch({ | |||
| url: item.pagePath | |||
| }) | |||
| }, | |||
| } | |||
| } | |||
| </script> | |||
| <style scoped lang="scss"> | |||
| .tabbar-box { | |||
| height: 120rpx; | |||
| padding-bottom: env(safe-area-inset-bottom); | |||
| .tabbar { | |||
| position: fixed; | |||
| width: 750rpx; | |||
| background-color: #fff; | |||
| display: flex; | |||
| justify-content: center; | |||
| align-items: center; | |||
| flex-direction: row; | |||
| height: 120rpx; | |||
| padding-bottom: env(safe-area-inset-bottom); | |||
| z-index: 999999; | |||
| bottom: 0; | |||
| left: 0; | |||
| color: #BCBCBC; | |||
| .tabbar-item { | |||
| flex: 1; | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: center; | |||
| align-items: center; | |||
| .tabbar-icon { | |||
| width: 54rpx; | |||
| height: 54rpx; | |||
| .tabbar-icon-image { | |||
| width: 54rpx; | |||
| height: 54rpx; | |||
| } | |||
| } | |||
| .tabbar-title { | |||
| overflow: hidden; | |||
| white-space: nowrap; | |||
| text-overflow: ellipsis; | |||
| -o-text-overflow: ellipsis; | |||
| font-size: 23rpx; | |||
| line-height: 35rpx; | |||
| } | |||
| } | |||
| .tabbar-active { | |||
| color: $uni-color !important; | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @ -1,380 +1,369 @@ | |||
| <template> | |||
| <view class="login-container"> | |||
| <!-- 应用标题和副标题 --> | |||
| <view class="app-header"> | |||
| <image :src="logoImage" alt="logo" style="width: 120rpx; height: 120rpx; display: block; margin: 0 auto;" /> | |||
| <view class="app-title">{{ logoName }}</view> | |||
| <!-- <view class="app-subtitle">旧物很有用·回收很简单</view> --> | |||
| </view> | |||
| <!-- 登录操作区域 --> | |||
| <view class="login-actions"> | |||
| <button class="login-btn" @click="handleLogin">登录</button> | |||
| <button class="cancel-btn" @click="handleCancel">取消登录</button> | |||
| <view class="agreement"> | |||
| <checkbox-group @change="handleAgreementChange" class="radio-group"> | |||
| <label class="radio-label"> | |||
| <checkbox :checked="agreed" color="#fdbd3e" class="custom-radio"/> | |||
| <text class="agreement-text">我已阅读并同意</text> | |||
| <text class="protocol-link" @click.stop="openProtocol('service')">《服务协议》</text> | |||
| <text class="agreement-text">和</text> | |||
| <text class="protocol-link" @click.stop="openProtocol('privacy')">《隐私政策》</text> | |||
| </label> | |||
| </checkbox-group> | |||
| </view> | |||
| </view> | |||
| <!-- <uv-divider :dashed = "true"></uv-divider> --> | |||
| <!-- <view class="admin-login" @click="goToAdminLogin">管理员登录→</view> --> | |||
| <PrivacyPopup | |||
| ref="privacyPopup" | |||
| :needPhone="needPhone" | |||
| @agree="handleAgreePrivacy" | |||
| @reject="handleRejectPrivacy" | |||
| @open-protocol="openProtocol" | |||
| /> | |||
| <ProtocolDialog | |||
| ref="protocolDialog" | |||
| :show="showProtocolDialog" | |||
| :title="protocolDialogTitle" | |||
| :content="protocolDialogContent" | |||
| @close="showProtocolDialog = false" | |||
| @agree="handleProtocolAgree" | |||
| @reject="handleProtocolReject" | |||
| /> | |||
| </view> | |||
| <view class="login-container"> | |||
| <!-- 应用标题和副标题 --> | |||
| <view class="app-header"> | |||
| <image :src="logoImage" mode="widthFix" | |||
| style="width: 170rpx; height: 170rpx; display: block; margin: 0 auto;" /> | |||
| <view class="app-title">{{ logoName }}</view> | |||
| <!-- <view class="app-subtitle">旧物很有用·回收很简单</view> --> | |||
| </view> | |||
| <!-- 登录操作区域 --> | |||
| <view class="login-actions"> | |||
| <button class="login-btn" @click="handleLogin">登录</button> | |||
| <button class="cancel-btn" @click="handleCancel">取消登录</button> | |||
| <view class="agreement"> | |||
| <checkbox-group @change="handleAgreementChange" class="radio-group"> | |||
| <label class="radio-label"> | |||
| <checkbox :checked="agreed" color="#fdbd3e" class="custom-radio" /> | |||
| <text class="agreement-text">我已阅读并同意</text> | |||
| <text class="protocol-link" @click.stop="openProtocol('service')">《服务协议》</text> | |||
| <text class="agreement-text">和</text> | |||
| <text class="protocol-link" @click.stop="openProtocol('privacy')">《隐私政策》</text> | |||
| </label> | |||
| </checkbox-group> | |||
| </view> | |||
| </view> | |||
| <!-- <uv-divider :dashed = "true"></uv-divider> --> | |||
| <!-- <view class="admin-login" @click="goToAdminLogin">管理员登录→</view> --> | |||
| <PrivacyPopup ref="privacyPopup" :needPhone="needPhone" @agree="handleAgreePrivacy" | |||
| @reject="handleRejectPrivacy" @open-protocol="openProtocol" /> | |||
| <ProtocolDialog ref="protocolDialog" :show="showProtocolDialog" :title="protocolDialogTitle" | |||
| :content="protocolDialogContent" @close="showProtocolDialog = false" @agree="handleProtocolAgree" | |||
| @reject="handleProtocolReject" /> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import PrivacyPopup from '@/wxcomponents/privacy-popup/privacy-popup.vue' | |||
| import ProtocolDialog from '@/wxcomponents/protocol-dialog/protocol-dialog.vue' | |||
| // import {banner} from '@/api.uts' | |||
| export default { | |||
| components: { | |||
| PrivacyPopup, | |||
| ProtocolDialog | |||
| }, | |||
| data() { | |||
| return { | |||
| agreed: false, | |||
| showProtocolDialog: false, | |||
| protocolDialogTitle: '', | |||
| protocolDialogContent: '', | |||
| configData: [], // 存储 getConfig 返回的 result | |||
| needPhone: false // 控制是否需要手机号授权 | |||
| } | |||
| }, | |||
| computed: { | |||
| logoImage() { | |||
| const item = this.configData.find(i => i.keyName === 'logo_image') | |||
| return item ? item.keyContent : '' | |||
| }, | |||
| logoName() { | |||
| const item = this.configData.find(i => i.keyName === 'logo_name') | |||
| return item ? item.keyContent : '' | |||
| } | |||
| }, | |||
| onLoad() { | |||
| this.getConfigData() | |||
| }, | |||
| methods: { | |||
| getConfigData() { | |||
| this.$api('getConfig', {}, res => { | |||
| // console.log('Config data response:', JSON.parse(JSON.stringify(res)) ) | |||
| if (res && res.success && Array.isArray(res.result)) { | |||
| this.configData = res.result | |||
| // console.log('Config data set:', JSON.parse(JSON.stringify(this.configData)) ) | |||
| } | |||
| }) | |||
| }, | |||
| getConfigByKey(key) { | |||
| const item = this.configData.find(i => i.keyName === key) | |||
| return item ? item.keyContent : '' | |||
| }, | |||
| handleLogin() { | |||
| if (!this.agreed) { | |||
| uni.showToast({ | |||
| title: '请阅读并勾选服务协议和隐私声明', | |||
| icon: 'none' | |||
| }); | |||
| return; | |||
| } | |||
| this.$refs.privacyPopup.open() | |||
| // uni.showLoading({ | |||
| // title: '登录中...' | |||
| // }); | |||
| // setTimeout(() => { | |||
| // uni.hideLoading(); | |||
| // uni.showToast({ | |||
| // title: '登录成功' | |||
| // }); | |||
| // uni.reLaunch({ | |||
| // url: '/pages/index/index' | |||
| // }); | |||
| // }, 1500); | |||
| }, | |||
| handleCancel() { | |||
| uni.navigateBack(); | |||
| }, | |||
| handleAgreementChange(e) { | |||
| // console.log(this.agreed); | |||
| // this.agreed = e.detail.value.length > 0; | |||
| if(this.agreed){ | |||
| this.agreed = false; | |||
| }else{ | |||
| this.agreed = true; | |||
| } | |||
| }, | |||
| openProtocol(type) { | |||
| console.log('Opening protocol:', type) | |||
| console.log('Current configData:', this.configData) | |||
| let protocol = null | |||
| if (type === 'privacy') { | |||
| protocol = this.configData.find(i => i.keyName === 'user_ys') | |||
| } else if (type === 'service') { | |||
| protocol = this.configData.find(i => i.keyName === 'user_xy') | |||
| } | |||
| console.log('Found protocol:', protocol) | |||
| this.protocolDialogTitle = protocol ? protocol.keyValue : (type === 'privacy' ? '隐私政策' : '服务协议') | |||
| this.protocolDialogContent = protocol && protocol.keyContent ? protocol.keyContent : | |||
| (type === 'privacy' ? | |||
| '<div style="padding: 20rpx;">暂无隐私政策内容</div>' : | |||
| '<div style="padding: 20rpx;">暂无服务协议内容</div>') | |||
| this.showProtocolDialog = true | |||
| console.log('Dialog state:', { | |||
| title: this.protocolDialogTitle, | |||
| content: this.protocolDialogContent, | |||
| show: this.showProtocolDialog | |||
| }) | |||
| }, | |||
| goToAdminLogin() { | |||
| uni.navigateTo({ | |||
| url: '/pages/component/admin_login' | |||
| }); | |||
| }, | |||
| // 同意隐私政策 | |||
| handleAgreePrivacy() { | |||
| uni.showLoading({ | |||
| title: '登录中...' | |||
| }) | |||
| // 执行登录逻辑... | |||
| let self = this | |||
| wx.login({ | |||
| success (res) { | |||
| // console.log(res.code,'code') | |||
| if (res.code) { | |||
| self.$api('wxLogin', {code : res.code}, res => { | |||
| console.log(res,'login') | |||
| if (res.code == 200) { | |||
| uni.hideLoading(); | |||
| // console.log(res) | |||
| uni.setStorageSync('token',res.result.token); | |||
| uni.setStorageSync('openid',res.result.userInfo && res.result.userInfo.appletOpenid); | |||
| getApp().globalData.login_status = true; | |||
| if (res.result.userInfo) { | |||
| const userInfo = res.result.userInfo; | |||
| console.log(userInfo,'userInfo') | |||
| if (!userInfo.headImage || !userInfo.nickName) { | |||
| import PrivacyPopup from '@/wxcomponents/privacy-popup/privacy-popup.vue' | |||
| import ProtocolDialog from '@/wxcomponents/protocol-dialog/protocol-dialog.vue' | |||
| // import {banner} from '@/api.uts' | |||
| export default { | |||
| components: { | |||
| PrivacyPopup, | |||
| ProtocolDialog | |||
| }, | |||
| data() { | |||
| return { | |||
| agreed: false, | |||
| showProtocolDialog: false, | |||
| protocolDialogTitle: '', | |||
| protocolDialogContent: '', | |||
| configData: [], // 存储 getConfig 返回的 result | |||
| needPhone: false // 控制是否需要手机号授权 | |||
| } | |||
| }, | |||
| computed: { | |||
| logoImage() { | |||
| const item = this.configData.find(i => i.keyName === 'logo_image') | |||
| return item ? item.keyContent : '' | |||
| }, | |||
| logoName() { | |||
| const item = this.configData.find(i => i.keyName === 'logo_name') | |||
| return item ? item.keyContent : '' | |||
| } | |||
| }, | |||
| onLoad() { | |||
| this.getConfigData() | |||
| }, | |||
| methods: { | |||
| getConfigData() { | |||
| this.$api('getConfig', {}, res => { | |||
| // console.log('Config data response:', JSON.parse(JSON.stringify(res)) ) | |||
| if (res && res.success && Array.isArray(res.result)) { | |||
| this.configData = res.result | |||
| // console.log('Config data set:', JSON.parse(JSON.stringify(this.configData)) ) | |||
| } | |||
| }) | |||
| }, | |||
| getConfigByKey(key) { | |||
| const item = this.configData.find(i => i.keyName === key) | |||
| return item ? item.keyContent : '' | |||
| }, | |||
| handleLogin() { | |||
| if (!this.agreed) { | |||
| uni.showToast({ | |||
| title: '请阅读并勾选服务协议和隐私声明', | |||
| icon: 'none' | |||
| }); | |||
| return; | |||
| } | |||
| this.$refs.privacyPopup.open() | |||
| // uni.showLoading({ | |||
| // title: '登录中...' | |||
| // }); | |||
| // setTimeout(() => { | |||
| // uni.hideLoading(); | |||
| // uni.showToast({ | |||
| // title: '登录成功' | |||
| // }); | |||
| // uni.reLaunch({ | |||
| // url: '/pages/index/index' | |||
| // }); | |||
| // }, 1500); | |||
| }, | |||
| handleCancel() { | |||
| uni.reLaunch({ | |||
| url: '/pages/component/home' | |||
| }); | |||
| }, | |||
| handleAgreementChange(e) { | |||
| // console.log(this.agreed); | |||
| // this.agreed = e.detail.value.length > 0; | |||
| if (this.agreed) { | |||
| this.agreed = false; | |||
| } else { | |||
| this.agreed = true; | |||
| } | |||
| }, | |||
| openProtocol(type) { | |||
| console.log('Opening protocol:', type) | |||
| console.log('Current configData:', this.configData) | |||
| let protocol = null | |||
| if (type === 'privacy') { | |||
| protocol = this.configData.find(i => i.keyName === 'user_ys') | |||
| } else if (type === 'service') { | |||
| protocol = this.configData.find(i => i.keyName === 'user_xy') | |||
| } | |||
| console.log('Found protocol:', protocol) | |||
| this.protocolDialogTitle = protocol ? protocol.keyValue : (type === 'privacy' ? '隐私政策' : '服务协议') | |||
| this.protocolDialogContent = protocol && protocol.keyContent ? protocol.keyContent : | |||
| (type === 'privacy' ? | |||
| '<div style="padding: 20rpx;">暂无隐私政策内容</div>' : | |||
| '<div style="padding: 20rpx;">暂无服务协议内容</div>') | |||
| this.showProtocolDialog = true | |||
| console.log('Dialog state:', { | |||
| title: this.protocolDialogTitle, | |||
| content: this.protocolDialogContent, | |||
| show: this.showProtocolDialog | |||
| }) | |||
| }, | |||
| goToAdminLogin() { | |||
| uni.navigateTo({ | |||
| url: '/pages/component/admin_login' | |||
| }); | |||
| }, | |||
| // 同意隐私政策 | |||
| handleAgreePrivacy() { | |||
| uni.showLoading({ | |||
| title: '登录中...' | |||
| }) | |||
| // 执行登录逻辑... | |||
| let self = this | |||
| wx.login({ | |||
| success(res) { | |||
| // console.log(res.code,'code') | |||
| if (res.code) { | |||
| self.$api('wxLogin', { | |||
| code: res.code | |||
| }, res => { | |||
| console.log(res, 'login') | |||
| if (res.code == 200) { | |||
| uni.hideLoading(); | |||
| // console.log(res) | |||
| uni.setStorageSync('token', res.result.token); | |||
| uni.setStorageSync('openid', res.result.userInfo && res.result.userInfo | |||
| .appletOpenid); | |||
| getApp().globalData.login_status = true; | |||
| if (res.result.userInfo) { | |||
| const userInfo = res.result.userInfo; | |||
| console.log(userInfo, 'userInfo') | |||
| if (!userInfo.headImage || !userInfo.nickName) { | |||
| uni.navigateTo({ | |||
| url: '/pages/wxUserInfo' | |||
| }); | |||
| } else { | |||
| uni.reLaunch({ | |||
| url: '/pages/component/home' | |||
| }); | |||
| } | |||
| } else { | |||
| uni.navigateTo({ | |||
| url: '/pages/wxUserInfo' | |||
| }); | |||
| } else { | |||
| uni.reLaunch({ | |||
| url: '/pages/component/home' | |||
| }); | |||
| } | |||
| } else { | |||
| uni.navigateTo({ | |||
| url: '/pages/wxUserInfo' | |||
| }); | |||
| } | |||
| } | |||
| }) | |||
| } else { | |||
| uni.hideLoading(); | |||
| console.log('登录失败!' + res.errMsg) | |||
| } | |||
| } | |||
| }) | |||
| }, | |||
| // 拒绝隐私政策 | |||
| handleRejectPrivacy() { | |||
| uni.reLaunch({ url: '/pages/component/home' }); | |||
| }, | |||
| // 打开协议页面 | |||
| openProtocolPage(type) { | |||
| this.openProtocol(type) | |||
| }, | |||
| handleProtocolAgree() { | |||
| this.showProtocolDialog = false | |||
| this.agreed = true | |||
| // 你可以在这里添加同意后的其他逻辑 | |||
| }, | |||
| handleProtocolReject() { | |||
| this.showProtocolDialog = false | |||
| if (this.agreed) { | |||
| this.agreed = false | |||
| } | |||
| // 你可以在这里添加拒绝后的其他逻辑 | |||
| } | |||
| } | |||
| } | |||
| }) | |||
| } else { | |||
| uni.hideLoading(); | |||
| console.log('登录失败!' + res.errMsg) | |||
| } | |||
| } | |||
| }) | |||
| }, | |||
| // 拒绝隐私政策 | |||
| handleRejectPrivacy() { | |||
| uni.reLaunch({ | |||
| url: '/pages/component/home' | |||
| }); | |||
| }, | |||
| // 打开协议页面 | |||
| openProtocolPage(type) { | |||
| this.openProtocol(type) | |||
| }, | |||
| handleProtocolAgree() { | |||
| this.showProtocolDialog = false | |||
| this.agreed = true | |||
| // 你可以在这里添加同意后的其他逻辑 | |||
| }, | |||
| handleProtocolReject() { | |||
| this.showProtocolDialog = false | |||
| if (this.agreed) { | |||
| this.agreed = false | |||
| } | |||
| // 你可以在这里添加拒绝后的其他逻辑 | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style scoped scss> | |||
| view { | |||
| padding-bottom: 0 !important; | |||
| } | |||
| view { | |||
| padding-bottom: 0 !important; | |||
| } | |||
| .login-container { | |||
| display: flex; | |||
| flex-direction: column; | |||
| height: 100vh; | |||
| background-color: #f9ece5; | |||
| padding: 0 40rpx; | |||
| padding-bottom: env(safe-area-inset-bottom); | |||
| } | |||
| .app-header { | |||
| margin-top: 220rpx; | |||
| margin-bottom: 180rpx; | |||
| text-align: center; | |||
| height: 30%; | |||
| } | |||
| image{ | |||
| width: 50%; | |||
| height: 50%; | |||
| } | |||
| .app-title { | |||
| display: flex; | |||
| justify-content: center; | |||
| align-items: center; | |||
| font-family: Alimama ShuHeiTi; | |||
| font-weight: 700; | |||
| font-size: 28px; | |||
| line-height: 140%; | |||
| letter-spacing: 0%; | |||
| /* background-color: rgba(254, 208, 116, 0.8); */ | |||
| border-radius: 3em; | |||
| letter-spacing: 0.2em; | |||
| /* text-shadow: 1px 2px #f7b737; */ | |||
| } | |||
| .app-subtitle { | |||
| font-size: 28rpx; | |||
| color: #fef6e3; | |||
| margin-top: 1rem; | |||
| letter-spacing: 0.11em; | |||
| } | |||
| .login-actions { | |||
| /* height: 30%; */ | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| justify-content: center; | |||
| background-color: #f9ece5; | |||
| border-radius: 30rpx; | |||
| } | |||
| .login-btn, .cancel-btn { | |||
| width: 82%; | |||
| height: 90rpx; | |||
| line-height: 90rpx; | |||
| border-radius: 45rpx; | |||
| font-size: 32rpx; | |||
| margin-bottom: 20rpx; | |||
| border: none; | |||
| } | |||
| .login-btn { | |||
| background-color: #f79400; | |||
| color: white; | |||
| } | |||
| .cancel-btn { | |||
| background-color: rgba(255, 253, 249); | |||
| color: #f7990c; | |||
| border: 1px solid rgba(249, 178, 71); | |||
| } | |||
| .agreement { | |||
| /* margin-top: 40rpx; */ | |||
| font-size: 24rpx; | |||
| color: #333; | |||
| display: flex; | |||
| justify-content: center; | |||
| } | |||
| .login-container { | |||
| display: flex; | |||
| flex-direction: column; | |||
| height: 100vh; | |||
| background-color: #f9ece5; | |||
| padding: 0 40rpx; | |||
| padding-bottom: env(safe-area-inset-bottom); | |||
| } | |||
| .radio-group { | |||
| display: flex; | |||
| align-items: center; | |||
| } | |||
| /* 自定义圆形checkbox样式 */ | |||
| .custom-radio { | |||
| /* border-radius: 50%; */ | |||
| width: 32rpx; | |||
| height: 32rpx; | |||
| transform: scale(0.7); | |||
| margin-right: 8rpx; | |||
| } | |||
| /* 覆盖uniapp默认checkbox样式 */ | |||
| .custom-radio .wx-checkbox-input, | |||
| .custom-radio .uni-checkbox-input { | |||
| border-radius: 50% !important; | |||
| width: 32rpx !important; | |||
| height: 32rpx !important; | |||
| } | |||
| .custom-radio .wx-checkbox-input.wx-checkbox-input-checked, | |||
| .custom-radio .uni-checkbox-input.uni-checkbox-input-checked { | |||
| background-color: #07C160 !important; | |||
| border-color: #07C160 !important; | |||
| color: #ffffff !important; | |||
| } | |||
| /* .radio-label { | |||
| .app-header { | |||
| margin-top: 320rpx; | |||
| margin-bottom: 180rpx; | |||
| text-align: center; | |||
| } | |||
| .app-title { | |||
| display: flex; | |||
| justify-content: center; | |||
| align-items: center; | |||
| font-family: Alimama ShuHeiTi; | |||
| font-weight: 700; | |||
| font-size: 28px; | |||
| /* background-color: rgba(254, 208, 116, 0.8); */ | |||
| /* text-shadow: 1px 2px #f7b737; */ | |||
| margin-top: 50rpx; | |||
| } | |||
| .app-subtitle { | |||
| font-size: 28rpx; | |||
| color: #fef6e3; | |||
| margin-top: 1rem; | |||
| letter-spacing: 0.11em; | |||
| } | |||
| .login-actions { | |||
| /* height: 30%; */ | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| justify-content: center; | |||
| background-color: #f9ece5; | |||
| border-radius: 30rpx; | |||
| } | |||
| .login-btn, | |||
| .cancel-btn { | |||
| width: 82%; | |||
| height: 90rpx; | |||
| line-height: 90rpx; | |||
| border-radius: 45rpx; | |||
| font-size: 32rpx; | |||
| margin-bottom: 20rpx; | |||
| border: none; | |||
| } | |||
| .login-btn { | |||
| background-color: #f79400; | |||
| color: white; | |||
| } | |||
| .cancel-btn { | |||
| background-color: rgba(255, 253, 249); | |||
| color: #f7990c; | |||
| border: 1px solid rgba(249, 178, 71); | |||
| } | |||
| .agreement { | |||
| /* margin-top: 40rpx; */ | |||
| font-size: 24rpx; | |||
| color: #333; | |||
| display: flex; | |||
| justify-content: center; | |||
| } | |||
| .radio-group { | |||
| display: flex; | |||
| align-items: center; | |||
| } | |||
| /* 自定义圆形checkbox样式 */ | |||
| .custom-radio { | |||
| /* border-radius: 50%; */ | |||
| width: 32rpx; | |||
| height: 32rpx; | |||
| transform: scale(0.7); | |||
| margin-right: 8rpx; | |||
| } | |||
| /* 覆盖uniapp默认checkbox样式 */ | |||
| .custom-radio .wx-checkbox-input, | |||
| .custom-radio .uni-checkbox-input { | |||
| border-radius: 50% !important; | |||
| width: 32rpx !important; | |||
| height: 32rpx !important; | |||
| } | |||
| .custom-radio .wx-checkbox-input.wx-checkbox-input-checked, | |||
| .custom-radio .uni-checkbox-input.uni-checkbox-input-checked { | |||
| background-color: #07C160 !important; | |||
| border-color: #07C160 !important; | |||
| color: #ffffff !important; | |||
| } | |||
| /* .radio-label { | |||
| display: flex; | |||
| align-items: center; | |||
| } */ | |||
| .agreement-text { | |||
| margin: 0 4rpx; | |||
| } | |||
| .protocol-link { | |||
| color: #fabe65; | |||
| } | |||
| .agreement-text { | |||
| margin: 0 4rpx; | |||
| } | |||
| .admin-login { | |||
| text-align: center; | |||
| color: #fffffe; | |||
| font-size: 28rpx; | |||
| letter-spacing: 0.15em; | |||
| } | |||
| .protocol-link { | |||
| color: #fabe65; | |||
| } | |||
| .admin-login { | |||
| text-align: center; | |||
| color: #fffffe; | |||
| font-size: 28rpx; | |||
| letter-spacing: 0.15em; | |||
| } | |||
| </style> | |||
| @ -0,0 +1,48 @@ | |||
| <template> | |||
| <view class="page"> | |||
| <!-- 回收去向 --> | |||
| <navbar :title="title" leftClick | |||
| @leftClick="$utils.navigateBack" /> | |||
| <view class="content"> | |||
| <uv-parse :content="detail.content"></uv-parse> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import navbar from '@/compoent/base/navbar.vue' | |||
| export default { | |||
| components : { | |||
| navbar | |||
| }, | |||
| onLoad({id}) { | |||
| this.id = id | |||
| this.getDetail() | |||
| }, | |||
| data() { | |||
| return { | |||
| id : 0, | |||
| detail : {}, | |||
| title : '回收去向', | |||
| } | |||
| }, | |||
| methods: { | |||
| getDetail(){ | |||
| this.$api('getRecyclingDestinationDetail', { | |||
| id : this.id | |||
| }).then(res => { | |||
| this.detail = res.result | |||
| this.title = res.result.title | |||
| }) | |||
| }, | |||
| } | |||
| } | |||
| </script> | |||
| <style scoped lang="scss"> | |||
| .content{ | |||
| padding: 20rpx; | |||
| } | |||
| </style> | |||
| @ -0,0 +1,48 @@ | |||
| <template> | |||
| <view class="page"> | |||
| <!-- 回收去向 --> | |||
| <navbar :title="title" leftClick | |||
| @leftClick="$utils.navigateBack" /> | |||
| <view class="content"> | |||
| <uv-parse :content="detail.content"></uv-parse> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import navbar from '@/compoent/base/navbar.vue' | |||
| export default { | |||
| components : { | |||
| navbar | |||
| }, | |||
| onLoad({id}) { | |||
| this.id = id | |||
| this.getDetail() | |||
| }, | |||
| data() { | |||
| return { | |||
| id : 0, | |||
| detail : {}, | |||
| title : '回收去向', | |||
| } | |||
| }, | |||
| methods: { | |||
| getDetail(){ | |||
| this.$api('getRecyclingDestinationDetail', { | |||
| id : this.id | |||
| }).then(res => { | |||
| this.detail = res.result | |||
| this.title = res.result.title | |||
| }) | |||
| }, | |||
| } | |||
| } | |||
| </script> | |||
| <style scoped lang="scss"> | |||
| .content{ | |||
| padding: 20rpx; | |||
| } | |||
| </style> | |||
| @ -0,0 +1,13 @@ | |||
| ## 1.0.4(2023-07-17) | |||
| 1. 优化文档 | |||
| 2. 优化其他 | |||
| ## 1.0.3(2023-06-19) | |||
| 1. 修复nvue模式下不显示的BUG | |||
| ## 1.0.2(2023-06-02) | |||
| 1. 修复可能存在的BUG | |||
| 2. 优化 | |||
| ## 1.0.1(2023-05-16) | |||
| 1. 优化组件依赖,修改后无需全局引入,组件导入即可使用 | |||
| 2. 优化部分功能 | |||
| ## 1.0.0(2023-05-10) | |||
| uv-parse 富文本解析器 | |||
| @ -0,0 +1,576 @@ | |||
| <template> | |||
| <view :id="attrs.id" :class="'_block _'+name+' '+attrs.class" :style="attrs.style"> | |||
| <block v-for="(n, i) in childs" v-bind:key="i"> | |||
| <!-- 图片 --> | |||
| <!-- 占位图 --> | |||
| <image v-if="n.name==='img'&&!n.t&&((opts[1]&&!ctrl[i])||ctrl[i]<0)" class="_img" :style="n.attrs.style" :src="ctrl[i]<0?opts[2]:opts[1]" mode="widthFix" /> | |||
| <!-- 显示图片 --> | |||
| <!-- #ifdef H5 || (APP-PLUS && VUE2) --> | |||
| <img v-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+n.attrs.style" :src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" /> | |||
| <!-- #endif --> | |||
| <!-- #ifndef H5 || (APP-PLUS && VUE2) --> | |||
| <!-- 表格中的图片,使用 rich-text 防止大小不正确 --> | |||
| <rich-text v-if="n.name==='img'&&n.t" :style="'display:'+n.t" :nodes="[{attrs:{style:n.attrs.style,src:n.attrs.src},name:'img'}]" :data-i="i" @tap.stop="imgTap" /> | |||
| <!-- #endif --> | |||
| <!-- #ifndef H5 || APP-PLUS --> | |||
| <image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+'width:'+(ctrl[i]||1)+'px;height:1px;'+n.attrs.style" :src="n.attrs.src" :mode="!n.h?'widthFix':(!n.w?'heightFix':'')" :lazy-load="opts[0]" :webp="n.webp" :show-menu-by-longpress="opts[3]&&!n.attrs.ignore" :image-menu-prevent="!opts[3]||n.attrs.ignore" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" /> | |||
| <!-- #endif --> | |||
| <!-- #ifdef APP-PLUS && VUE3 --> | |||
| <image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+'width:'+(ctrl[i]||1)+'px;'+n.attrs.style" :src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')" :mode="!n.h?'widthFix':(!n.w?'heightFix':'')" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" /> | |||
| <!-- #endif --> | |||
| <!-- 文本 --> | |||
| <!-- #ifdef MP-WEIXIN --> | |||
| <text v-else-if="n.text" :user-select="opts[4]=='force'&&isiOS" decode>{{n.text}}</text> | |||
| <!-- #endif --> | |||
| <!-- #ifndef MP-WEIXIN || MP-BAIDU || MP-ALIPAY || MP-TOUTIAO --> | |||
| <text v-else-if="n.text" decode>{{n.text}}</text> | |||
| <!-- #endif --> | |||
| <text v-else-if="n.name==='br'">\n</text> | |||
| <!-- 链接 --> | |||
| <view v-else-if="n.name==='a'" :id="n.attrs.id" :class="(n.attrs.href?'_a ':'')+n.attrs.class" hover-class="_hover" :style="'display:inline;'+n.attrs.style" :data-i="i" @tap.stop="linkTap"> | |||
| <node name="span" :childs="n.children" :opts="opts" style="display:inherit" /> | |||
| </view> | |||
| <!-- 视频 --> | |||
| <!-- #ifdef APP-PLUS --> | |||
| <view v-else-if="n.html" :id="n.attrs.id" :class="'_video '+n.attrs.class" :style="n.attrs.style" v-html="n.html" @vplay.stop="play" /> | |||
| <!-- #endif --> | |||
| <!-- #ifndef APP-PLUS --> | |||
| <video v-else-if="n.name==='video'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :autoplay="n.attrs.autoplay" :controls="n.attrs.controls" :loop="n.attrs.loop" :muted="n.attrs.muted" :object-fit="n.attrs['object-fit']" :poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i" @play="play" @error="mediaError" /> | |||
| <!-- #endif --> | |||
| <!-- #ifdef H5 || APP-PLUS --> | |||
| <iframe v-else-if="n.name==='iframe'" :style="n.attrs.style" :allowfullscreen="n.attrs.allowfullscreen" :frameborder="n.attrs.frameborder" :src="n.attrs.src" /> | |||
| <embed v-else-if="n.name==='embed'" :style="n.attrs.style" :src="n.attrs.src" /> | |||
| <!-- #endif --> | |||
| <!-- #ifndef MP-TOUTIAO || ((H5 || APP-PLUS) && VUE3) --> | |||
| <!-- 音频 --> | |||
| <audio v-else-if="n.name==='audio'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :author="n.attrs.author" :controls="n.attrs.controls" :loop="n.attrs.loop" :name="n.attrs.name" :poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i" @play="play" @error="mediaError" /> | |||
| <!-- #endif --> | |||
| <view v-else-if="(n.name==='table'&&n.c)||n.name==='li'" :id="n.attrs.id" :class="'_'+n.name+' '+n.attrs.class" :style="n.attrs.style"> | |||
| <node v-if="n.name==='li'" :childs="n.children" :opts="opts" /> | |||
| <view v-else v-for="(tbody, x) in n.children" v-bind:key="x" :class="'_'+tbody.name+' '+tbody.attrs.class" :style="tbody.attrs.style"> | |||
| <node v-if="tbody.name==='td'||tbody.name==='th'" :childs="tbody.children" :opts="opts" /> | |||
| <block v-else v-for="(tr, y) in tbody.children" v-bind:key="y"> | |||
| <view v-if="tr.name==='td'||tr.name==='th'" :class="'_'+tr.name+' '+tr.attrs.class" :style="tr.attrs.style"> | |||
| <node :childs="tr.children" :opts="opts" /> | |||
| </view> | |||
| <view v-else :class="'_'+tr.name+' '+tr.attrs.class" :style="tr.attrs.style"> | |||
| <view v-for="(td, z) in tr.children" v-bind:key="z" :class="'_'+td.name+' '+td.attrs.class" :style="td.attrs.style"> | |||
| <node :childs="td.children" :opts="opts" /> | |||
| </view> | |||
| </view> | |||
| </block> | |||
| </view> | |||
| </view> | |||
| <!-- 富文本 --> | |||
| <!-- #ifdef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) --> | |||
| <rich-text v-else-if="!n.c&&!handler.isInline(n.name, n.attrs.style)" :id="n.attrs.id" :style="n.f" :user-select="opts[4]" :nodes="[n]" /> | |||
| <!-- #endif --> | |||
| <!-- #ifndef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) --> | |||
| <rich-text v-else-if="!n.c" :id="n.attrs.id" :style="'display:inline;'+n.f" :preview="false" :selectable="opts[4]" :user-select="opts[4]" :nodes="[n]" /> | |||
| <!-- #endif --> | |||
| <!-- 继续递归 --> | |||
| <view v-else-if="n.c===2" :id="n.attrs.id" :class="'_block _'+n.name+' '+n.attrs.class" :style="n.f+';'+n.attrs.style"> | |||
| <node v-for="(n2, j) in n.children" v-bind:key="j" :style="n2.f" :name="n2.name" :attrs="n2.attrs" :childs="n2.children" :opts="opts" /> | |||
| </view> | |||
| <node v-else :style="n.f" :name="n.name" :attrs="n.attrs" :childs="n.children" :opts="opts" /> | |||
| </block> | |||
| </view> | |||
| </template> | |||
| <script module="handler" lang="wxs"> | |||
| // 行内标签列表 | |||
| var inlineTags = { | |||
| abbr: true, | |||
| b: true, | |||
| big: true, | |||
| code: true, | |||
| del: true, | |||
| em: true, | |||
| i: true, | |||
| ins: true, | |||
| label: true, | |||
| q: true, | |||
| small: true, | |||
| span: true, | |||
| strong: true, | |||
| sub: true, | |||
| sup: true | |||
| } | |||
| /** | |||
| * @description 判断是否为行内标签 | |||
| */ | |||
| module.exports = { | |||
| isInline: function (tagName, style) { | |||
| return inlineTags[tagName] || (style || '').indexOf('display:inline') !== -1 | |||
| } | |||
| } | |||
| </script> | |||
| <script> | |||
| import node from './node' | |||
| export default { | |||
| name: 'node', | |||
| options: { | |||
| // #ifdef MP-WEIXIN | |||
| virtualHost: true, | |||
| // #endif | |||
| // #ifdef MP-TOUTIAO | |||
| addGlobalClass: false | |||
| // #endif | |||
| }, | |||
| data () { | |||
| return { | |||
| ctrl: {}, | |||
| // #ifdef MP-WEIXIN | |||
| isiOS: uni.getSystemInfoSync().system.includes('iOS') | |||
| // #endif | |||
| } | |||
| }, | |||
| props: { | |||
| name: String, | |||
| attrs: { | |||
| type: Object, | |||
| default () { | |||
| return {} | |||
| } | |||
| }, | |||
| childs: Array, | |||
| opts: Array | |||
| }, | |||
| components: { | |||
| // #ifndef (H5 || APP-PLUS) && VUE3 | |||
| node | |||
| // #endif | |||
| }, | |||
| mounted () { | |||
| this.$nextTick(() => { | |||
| for (this.root = this.$parent; this.root.$options.name !== 'uv-parse'; this.root = this.root.$parent); | |||
| }) | |||
| // #ifdef H5 || APP-PLUS | |||
| if (this.opts[0]) { | |||
| let i | |||
| for (i = this.childs.length; i--;) { | |||
| if (this.childs[i].name === 'img') break | |||
| } | |||
| if (i !== -1) { | |||
| this.observer = uni.createIntersectionObserver(this).relativeToViewport({ | |||
| top: 500, | |||
| bottom: 500 | |||
| }) | |||
| this.observer.observe('._img', res => { | |||
| if (res.intersectionRatio) { | |||
| this.$set(this.ctrl, 'load', 1) | |||
| this.observer.disconnect() | |||
| } | |||
| }) | |||
| } | |||
| } | |||
| // #endif | |||
| }, | |||
| beforeDestroy () { | |||
| // #ifdef H5 || APP-PLUS | |||
| if (this.observer) { | |||
| this.observer.disconnect() | |||
| } | |||
| // #endif | |||
| }, | |||
| methods:{ | |||
| // #ifdef MP-WEIXIN | |||
| toJSON () { return this }, | |||
| // #endif | |||
| /** | |||
| * @description 播放视频事件 | |||
| * @param {Event} e | |||
| */ | |||
| play (e) { | |||
| this.root.$emit('play') | |||
| // #ifndef APP-PLUS | |||
| if (this.root.pauseVideo) { | |||
| let flag = false | |||
| const id = e.target.id | |||
| for (let i = this.root._videos.length; i--;) { | |||
| if (this.root._videos[i].id === id) { | |||
| flag = true | |||
| } else { | |||
| this.root._videos[i].pause() // 自动暂停其他视频 | |||
| } | |||
| } | |||
| // 将自己加入列表 | |||
| if (!flag) { | |||
| const ctx = uni.createVideoContext(id | |||
| // #ifndef MP-BAIDU | |||
| , this | |||
| // #endif | |||
| ) | |||
| ctx.id = id | |||
| if (this.root.playbackRate) { | |||
| ctx.playbackRate(this.root.playbackRate) | |||
| } | |||
| this.root._videos.push(ctx) | |||
| } | |||
| } | |||
| // #endif | |||
| }, | |||
| /** | |||
| * @description 图片点击事件 | |||
| * @param {Event} e | |||
| */ | |||
| imgTap (e) { | |||
| const node = this.childs[e.currentTarget.dataset.i] | |||
| if (node.a) { | |||
| this.linkTap(node.a) | |||
| return | |||
| } | |||
| if (node.attrs.ignore) return | |||
| // #ifdef H5 || APP-PLUS | |||
| node.attrs.src = node.attrs.src || node.attrs['data-src'] | |||
| // #endif | |||
| this.root.$emit('imgtap', node.attrs) | |||
| // 自动预览图片 | |||
| if (this.root.previewImg) { | |||
| uni.previewImage({ | |||
| // #ifdef MP-WEIXIN | |||
| showmenu: this.root.showImgMenu, | |||
| // #endif | |||
| // #ifdef MP-ALIPAY | |||
| enablesavephoto: this.root.showImgMenu, | |||
| enableShowPhotoDownload: this.root.showImgMenu, | |||
| // #endif | |||
| current: parseInt(node.attrs.i), | |||
| urls: this.root.imgList | |||
| }) | |||
| } | |||
| }, | |||
| /** | |||
| * @description 图片长按 | |||
| */ | |||
| imgLongTap (e) { | |||
| // #ifdef APP-PLUS | |||
| const attrs = this.childs[e.currentTarget.dataset.i].attrs | |||
| if (this.opts[3] && !attrs.ignore) { | |||
| uni.showActionSheet({ | |||
| itemList: ['保存图片'], | |||
| success: () => { | |||
| const save = path => { | |||
| uni.saveImageToPhotosAlbum({ | |||
| filePath: path, | |||
| success () { | |||
| uni.showToast({ | |||
| title: '保存成功' | |||
| }) | |||
| } | |||
| }) | |||
| } | |||
| if (this.root.imgList[attrs.i].startsWith('http')) { | |||
| uni.downloadFile({ | |||
| url: this.root.imgList[attrs.i], | |||
| success: res => save(res.tempFilePath) | |||
| }) | |||
| } else { | |||
| save(this.root.imgList[attrs.i]) | |||
| } | |||
| } | |||
| }) | |||
| } | |||
| // #endif | |||
| }, | |||
| /** | |||
| * @description 图片加载完成事件 | |||
| * @param {Event} e | |||
| */ | |||
| imgLoad (e) { | |||
| const i = e.currentTarget.dataset.i | |||
| /* #ifndef H5 || (APP-PLUS && VUE2) */ | |||
| if (!this.childs[i].w) { | |||
| // 设置原宽度 | |||
| this.$set(this.ctrl, i, e.detail.width) | |||
| } else /* #endif */ if ((this.opts[1] && !this.ctrl[i]) || this.ctrl[i] === -1) { | |||
| // 加载完毕,取消加载中占位图 | |||
| this.$set(this.ctrl, i, 1) | |||
| } | |||
| this.checkReady() | |||
| }, | |||
| /** | |||
| * @description 检查是否所有图片加载完毕 | |||
| */ | |||
| checkReady () { | |||
| if (this.root && !this.root.lazyLoad) { | |||
| this.root._unloadimgs -= 1 | |||
| if (!this.root._unloadimgs) { | |||
| setTimeout(() => { | |||
| this.root.getRect().then(rect => { | |||
| this.root.$emit('ready', rect) | |||
| }).catch(() => { | |||
| this.root.$emit('ready', {}) | |||
| }) | |||
| }, 350) | |||
| } | |||
| } | |||
| }, | |||
| /** | |||
| * @description 链接点击事件 | |||
| * @param {Event} e | |||
| */ | |||
| linkTap (e) { | |||
| const node = e.currentTarget ? this.childs[e.currentTarget.dataset.i] : {} | |||
| const attrs = node.attrs || e | |||
| const href = attrs.href | |||
| this.root.$emit('linktap', Object.assign({ | |||
| innerText: this.root.getText(node.children || []) // 链接内的文本内容 | |||
| }, attrs)) | |||
| if (href) { | |||
| if (href[0] === '#') { | |||
| // 跳转锚点 | |||
| this.root.navigateTo(href.substring(1)).catch(() => { }) | |||
| } else if (href.split('?')[0].includes('://')) { | |||
| // 复制外部链接 | |||
| if (this.root.copyLink) { | |||
| // #ifdef H5 | |||
| window.open(href) | |||
| // #endif | |||
| // #ifdef MP | |||
| uni.setClipboardData({ | |||
| data: href, | |||
| success: () => | |||
| uni.showToast({ | |||
| title: '链接已复制' | |||
| }) | |||
| }) | |||
| // #endif | |||
| // #ifdef APP-PLUS | |||
| plus.runtime.openWeb(href) | |||
| // #endif | |||
| } | |||
| } else { | |||
| // 跳转页面 | |||
| uni.navigateTo({ | |||
| url: href, | |||
| fail () { | |||
| uni.switchTab({ | |||
| url: href, | |||
| fail () { } | |||
| }) | |||
| } | |||
| }) | |||
| } | |||
| } | |||
| }, | |||
| /** | |||
| * @description 错误事件 | |||
| * @param {Event} e | |||
| */ | |||
| mediaError (e) { | |||
| const i = e.currentTarget.dataset.i | |||
| const node = this.childs[i] | |||
| // 加载其他源 | |||
| if (node.name === 'video' || node.name === 'audio') { | |||
| let index = (this.ctrl[i] || 0) + 1 | |||
| if (index > node.src.length) { | |||
| index = 0 | |||
| } | |||
| if (index < node.src.length) { | |||
| this.$set(this.ctrl, i, index) | |||
| return | |||
| } | |||
| } else if (node.name === 'img') { | |||
| // #ifdef H5 && VUE3 | |||
| if (this.opts[0] && !this.ctrl.load) return | |||
| // #endif | |||
| // 显示错误占位图 | |||
| if (this.opts[2]) { | |||
| this.$set(this.ctrl, i, -1) | |||
| } | |||
| this.checkReady() | |||
| } | |||
| if (this.root) { | |||
| this.root.$emit('error', { | |||
| source: node.name, | |||
| attrs: node.attrs, | |||
| // #ifndef H5 && VUE3 | |||
| errMsg: e.detail.errMsg | |||
| // #endif | |||
| }) | |||
| } | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style> | |||
| /* a 标签默认效果 */ | |||
| ._a { | |||
| padding: 1.5px 0 1.5px 0; | |||
| color: #366092; | |||
| word-break: break-all; | |||
| } | |||
| /* a 标签点击态效果 */ | |||
| ._hover { | |||
| text-decoration: underline; | |||
| opacity: 0.7; | |||
| } | |||
| /* 图片默认效果 */ | |||
| ._img { | |||
| max-width: 100%; | |||
| -webkit-touch-callout: none; | |||
| } | |||
| /* 内部样式 */ | |||
| ._block { | |||
| display: block; | |||
| } | |||
| ._b, | |||
| ._strong { | |||
| font-weight: bold; | |||
| } | |||
| ._code { | |||
| font-family: monospace; | |||
| } | |||
| ._del { | |||
| text-decoration: line-through; | |||
| } | |||
| ._em, | |||
| ._i { | |||
| font-style: italic; | |||
| } | |||
| ._h1 { | |||
| font-size: 2em; | |||
| } | |||
| ._h2 { | |||
| font-size: 1.5em; | |||
| } | |||
| ._h3 { | |||
| font-size: 1.17em; | |||
| } | |||
| ._h5 { | |||
| font-size: 0.83em; | |||
| } | |||
| ._h6 { | |||
| font-size: 0.67em; | |||
| } | |||
| ._h1, | |||
| ._h2, | |||
| ._h3, | |||
| ._h4, | |||
| ._h5, | |||
| ._h6 { | |||
| display: block; | |||
| font-weight: bold; | |||
| } | |||
| ._image { | |||
| height: 1px; | |||
| } | |||
| ._ins { | |||
| text-decoration: underline; | |||
| } | |||
| ._li { | |||
| display: list-item; | |||
| } | |||
| ._ol { | |||
| list-style-type: decimal; | |||
| } | |||
| ._ol, | |||
| ._ul { | |||
| display: block; | |||
| padding-left: 40px; | |||
| margin: 1em 0; | |||
| } | |||
| ._q::before { | |||
| content: '"'; | |||
| } | |||
| ._q::after { | |||
| content: '"'; | |||
| } | |||
| ._sub { | |||
| font-size: smaller; | |||
| vertical-align: sub; | |||
| } | |||
| ._sup { | |||
| font-size: smaller; | |||
| vertical-align: super; | |||
| } | |||
| ._thead, | |||
| ._tbody, | |||
| ._tfoot { | |||
| display: table-row-group; | |||
| } | |||
| ._tr { | |||
| display: table-row; | |||
| } | |||
| ._td, | |||
| ._th { | |||
| display: table-cell; | |||
| vertical-align: middle; | |||
| } | |||
| ._th { | |||
| font-weight: bold; | |||
| text-align: center; | |||
| } | |||
| ._ul { | |||
| list-style-type: disc; | |||
| } | |||
| ._ul ._ul { | |||
| margin: 0; | |||
| list-style-type: circle; | |||
| } | |||
| ._ul ._ul ._ul { | |||
| list-style-type: square; | |||
| } | |||
| ._abbr, | |||
| ._b, | |||
| ._code, | |||
| ._del, | |||
| ._em, | |||
| ._i, | |||
| ._ins, | |||
| ._label, | |||
| ._q, | |||
| ._span, | |||
| ._strong, | |||
| ._sub, | |||
| ._sup { | |||
| display: inline; | |||
| } | |||
| /* #ifdef APP-PLUS */ | |||
| ._video { | |||
| width: 300px; | |||
| height: 225px; | |||
| } | |||
| /* #endif */ | |||
| </style> | |||
| @ -0,0 +1,498 @@ | |||
| <template> | |||
| <view id="_root" :class="(selectable?'_select ':'')+'_root'" :style="containerStyle"> | |||
| <slot v-if="!nodes[0]" /> | |||
| <!-- #ifndef APP-PLUS-NVUE --> | |||
| <node v-else :childs="nodes" :opts="[lazyLoad,loadingImg,errorImg,showImgMenu,selectable]" name="span" /> | |||
| <!-- #endif --> | |||
| <!-- #ifdef APP-PLUS-NVUE --> | |||
| <web-view ref="web" src="/uni_modules/uv-parse/static/app-plus/uv-parse/local.html" :style="'margin-top:-2px;height:' + height + 'px'" @onPostMessage="_onMessage" /> | |||
| <!-- #endif --> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| /** | |||
| * uv-parse v1.0.3 | |||
| * @description 富文本组件 | |||
| * @tutorial https://www.uvui.cn/components/parse.html | |||
| * @property {String} container-style 容器的样式 | |||
| * @property {String} content 用于渲染的 html 字符串 | |||
| * @property {Boolean} copy-link 是否允许外部链接被点击时自动复制 | |||
| * @property {String} domain 主域名,用于拼接链接 | |||
| * @property {String} error-img 图片出错时的占位图链接 | |||
| * @property {Boolean} lazy-load 是否开启图片懒加载 | |||
| * @property {string} loading-img 图片加载过程中的占位图链接 | |||
| * @property {Boolean} pause-video 是否在播放一个视频时自动暂停其他视频 | |||
| * @property {Boolean} preview-img 是否允许图片被点击时自动预览 | |||
| * @property {Boolean} scroll-table 是否给每个表格添加一个滚动层使其能单独横向滚动 | |||
| * @property {Boolean | String} selectable 是否开启长按复制 | |||
| * @property {Boolean} set-title 是否将 title 标签的内容设置到页面标题 | |||
| * @property {Boolean} show-img-menu 是否允许图片被长按时显示菜单 | |||
| * @property {Object} tag-style 标签的默认样式 | |||
| * @property {Boolean | Number} use-anchor 是否使用锚点链接 | |||
| * @event {Function} load dom 结构加载完毕时触发 | |||
| * @event {Function} ready 所有图片加载完毕时触发 | |||
| * @event {Function} imgtap 图片被点击时触发 | |||
| * @event {Function} linktap 链接被点击时触发 | |||
| * @event {Function} play 音视频播放时触发 | |||
| * @event {Function} error 媒体加载出错时触发 | |||
| */ | |||
| // #ifndef APP-PLUS-NVUE | |||
| import node from './node/node' | |||
| // #endif | |||
| import Parser from './parser' | |||
| const plugins=[] | |||
| // #ifdef APP-PLUS-NVUE | |||
| const dom = weex.requireModule('dom') | |||
| // #endif | |||
| export default { | |||
| name: 'uv-parse', | |||
| data () { | |||
| return { | |||
| nodes: [], | |||
| // #ifdef APP-PLUS-NVUE | |||
| height: 3 | |||
| // #endif | |||
| } | |||
| }, | |||
| props: { | |||
| containerStyle: { | |||
| type: String, | |||
| default: '' | |||
| }, | |||
| content: { | |||
| type: String, | |||
| default: '' | |||
| }, | |||
| copyLink: { | |||
| type: [Boolean, String], | |||
| default: true | |||
| }, | |||
| domain: String, | |||
| errorImg: { | |||
| type: String, | |||
| default: '' | |||
| }, | |||
| lazyLoad: { | |||
| type: [Boolean, String], | |||
| default: false | |||
| }, | |||
| loadingImg: { | |||
| type: String, | |||
| default: '' | |||
| }, | |||
| pauseVideo: { | |||
| type: [Boolean, String], | |||
| default: true | |||
| }, | |||
| previewImg: { | |||
| type: [Boolean, String], | |||
| default: true | |||
| }, | |||
| scrollTable: [Boolean, String], | |||
| selectable: [Boolean, String], | |||
| setTitle: { | |||
| type: [Boolean, String], | |||
| default: true | |||
| }, | |||
| showImgMenu: { | |||
| type: [Boolean, String], | |||
| default: true | |||
| }, | |||
| tagStyle: Object, | |||
| useAnchor: [Boolean, Number] | |||
| }, | |||
| // #ifdef VUE3 | |||
| emits: ['load', 'ready', 'imgtap', 'linktap', 'play', 'error'], | |||
| // #endif | |||
| // #ifndef APP-PLUS-NVUE | |||
| components: { | |||
| node | |||
| }, | |||
| // #endif | |||
| watch: { | |||
| content (content) { | |||
| this.setContent(content) | |||
| } | |||
| }, | |||
| created () { | |||
| this.plugins = [] | |||
| for (let i = plugins.length; i--;) { | |||
| this.plugins.push(new plugins[i](this)) | |||
| } | |||
| }, | |||
| mounted () { | |||
| if (this.content && !this.nodes.length) { | |||
| this.setContent(this.content) | |||
| } | |||
| }, | |||
| beforeDestroy () { | |||
| this._hook('onDetached') | |||
| }, | |||
| methods: { | |||
| /** | |||
| * @description 将锚点跳转的范围限定在一个 scroll-view 内 | |||
| * @param {Object} page scroll-view 所在页面的示例 | |||
| * @param {String} selector scroll-view 的选择器 | |||
| * @param {String} scrollTop scroll-view scroll-top 属性绑定的变量名 | |||
| */ | |||
| in (page, selector, scrollTop) { | |||
| // #ifndef APP-PLUS-NVUE | |||
| if (page && selector && scrollTop) { | |||
| this._in = { | |||
| page, | |||
| selector, | |||
| scrollTop | |||
| } | |||
| } | |||
| // #endif | |||
| }, | |||
| /** | |||
| * @description 锚点跳转 | |||
| * @param {String} id 要跳转的锚点 id | |||
| * @param {Number} offset 跳转位置的偏移量 | |||
| * @returns {Promise} | |||
| */ | |||
| navigateTo (id, offset) { | |||
| return new Promise((resolve, reject) => { | |||
| if (!this.useAnchor) { | |||
| reject(Error('Anchor is disabled')) | |||
| return | |||
| } | |||
| offset = offset || parseInt(this.useAnchor) || 0 | |||
| // #ifdef APP-PLUS-NVUE | |||
| if (!id) { | |||
| dom.scrollToElement(this.$refs.web, { | |||
| offset | |||
| }) | |||
| resolve() | |||
| } else { | |||
| this._navigateTo = { | |||
| resolve, | |||
| reject, | |||
| offset | |||
| } | |||
| this.$refs.web.evalJs('uni.postMessage({data:{action:"getOffset",offset:(document.getElementById(' + id + ')||{}).offsetTop}})') | |||
| } | |||
| // #endif | |||
| // #ifndef APP-PLUS-NVUE | |||
| let deep = ' ' | |||
| // #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO | |||
| deep = '>>>' | |||
| // #endif | |||
| const selector = uni.createSelectorQuery() | |||
| // #ifndef MP-ALIPAY | |||
| .in(this._in ? this._in.page : this) | |||
| // #endif | |||
| .select((this._in ? this._in.selector : '._root') + (id ? `${deep}#${id}` : '')).boundingClientRect() | |||
| if (this._in) { | |||
| selector.select(this._in.selector).scrollOffset() | |||
| .select(this._in.selector).boundingClientRect() | |||
| } else { | |||
| // 获取 scroll-view 的位置和滚动距离 | |||
| selector.selectViewport().scrollOffset() // 获取窗口的滚动距离 | |||
| } | |||
| selector.exec(res => { | |||
| if (!res[0]) { | |||
| reject(Error('Label not found')) | |||
| return | |||
| } | |||
| const scrollTop = res[1].scrollTop + res[0].top - (res[2] ? res[2].top : 0) + offset | |||
| if (this._in) { | |||
| // scroll-view 跳转 | |||
| this._in.page[this._in.scrollTop] = scrollTop | |||
| } else { | |||
| // 页面跳转 | |||
| uni.pageScrollTo({ | |||
| scrollTop, | |||
| duration: 300 | |||
| }) | |||
| } | |||
| resolve() | |||
| }) | |||
| // #endif | |||
| }) | |||
| }, | |||
| /** | |||
| * @description 获取文本内容 | |||
| * @return {String} | |||
| */ | |||
| getText (nodes) { | |||
| let text = ''; | |||
| (function traversal (nodes) { | |||
| for (let i = 0; i < nodes.length; i++) { | |||
| const node = nodes[i] | |||
| if (node.type === 'text') { | |||
| text += node.text.replace(/&/g, '&') | |||
| } else if (node.name === 'br') { | |||
| text += '\n' | |||
| } else { | |||
| // 块级标签前后加换行 | |||
| const isBlock = node.name === 'p' || node.name === 'div' || node.name === 'tr' || node.name === 'li' || (node.name[0] === 'h' && node.name[1] > '0' && node.name[1] < '7') | |||
| if (isBlock && text && text[text.length - 1] !== '\n') { | |||
| text += '\n' | |||
| } | |||
| // 递归获取子节点的文本 | |||
| if (node.children) { | |||
| traversal(node.children) | |||
| } | |||
| if (isBlock && text[text.length - 1] !== '\n') { | |||
| text += '\n' | |||
| } else if (node.name === 'td' || node.name === 'th') { | |||
| text += '\t' | |||
| } | |||
| } | |||
| } | |||
| })(nodes || this.nodes) | |||
| return text | |||
| }, | |||
| /** | |||
| * @description 获取内容大小和位置 | |||
| * @return {Promise} | |||
| */ | |||
| getRect () { | |||
| return new Promise((resolve, reject) => { | |||
| uni.createSelectorQuery() | |||
| // #ifndef MP-ALIPAY | |||
| .in(this) | |||
| // #endif | |||
| .select('#_root').boundingClientRect().exec(res => res[0] ? resolve(res[0]) : reject(Error('Root label not found'))) | |||
| }) | |||
| }, | |||
| /** | |||
| * @description 暂停播放媒体 | |||
| */ | |||
| pauseMedia () { | |||
| for (let i = (this._videos || []).length; i--;) { | |||
| this._videos[i].pause() | |||
| } | |||
| // #ifdef APP-PLUS | |||
| const command = 'for(var e=document.getElementsByTagName("video"),i=e.length;i--;)e[i].pause()' | |||
| // #ifndef APP-PLUS-NVUE | |||
| let page = this.$parent | |||
| while (!page.$scope) page = page.$parent | |||
| page.$scope.$getAppWebview().evalJS(command) | |||
| // #endif | |||
| // #ifdef APP-PLUS-NVUE | |||
| this.$refs.web.evalJs(command) | |||
| // #endif | |||
| // #endif | |||
| }, | |||
| /** | |||
| * @description 设置媒体播放速率 | |||
| * @param {Number} rate 播放速率 | |||
| */ | |||
| setPlaybackRate (rate) { | |||
| this.playbackRate = rate | |||
| for (let i = (this._videos || []).length; i--;) { | |||
| this._videos[i].playbackRate(rate) | |||
| } | |||
| // #ifdef APP-PLUS | |||
| const command = 'for(var e=document.getElementsByTagName("video"),i=e.length;i--;)e[i].playbackRate=' + rate | |||
| // #ifndef APP-PLUS-NVUE | |||
| let page = this.$parent | |||
| while (!page.$scope) page = page.$parent | |||
| page.$scope.$getAppWebview().evalJS(command) | |||
| // #endif | |||
| // #ifdef APP-PLUS-NVUE | |||
| this.$refs.web.evalJs(command) | |||
| // #endif | |||
| // #endif | |||
| }, | |||
| /** | |||
| * @description 设置内容 | |||
| * @param {String} content html 内容 | |||
| * @param {Boolean} append 是否在尾部追加 | |||
| */ | |||
| setContent (content, append) { | |||
| if (!append || !this.imgList) { | |||
| this.imgList = [] | |||
| } | |||
| const nodes = new Parser(this).parse(content) | |||
| // #ifdef APP-PLUS-NVUE | |||
| if (this._ready) { | |||
| this._set(nodes, append) | |||
| } | |||
| // #endif | |||
| this.$set(this, 'nodes', append ? (this.nodes || []).concat(nodes) : nodes) | |||
| // #ifndef APP-PLUS-NVUE | |||
| this._videos = [] | |||
| this.$nextTick(() => { | |||
| this._hook('onLoad') | |||
| this.$emit('load') | |||
| }) | |||
| if (this.lazyLoad || this.imgList._unloadimgs < this.imgList.length / 2) { | |||
| // 设置懒加载,每 350ms 获取高度,不变则认为加载完毕 | |||
| let height = 0 | |||
| const callback = rect => { | |||
| if (!rect || !rect.height) rect = {} | |||
| // 350ms 总高度无变化就触发 ready 事件 | |||
| if (rect.height === height) { | |||
| this.$emit('ready', rect) | |||
| } else { | |||
| height = rect.height | |||
| setTimeout(() => { | |||
| this.getRect().then(callback).catch(callback) | |||
| }, 350) | |||
| } | |||
| } | |||
| this.getRect().then(callback).catch(callback) | |||
| } else { | |||
| // 未设置懒加载,等待所有图片加载完毕 | |||
| if (!this.imgList._unloadimgs) { | |||
| this.getRect().then(rect => { | |||
| this.$emit('ready', rect) | |||
| }).catch(() => { | |||
| this.$emit('ready', {}) | |||
| }) | |||
| } | |||
| } | |||
| // #endif | |||
| }, | |||
| /** | |||
| * @description 调用插件钩子函数 | |||
| */ | |||
| _hook (name) { | |||
| for (let i = plugins.length; i--;) { | |||
| if (this.plugins[i][name]) { | |||
| this.plugins[i][name]() | |||
| } | |||
| } | |||
| }, | |||
| // #ifdef APP-PLUS-NVUE | |||
| /** | |||
| * @description 设置内容 | |||
| */ | |||
| _set (nodes, append) { | |||
| this.$refs.web.evalJs('setContent(' + JSON.stringify(nodes).replace(/%22/g, '') + ',' + JSON.stringify([this.containerStyle.replace(/(?:margin|padding)[^;]+/g, ''), this.errorImg, this.loadingImg, this.pauseVideo, this.scrollTable, this.selectable]) + ',' + append + ')') | |||
| }, | |||
| /** | |||
| * @description 接收到 web-view 消息 | |||
| */ | |||
| _onMessage (e) { | |||
| const message = e.detail.data[0] | |||
| switch (message.action) { | |||
| // web-view 初始化完毕 | |||
| case 'onJSBridgeReady': | |||
| this._ready = true | |||
| if (this.nodes) { | |||
| this._set(this.nodes) | |||
| } | |||
| break | |||
| // 内容 dom 加载完毕 | |||
| case 'onLoad': | |||
| this.height = message.height | |||
| this._hook('onLoad') | |||
| this.$emit('load') | |||
| break | |||
| // 所有图片加载完毕 | |||
| case 'onReady': | |||
| this.getRect().then(res => { | |||
| this.$emit('ready', res) | |||
| }).catch(() => { | |||
| this.$emit('ready', {}) | |||
| }) | |||
| break | |||
| // 总高度发生变化 | |||
| case 'onHeightChange': | |||
| this.height = message.height | |||
| break | |||
| // 图片点击 | |||
| case 'onImgTap': | |||
| this.$emit('imgtap', message.attrs) | |||
| if (this.previewImg) { | |||
| uni.previewImage({ | |||
| current: parseInt(message.attrs.i), | |||
| urls: this.imgList | |||
| }) | |||
| } | |||
| break | |||
| // 链接点击 | |||
| case 'onLinkTap': { | |||
| const href = message.attrs.href | |||
| this.$emit('linktap', message.attrs) | |||
| if (href) { | |||
| // 锚点跳转 | |||
| if (href[0] === '#') { | |||
| if (this.useAnchor) { | |||
| dom.scrollToElement(this.$refs.web, { | |||
| offset: message.offset | |||
| }) | |||
| } | |||
| } else if (href.includes('://')) { | |||
| // 打开外链 | |||
| if (this.copyLink) { | |||
| plus.runtime.openWeb(href) | |||
| } | |||
| } else { | |||
| uni.navigateTo({ | |||
| url: href, | |||
| fail () { | |||
| uni.switchTab({ | |||
| url: href | |||
| }) | |||
| } | |||
| }) | |||
| } | |||
| } | |||
| break | |||
| } | |||
| case 'onPlay': | |||
| this.$emit('play') | |||
| break | |||
| // 获取到锚点的偏移量 | |||
| case 'getOffset': | |||
| if (typeof message.offset === 'number') { | |||
| dom.scrollToElement(this.$refs.web, { | |||
| offset: message.offset + this._navigateTo.offset | |||
| }) | |||
| this._navigateTo.resolve() | |||
| } else { | |||
| this._navigateTo.reject(Error('Label not found')) | |||
| } | |||
| break | |||
| // 点击 | |||
| case 'onClick': | |||
| this.$emit('tap') | |||
| this.$emit('click') | |||
| break | |||
| // 出错 | |||
| case 'onError': | |||
| this.$emit('error', { | |||
| source: message.source, | |||
| attrs: message.attrs | |||
| }) | |||
| } | |||
| } | |||
| // #endif | |||
| } | |||
| } | |||
| </script> | |||
| <style> | |||
| /* #ifndef APP-PLUS-NVUE */ | |||
| /* 根节点样式 */ | |||
| ._root { | |||
| padding: 1px 0; | |||
| overflow-x: auto; | |||
| overflow-y: hidden; | |||
| -webkit-overflow-scrolling: touch; | |||
| } | |||
| /* 长按复制 */ | |||
| ._select { | |||
| user-select: text; | |||
| } | |||
| /* #endif */ | |||
| </style> | |||
| @ -0,0 +1,87 @@ | |||
| { | |||
| "id": "uv-parse", | |||
| "displayName": "uv-parse 富文本解析器 全面兼容vue3+2、app、h5、小程序等多端", | |||
| "version": "1.0.4", | |||
| "description": "uv-parse 该组件一般用于富文本解析场景,比如解析文章内容,商品详情,带原生HTML标签的各类字符串等,此组件和uni-app官方的rich-text组件功能有重合之处,但是也有不同的地方。", | |||
| "keywords": [ | |||
| "uv-parse", | |||
| "uvui", | |||
| "uv-ui", | |||
| "parse", | |||
| "富文本" | |||
| ], | |||
| "repository": "", | |||
| "engines": { | |||
| "HBuilderX": "^3.1.0" | |||
| }, | |||
| "dcloudext": { | |||
| "type": "component-vue", | |||
| "sale": { | |||
| "regular": { | |||
| "price": "0.00" | |||
| }, | |||
| "sourcecode": { | |||
| "price": "0.00" | |||
| } | |||
| }, | |||
| "contact": { | |||
| "qq": "" | |||
| }, | |||
| "declaration": { | |||
| "ads": "无", | |||
| "data": "插件不采集任何数据", | |||
| "permissions": "无" | |||
| }, | |||
| "npmurl": "" | |||
| }, | |||
| "uni_modules": { | |||
| "dependencies": [ | |||
| "uv-ui-tools" | |||
| ], | |||
| "encrypt": [], | |||
| "platforms": { | |||
| "cloud": { | |||
| "tcb": "y", | |||
| "aliyun": "y" | |||
| }, | |||
| "client": { | |||
| "Vue": { | |||
| "vue2": "y", | |||
| "vue3": "y" | |||
| }, | |||
| "App": { | |||
| "app-vue": "y", | |||
| "app-nvue": "y" | |||
| }, | |||
| "H5-mobile": { | |||
| "Safari": "y", | |||
| "Android Browser": "y", | |||
| "微信浏览器(Android)": "y", | |||
| "QQ浏览器(Android)": "y" | |||
| }, | |||
| "H5-pc": { | |||
| "Chrome": "y", | |||
| "IE": "y", | |||
| "Edge": "y", | |||
| "Firefox": "y", | |||
| "Safari": "y" | |||
| }, | |||
| "小程序": { | |||
| "微信": "y", | |||
| "阿里": "y", | |||
| "百度": "y", | |||
| "字节跳动": "y", | |||
| "QQ": "y", | |||
| "钉钉": "u", | |||
| "快手": "u", | |||
| "飞书": "u", | |||
| "京东": "u" | |||
| }, | |||
| "快应用": { | |||
| "华为": "u", | |||
| "联盟": "u" | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @ -0,0 +1,21 @@ | |||
| ## Parse 富文本解析器 | |||
| > **组件名:uv-parse** | |||
| 该组件一般用于富文本解析场景,比如解析文章内容,商品详情,带原生`HTML`标签的各类字符串等,此组件和`uni-app`官方的`rich-text`组件功能有重合之处,但是也有不同的地方。 | |||
| 该插件只提供富文本的解析,该功能已经足够丰富。如果需要富文本的编辑,可使用`uniapp`官方提供的组件。 | |||
| # <a href="https://www.uvui.cn/components/parse.html" target="_blank">查看文档</a> | |||
| ## [下载完整示例项目](https://ext.dcloud.net.cn/plugin?name=uv-ui) | |||
| ### [更多插件,请关注uv-ui组件库](https://ext.dcloud.net.cn/plugin?name=uv-ui) | |||
| <a href="https://ext.dcloud.net.cn/plugin?name=uv-ui" target="_blank"> | |||
|  | |||
| </a> | |||
| #### 如使用过程中有任何问题反馈,或者您对uv-ui有一些好的建议,欢迎加入uv-ui官方交流群:<a href="https://www.uvui.cn/components/addQQGroup.html" target="_blank">官方QQ群</a> | |||
| @ -0,0 +1,224 @@ | |||
| 'use strict' | |||
| // 等待初始化完毕 | |||
| document.addEventListener('UniAppJSBridgeReady', () => { | |||
| document.body.onclick = function () { | |||
| return uni.postMessage({ | |||
| data: { | |||
| action: 'onClick' | |||
| } | |||
| }) | |||
| } | |||
| uni.postMessage({ | |||
| data: { | |||
| action: 'onJSBridgeReady' | |||
| } | |||
| }) | |||
| }) | |||
| let options | |||
| let medias = [] | |||
| /** | |||
| * @description 获取标签的所有属性 | |||
| * @param {Element} ele | |||
| */ | |||
| function getAttrs(ele) { | |||
| const attrs = Object.create(null) | |||
| for (let i = ele.attributes.length; i--;) { | |||
| attrs[ele.attributes[i].name] = ele.attributes[i].value | |||
| } | |||
| return attrs | |||
| } | |||
| /** | |||
| * @description 图片加载出错 | |||
| */ | |||
| function onImgError() { | |||
| if (options[1]) { | |||
| this.src = options[1] | |||
| this.onerror = null | |||
| } // 取消监听点击 | |||
| this.onclick = null | |||
| this.ontouchstart = null | |||
| uni.postMessage({ | |||
| data: { | |||
| action: 'onError', | |||
| source: 'img', | |||
| attrs: getAttrs(this) | |||
| } | |||
| }) | |||
| } | |||
| /** | |||
| * @description 创建 dom 结构 | |||
| * @param {object[]} nodes 节点数组 | |||
| * @param {Element} parent 父节点 | |||
| * @param {string} namespace 命名空间 | |||
| */ | |||
| function createDom(nodes, parent, namespace) { | |||
| const _loop = function _loop(i) { | |||
| const node = nodes[i] | |||
| let ele = void 0 | |||
| if (!node.type || node.type == 'node') { | |||
| let { name } = node // svg 需要设置 namespace | |||
| if (name == 'svg') namespace = 'http://www.w3.org/2000/svg' | |||
| if (name == 'html' || name == 'body') name = 'div' // 创建标签 | |||
| if (!namespace) ele = document.createElement(name); else ele = document.createElementNS(namespace, name) // 设置属性 | |||
| for (const item in node.attrs) { | |||
| ele.setAttribute(item, node.attrs[item]) | |||
| } // 递归创建子节点 | |||
| if (node.children) createDom(node.children, ele, namespace) // 处理图片 | |||
| if (name == 'img') { | |||
| if (!ele.src && ele.getAttribute('data-src')) ele.src = ele.getAttribute('data-src') | |||
| if (!node.attrs.ignore) { | |||
| // 监听图片点击事件 | |||
| ele.onclick = function (e) { | |||
| e.stopPropagation() | |||
| uni.postMessage({ | |||
| data: { | |||
| action: 'onImgTap', | |||
| attrs: getAttrs(this) | |||
| } | |||
| }) | |||
| } | |||
| } | |||
| if (options[2]) { | |||
| image = new Image() | |||
| image.src = ele.src | |||
| ele.src = options[2] | |||
| image.onload = function () { | |||
| ele.src = this.src | |||
| } | |||
| image.onerror = function () { | |||
| ele.onerror() | |||
| } | |||
| } | |||
| ele.onerror = onImgError | |||
| } // 处理链接 | |||
| else if (name == 'a') { | |||
| ele.addEventListener('click', function (e) { | |||
| e.stopPropagation() | |||
| e.preventDefault() // 阻止默认跳转 | |||
| const href = this.getAttribute('href') | |||
| let offset | |||
| if (href && href[0] == '#') offset = (document.getElementById(href.substr(1)) || {}).offsetTop | |||
| uni.postMessage({ | |||
| data: { | |||
| action: 'onLinkTap', | |||
| attrs: getAttrs(this), | |||
| offset | |||
| } | |||
| }) | |||
| }, true) | |||
| } // 处理音视频 | |||
| else if (name == 'video' || name == 'audio') { | |||
| medias.push(ele) | |||
| if (!node.attrs.autoplay) { | |||
| if (!node.attrs.controls) ele.setAttribute('controls', 'true') // 空白图占位 | |||
| if (!node.attrs.poster) ele.setAttribute('poster', "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'/>") | |||
| } | |||
| if (options[3]) { | |||
| ele.onplay = function () { | |||
| for (let _i = 0; _i < medias.length; _i++) { | |||
| if (medias[_i] != this) medias[_i].pause() | |||
| } | |||
| } | |||
| } | |||
| ele.onerror = function () { | |||
| uni.postMessage({ | |||
| data: { | |||
| action: 'onError', | |||
| source: name, | |||
| attrs: getAttrs(this) | |||
| } | |||
| }) | |||
| } | |||
| } // 处理表格 | |||
| else if (name == 'table' && options[4] && !ele.style.cssText.includes('inline')) { | |||
| const div = document.createElement('div') | |||
| div.style.overflow = 'auto' | |||
| div.appendChild(ele) | |||
| ele = div | |||
| } else if (name == 'svg') namespace = void 0 | |||
| } else ele = document.createTextNode(node.text.replace(/&/g, '&')) | |||
| parent.appendChild(ele) | |||
| } | |||
| for (let i = 0; i < nodes.length; i++) { | |||
| var image | |||
| _loop(i) | |||
| } | |||
| } // 设置 html 内容 | |||
| window.setContent = function (nodes, opts, append) { | |||
| const ele = document.getElementById('content') // 背景颜色 | |||
| if (opts[0]) document.body.bgColor = opts[0] // 长按复制 | |||
| if (!opts[5]) ele.style.userSelect = 'none' | |||
| if (!append) { | |||
| ele.innerHTML = '' // 不追加则先清空 | |||
| medias = [] | |||
| } | |||
| options = opts | |||
| const fragment = document.createDocumentFragment() | |||
| createDom(nodes, fragment) | |||
| ele.appendChild(fragment) // 触发事件 | |||
| let height = ele.scrollHeight | |||
| uni.postMessage({ | |||
| data: { | |||
| action: 'onLoad', | |||
| height | |||
| } | |||
| }) | |||
| clearInterval(window.timer) | |||
| let ready = false | |||
| window.timer = setInterval(() => { | |||
| if (ele.scrollHeight != height) { | |||
| height = ele.scrollHeight | |||
| uni.postMessage({ | |||
| data: { | |||
| action: 'onHeightChange', | |||
| height | |||
| } | |||
| }) | |||
| } else if (!ready) { | |||
| ready = true | |||
| uni.postMessage({ | |||
| data: { | |||
| action: 'onReady' | |||
| } | |||
| }) | |||
| } | |||
| }, 350) | |||
| } // 回收计时器 | |||
| window.onunload = function () { | |||
| clearInterval(window.timer) | |||
| } | |||
| @ -0,0 +1,19 @@ | |||
| !(function (e, n) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = n() : typeof define === 'function' && define.amd ? define(n) : (e = e || self).uni = n() }(this, (() => { | |||
| 'use strict' | |||
| try { const e = {}; Object.defineProperty(e, 'passive', { get() { !0 } }), window.addEventListener('test-passive', null, e) } catch (e) {} const n = Object.prototype.hasOwnProperty; function t(e, t) { return n.call(e, t) } const i = []; const a = function (e, n) { const t = { options: { timestamp: +new Date() }, name: e, arg: n }; if (window.__dcloud_weex_postMessage || window.__dcloud_weex_) { if (e === 'postMessage') { const a = { data: [n] }; return window.__dcloud_weex_postMessage ? window.__dcloud_weex_postMessage(a) : window.__dcloud_weex_.postMessage(JSON.stringify(a)) } const o = { type: 'WEB_INVOKE_APPSERVICE', args: { data: t, webviewIds: i } }; window.__dcloud_weex_postMessage ? window.__dcloud_weex_postMessageToService(o) : window.__dcloud_weex_.postMessageToService(JSON.stringify(o)) } if (!window.plus) return window.parent.postMessage({ type: 'WEB_INVOKE_APPSERVICE', data: t, pageId: '' }, '*'); if (i.length === 0) { const r = plus.webview.currentWebview(); if (!r) throw new Error('plus.webview.currentWebview() is undefined'); const d = r.parent(); let s = ''; s = d ? d.id : r.id, i.push(s) } if (plus.webview.getWebviewById('__uniapp__service'))plus.webview.postMessageToUniNView({ type: 'WEB_INVOKE_APPSERVICE', args: { data: t, webviewIds: i } }, '__uniapp__service'); else { const w = JSON.stringify(t); plus.webview.getLaunchWebview().evalJS('UniPlusBridge.subscribeHandler("'.concat('WEB_INVOKE_APPSERVICE', '",').concat(w, ',').concat(JSON.stringify(i), ');')) } }; const o = { | |||
| navigateTo() { const e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}; const n = e.url; a('navigateTo', { url: encodeURI(n) }) }, navigateBack() { const e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}; const n = e.delta; a('navigateBack', { delta: parseInt(n) || 1 }) }, switchTab() { const e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}; const n = e.url; a('switchTab', { url: encodeURI(n) }) }, reLaunch() { const e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}; const n = e.url; a('reLaunch', { url: encodeURI(n) }) }, redirectTo() { const e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}; const n = e.url; a('redirectTo', { url: encodeURI(n) }) }, getEnv(e) { window.plus ? e({ plus: !0 }) : e({ h5: !0 }) }, postMessage() { const e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}; a('postMessage', e.data || {}) } | |||
| }; const r = /uni-app/i.test(navigator.userAgent); const d = /Html5Plus/i.test(navigator.userAgent); const s = /complete|loaded|interactive/; const w = window.my && navigator.userAgent.indexOf('AlipayClient') > -1; const u = window.swan && window.swan.webView && /swan/i.test(navigator.userAgent); const c = window.qq && window.qq.miniProgram && /QQ/i.test(navigator.userAgent) && /miniProgram/i.test(navigator.userAgent); const g = window.tt && window.tt.miniProgram && /toutiaomicroapp/i.test(navigator.userAgent); const v = window.wx && window.wx.miniProgram && /micromessenger/i.test(navigator.userAgent) && /miniProgram/i.test(navigator.userAgent); const p = window.qa && /quickapp/i.test(navigator.userAgent); for (var l, _ = function () { window.UniAppJSBridge = !0, document.dispatchEvent(new CustomEvent('UniAppJSBridgeReady', { bubbles: !0, cancelable: !0 })) }, f = [function (e) { if (r || d) return window.__dcloud_weex_postMessage || window.__dcloud_weex_ ? document.addEventListener('DOMContentLoaded', e) : window.plus && s.test(document.readyState) ? setTimeout(e, 0) : document.addEventListener('plusready', e), o }, function (e) { if (v) return window.WeixinJSBridge && window.WeixinJSBridge.invoke ? setTimeout(e, 0) : document.addEventListener('WeixinJSBridgeReady', e), window.wx.miniProgram }, function (e) { if (c) return window.QQJSBridge && window.QQJSBridge.invoke ? setTimeout(e, 0) : document.addEventListener('QQJSBridgeReady', e), window.qq.miniProgram }, function (e) { | |||
| if (w) { | |||
| document.addEventListener('DOMContentLoaded', e); const n = window.my; return { | |||
| navigateTo: n.navigateTo, navigateBack: n.navigateBack, switchTab: n.switchTab, reLaunch: n.reLaunch, redirectTo: n.redirectTo, postMessage: n.postMessage, getEnv: n.getEnv | |||
| } | |||
| } | |||
| }, function (e) { if (u) return document.addEventListener('DOMContentLoaded', e), window.swan.webView }, function (e) { if (g) return document.addEventListener('DOMContentLoaded', e), window.tt.miniProgram }, function (e) { | |||
| if (p) { | |||
| window.QaJSBridge && window.QaJSBridge.invoke ? setTimeout(e, 0) : document.addEventListener('QaJSBridgeReady', e); const n = window.qa; return { | |||
| navigateTo: n.navigateTo, navigateBack: n.navigateBack, switchTab: n.switchTab, reLaunch: n.reLaunch, redirectTo: n.redirectTo, postMessage: n.postMessage, getEnv: n.getEnv | |||
| } | |||
| } | |||
| }, function (e) { return document.addEventListener('DOMContentLoaded', e), o }], m = 0; m < f.length && !(l = f[m](_)); m++);l || (l = {}); const E = typeof uni !== 'undefined' ? uni : {}; if (!E.navigateTo) for (const b in l)t(l, b) && (E[b] = l[b]); return E.webView = l, E | |||
| }))) | |||
| @ -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:hidden}body{margin:0}video{width:300px;height:225px}img{max-width:100%;-webkit-touch-callout:none}@keyframes show{0%{opacity:0}100%{opacity:1}}</style></head><body><div id="content"></div><script type="text/javascript" src="./js/uni.webview.min.js"></script><script type="text/javascript" src="./js/handler.js"></script></body> | |||