Browse Source

feat: 新增回收去向功能及相关页面和组件

refactor: 替换rich-text为uv-parse组件以优化富文本解析

fix: 修复图片加载错误处理和黑名单用户限制

style: 调整页面样式和布局

docs: 更新uv-parse组件文档和配置

chore: 添加uv-parse组件依赖和静态资源
v1
前端-胡立永 5 months ago
parent
commit
17463ee610
25 changed files with 3677 additions and 380 deletions
  1. +1
    -2
      App.vue
  2. +23
    -0
      api/model/recyclingDestination.js
  3. +63
    -0
      compoent/base/kefu.vue
  4. +143
    -0
      compoent/base/navbar.vue
  5. +134
    -0
      compoent/base/tabbar.vue
  6. +3
    -0
      config.js
  7. +17
    -0
      pages.json
  8. +9
    -1
      pages/baoyou-city/baoyou-city.vue
  9. +18
    -8
      pages/component/home.vue
  10. +3
    -3
      pages/component/my.vue
  11. +1
    -1
      pages/component/recycle copy.vue
  12. +45
    -7
      pages/component/recycle.vue
  13. +346
    -357
      pages/index/index.vue
  14. +48
    -0
      pages/mine/questionDetail.vue
  15. +48
    -0
      pages/mine/recyclingDestination.vue
  16. +1
    -1
      pages/subcomponent/about.vue
  17. +13
    -0
      uni_modules/uv-parse/changelog.md
  18. +576
    -0
      uni_modules/uv-parse/components/uv-parse/node/node.vue
  19. +1335
    -0
      uni_modules/uv-parse/components/uv-parse/parser.js
  20. +498
    -0
      uni_modules/uv-parse/components/uv-parse/uv-parse.vue
  21. +87
    -0
      uni_modules/uv-parse/package.json
  22. +21
    -0
      uni_modules/uv-parse/readme.md
  23. +224
    -0
      uni_modules/uv-parse/static/app-plus/uv-parse/js/handler.js
  24. +19
    -0
      uni_modules/uv-parse/static/app-plus/uv-parse/js/uni.webview.min.js
  25. +1
    -0
      uni_modules/uv-parse/static/app-plus/uv-parse/local.html

+ 1
- 2
App.vue View File

@ -61,9 +61,8 @@
this.getBannerList(),
this.getPricePreviewList(),
this.getConfigData(),
this.getQrCode()
]
this.getQrCode()
await Promise.all(promises)
// 100%


+ 23
- 0
api/model/recyclingDestination.js View File

@ -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

+ 63
- 0
compoent/base/kefu.vue View File

@ -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>

+ 143
- 0
compoent/base/navbar.vue View File

@ -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>

+ 134
- 0
compoent/base/tabbar.vue View File

@ -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>

+ 3
- 0
config.js View File

@ -2,6 +2,9 @@
const type = 'dev'
const config = {
local: {
baseUrl: 'http://127.0.0.1',
},
dev: {
baseUrl: 'http://h5.xzaiyp.top',
},


+ 17
- 0
pages.json View File

@ -31,6 +31,23 @@
}
],
"subPackages": [
{
"root": "pages/mine",
"pages": [
{
"path": "recyclingDestination",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "questionDetail",
"style": {
"navigationStyle": "custom"
}
}
]
},
{
"root": "pages/subcomponent",
"pages": [


+ 9
- 1
pages/baoyou-city/baoyou-city.vue View File

@ -9,7 +9,9 @@
</view>
<!-- 蓝色banner卡片 -->
<view class="byc-banner">
<image class="byc-banner-img" src="https://oss.budingxiaoshuo.com/upload/已开通包邮服务的城市-banner_1748252607736.png" mode="widthFix" />
<image class="byc-banner-img"
:src="city_banner"
mode="widthFix" />
</view>
<!-- 主内容卡片 -->
<view class="byc-main-card">
@ -65,6 +67,12 @@ export default {
}
});
},
computed: {
city_banner() {
const item = getApp().globalData.configData.find(i => i.keyName === 'city_banner')
return item ? item.keyContent : ''
}
},
methods: {
goBack() {
uni.navigateBack()


+ 18
- 8
pages/component/home.vue View File

@ -8,7 +8,7 @@
:interval="3000"
:duration="500"
circular
style="width: 100%; height: 380rpx;"
style="width: 100%; height: 320rpx;"
>
<swiper-item v-for="(item, index) in bannerList" :key="item.id || index">
<video
@ -137,11 +137,14 @@
<text class="title">回收去向</text>
</view>
<view class="destination-grid">
<view class="destination-item " :class="`destination-item${index + 1}`" v-for="(item, index) in destinations" :key="index">
<view class="destination-item "
@click="$utils.navigateTo('/pages/mine/recyclingDestination?id=' + item.id)"
:class="`destination-item${index + 1}`"
v-for="(item, index) in destinations" :key="index">
<image :src="item.icon" mode="aspectFit" class="dest-icon"></image>
<view class="dest-info">
<text class="dest-title">{{item.title}}</text>
<text class="dest-desc">{{item.desc}}</text>
<text class="dest-desc">{{item.description}}</text>
</view>
</view>
</view>
@ -197,22 +200,22 @@ export default {
{
icon: '/static/home/爱心援乡.png',
title: '爱心援乡',
desc: '精准帮扶贫困群体'
description: '精准帮扶贫困群体'
},
{
icon: '/static/home/回塑新源.png',
title: '回塑新源',
desc: '塑料的第二次成型'
description: '塑料的第二次成型'
},
{
icon: '/static/home/织物出海.png',
title: '织物出海',
desc: '分拣出最高价值'
description: '分拣出最高价值'
},
{
icon: '/static/home/碳循再生.png',
title: '碳循再生',
desc: '减碳从出发生系统'
description: '减碳从出发生系统'
}
],
bannerList: [],
@ -336,6 +339,12 @@ export default {
}
})
},
getRecyclingDestination() {
this.$api('getRecyclingDestination')
.then(res => {
this.destinations = res.result
})
},
goToInspectionReport(item) {
uni.navigateTo({
url: `/pages/subcomponent/inspection-report?id=${item.id}`
@ -415,6 +424,7 @@ export default {
//
uni.$on('configDataUpdated', this.updateCionData);
this.getPricePreview();
this.getRecyclingDestination()
},
}
</script>
@ -435,7 +445,7 @@ export default {
.banner {
width: 100%;
height: 390rpx;
height: 330rpx;
position: relative;
overflow: hidden;
border-radius: 0 0 30rpx 30rpx;


+ 3
- 3
pages/component/my.vue View File

@ -393,8 +393,8 @@ export default {
return getApp().globalData.bannerList || []
},
myBannerImage() {
const banner = (getApp().globalData.bannerList || []).find(item => item.title === '我的-轮播图');
return banner ? banner.image : '';
const item = getApp().globalData.configData.find(i => i.keyName === 'user_banner')
return item ? item.keyContent : ''
},
userTypeText() {
// 0 , 1 广, 2 广使
@ -452,7 +452,7 @@ export default {
}
.banner {
height: 400rpx;
height: 320rpx;
background: #ff6b35;
position: relative;
overflow: hidden;


+ 1
- 1
pages/component/recycle copy.vue View File

@ -249,7 +249,7 @@
<view class="rule-popup">
<view class="rule-popup-title">回收规则</view>
<scroll-view class="rule-popup-content" scroll-y>
<rich-text :nodes="ruleHtml" />
<uv-parse :nodes="ruleHtml" />
</scroll-view>
<button class="rule-popup-btn" @click="closeRulePopup">我知道了</button>
<!-- <view class="rule-popup-close" @tap="closeRulePopup">


+ 45
- 7
pages/component/recycle.vue View File

@ -2,15 +2,15 @@
<view class="container">
<!-- 顶部banner -->
<view class="banner">
<swiper
<!-- <swiper
:indicator-dots="false"
:autoplay="true"
:interval="3000"
:duration="500"
circular
style="width: 100%; height: 400rpx;"
>
<swiper-item v-for="(item, index) in bannerList" :key="item.id || index">
style="width: 100%; height: 320rpx;"
> -->
<!-- <swiper-item v-for="(item, index) in bannerList" :key="item.id || index">
<video
v-if="item.type == 1"
:src="item.voUrl"
@ -25,8 +25,11 @@
></video>
<image v-else :src="item.image" mode="aspectFill" style="width: 100%; height: 100%;" />
</swiper-item>
</swiper>
</swiper> -->
<image v-if="recycle_banner" :src="recycle_banner" mode="aspectFill" style="width: 100%; height: 100%;" />
</view>
<!-- 商品列表 -->
<view class="goods-list">
@ -250,7 +253,7 @@
<view class="rule-popup">
<view class="rule-popup-title">回收规则</view>
<scroll-view class="rule-popup-content" scroll-y>
<rich-text :nodes="ruleHtml" />
<uv-parse :content="ruleHtml"></uv-parse>
</scroll-view>
<button class="rule-popup-btn" @click="closeRulePopup">我知道了</button>
<!-- <view class="rule-popup-close" @tap="closeRulePopup">
@ -352,9 +355,15 @@ export default {
reduceBrandList: [], //
viewedRuleItems: new Set(), // ID
loadOptions: null, // options
userInfo: null, //
isUserBlacklisted: false, //
}
},
computed: {
recycle_banner() {
const item = getApp().globalData.configData.find(i => i.keyName === 'recycle_banner')
return item ? item.keyContent : ''
},
//
recycleList() {
const currentCategoryId = this.categories[this.currentCategory]?.id
@ -625,6 +634,17 @@ export default {
})
},
submitOrder() {
//
if (this.isUserBlacklisted) {
uni.showModal({
title: '提示',
content: '您的账户已被限制使用回收服务,如有疑问请联系客服。',
showCancel: false,
confirmText: '我知道了'
})
return
}
if (this.totalCount < 3) {
uni.showToast({
title: '各品类混合需要满3件才能回收哦',
@ -740,6 +760,21 @@ export default {
toggleDetailPanel() {
this.showDetailPanel = !this.showDetailPanel
},
fetchUserInfo() {
if(uni.getStorageSync('token')){
this.login_status = getApp().globalData.login_status;
this.$api("getUserByToken",{},(res)=>{
if(res.code == 200){
this.userInfo = res.result
//
this.isUserBlacklisted = res.result.isBlack === 'Y'
// isTuiType 0,1广,2广使
}
})
} else {
this.login_status = false;
}
},
updateQuantityByProduct(item, delta) {
//
if (item.brandId) {
@ -984,6 +1019,9 @@ export default {
uni.$off('clearRecycleOrderData')
},
onShow() {
//
this.fetchUserInfo()
const id = getApp().globalData.targetRecycleCategoryId
if (id) {
const trySwitch = () => {
@ -1083,7 +1121,7 @@ export default {
// flex: 1;
display: flex;
position: relative;
height: calc(100vh - 400rpx - 120rpx - env(safe-area-inset-bottom)); /* 减去banner和底部栏的高度 */
height: calc(100vh - 320rpx - 120rpx - env(safe-area-inset-bottom)); /* 减去banner和底部栏的高度 */
margin-top: -10rpx;
z-index: 2;
border-radius: 20rpx 20rpx 0 0;


+ 346
- 357
pages/index/index.vue View File

@ -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>

+ 48
- 0
pages/mine/questionDetail.vue View File

@ -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>

+ 48
- 0
pages/mine/recyclingDestination.vue View File

@ -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>

+ 1
- 1
pages/subcomponent/about.vue View File

@ -10,7 +10,7 @@
<!-- 主卡片 -->
<view class="main-card" :style="{marginTop: (statusBarHeight + 88) + 'rpx'}">
<!-- 富文本内容 -->
<rich-text :nodes="memberTextContent" class="rich-text-content"></rich-text>
<uv-parse :nodes="memberTextContent" class="rich-text-content"></uv-parse>
</view>
</view>
</template>


+ 13
- 0
uni_modules/uv-parse/changelog.md View File

@ -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 富文本解析器

+ 576
- 0
uni_modules/uv-parse/components/uv-parse/node/node.vue View File

@ -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>

+ 1335
- 0
uni_modules/uv-parse/components/uv-parse/parser.js
File diff suppressed because it is too large
View File


+ 498
- 0
uni_modules/uv-parse/components/uv-parse/uv-parse.vue View File

@ -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(/&amp;/g, '&')
} else if (node.name === 'br') {
text += '\n'
} else {
//
const isBlock = node.name === 'p' || node.name === 'div' || node.name === 'tr' || node.name === 'li' || (node.name[0] === 'h' && node.name[1] > '0' && node.name[1] < '7')
if (isBlock && text && text[text.length - 1] !== '\n') {
text += '\n'
}
//
if (node.children) {
traversal(node.children)
}
if (isBlock && text[text.length - 1] !== '\n') {
text += '\n'
} else if (node.name === 'td' || node.name === 'th') {
text += '\t'
}
}
}
})(nodes || this.nodes)
return text
},
/**
* @description 获取内容大小和位置
* @return {Promise}
*/
getRect () {
return new Promise((resolve, reject) => {
uni.createSelectorQuery()
// #ifndef MP-ALIPAY
.in(this)
// #endif
.select('#_root').boundingClientRect().exec(res => res[0] ? resolve(res[0]) : reject(Error('Root label not found')))
})
},
/**
* @description 暂停播放媒体
*/
pauseMedia () {
for (let i = (this._videos || []).length; i--;) {
this._videos[i].pause()
}
// #ifdef APP-PLUS
const command = 'for(var e=document.getElementsByTagName("video"),i=e.length;i--;)e[i].pause()'
// #ifndef APP-PLUS-NVUE
let page = this.$parent
while (!page.$scope) page = page.$parent
page.$scope.$getAppWebview().evalJS(command)
// #endif
// #ifdef APP-PLUS-NVUE
this.$refs.web.evalJs(command)
// #endif
// #endif
},
/**
* @description 设置媒体播放速率
* @param {Number} rate 播放速率
*/
setPlaybackRate (rate) {
this.playbackRate = rate
for (let i = (this._videos || []).length; i--;) {
this._videos[i].playbackRate(rate)
}
// #ifdef APP-PLUS
const command = 'for(var e=document.getElementsByTagName("video"),i=e.length;i--;)e[i].playbackRate=' + rate
// #ifndef APP-PLUS-NVUE
let page = this.$parent
while (!page.$scope) page = page.$parent
page.$scope.$getAppWebview().evalJS(command)
// #endif
// #ifdef APP-PLUS-NVUE
this.$refs.web.evalJs(command)
// #endif
// #endif
},
/**
* @description 设置内容
* @param {String} content html 内容
* @param {Boolean} append 是否在尾部追加
*/
setContent (content, append) {
if (!append || !this.imgList) {
this.imgList = []
}
const nodes = new Parser(this).parse(content)
// #ifdef APP-PLUS-NVUE
if (this._ready) {
this._set(nodes, append)
}
// #endif
this.$set(this, 'nodes', append ? (this.nodes || []).concat(nodes) : nodes)
// #ifndef APP-PLUS-NVUE
this._videos = []
this.$nextTick(() => {
this._hook('onLoad')
this.$emit('load')
})
if (this.lazyLoad || this.imgList._unloadimgs < this.imgList.length / 2) {
// 350ms
let height = 0
const callback = rect => {
if (!rect || !rect.height) rect = {}
// 350ms ready
if (rect.height === height) {
this.$emit('ready', rect)
} else {
height = rect.height
setTimeout(() => {
this.getRect().then(callback).catch(callback)
}, 350)
}
}
this.getRect().then(callback).catch(callback)
} else {
//
if (!this.imgList._unloadimgs) {
this.getRect().then(rect => {
this.$emit('ready', rect)
}).catch(() => {
this.$emit('ready', {})
})
}
}
// #endif
},
/**
* @description 调用插件钩子函数
*/
_hook (name) {
for (let i = plugins.length; i--;) {
if (this.plugins[i][name]) {
this.plugins[i][name]()
}
}
},
// #ifdef APP-PLUS-NVUE
/**
* @description 设置内容
*/
_set (nodes, append) {
this.$refs.web.evalJs('setContent(' + JSON.stringify(nodes).replace(/%22/g, '') + ',' + JSON.stringify([this.containerStyle.replace(/(?:margin|padding)[^;]+/g, ''), this.errorImg, this.loadingImg, this.pauseVideo, this.scrollTable, this.selectable]) + ',' + append + ')')
},
/**
* @description 接收到 web-view 消息
*/
_onMessage (e) {
const message = e.detail.data[0]
switch (message.action) {
// web-view
case 'onJSBridgeReady':
this._ready = true
if (this.nodes) {
this._set(this.nodes)
}
break
// dom
case 'onLoad':
this.height = message.height
this._hook('onLoad')
this.$emit('load')
break
//
case 'onReady':
this.getRect().then(res => {
this.$emit('ready', res)
}).catch(() => {
this.$emit('ready', {})
})
break
//
case 'onHeightChange':
this.height = message.height
break
//
case 'onImgTap':
this.$emit('imgtap', message.attrs)
if (this.previewImg) {
uni.previewImage({
current: parseInt(message.attrs.i),
urls: this.imgList
})
}
break
//
case 'onLinkTap': {
const href = message.attrs.href
this.$emit('linktap', message.attrs)
if (href) {
//
if (href[0] === '#') {
if (this.useAnchor) {
dom.scrollToElement(this.$refs.web, {
offset: message.offset
})
}
} else if (href.includes('://')) {
//
if (this.copyLink) {
plus.runtime.openWeb(href)
}
} else {
uni.navigateTo({
url: href,
fail () {
uni.switchTab({
url: href
})
}
})
}
}
break
}
case 'onPlay':
this.$emit('play')
break
//
case 'getOffset':
if (typeof message.offset === 'number') {
dom.scrollToElement(this.$refs.web, {
offset: message.offset + this._navigateTo.offset
})
this._navigateTo.resolve()
} else {
this._navigateTo.reject(Error('Label not found'))
}
break
//
case 'onClick':
this.$emit('tap')
this.$emit('click')
break
//
case 'onError':
this.$emit('error', {
source: message.source,
attrs: message.attrs
})
}
}
// #endif
}
}
</script>
<style>
/* #ifndef APP-PLUS-NVUE */
/* 根节点样式 */
._root {
padding: 1px 0;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
}
/* 长按复制 */
._select {
user-select: text;
}
/* #endif */
</style>

+ 87
- 0
uni_modules/uv-parse/package.json View File

@ -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"
}
}
}
}
}

+ 21
- 0
uni_modules/uv-parse/readme.md View File

@ -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">
![image](https://mp-a667b617-c5f1-4a2d-9a54-683a67cff588.cdn.bspapp.com/uv-ui/banner.png)
</a>
#### 如使用过程中有任何问题反馈,或者您对uv-ui有一些好的建议,欢迎加入uv-ui官方交流群:<a href="https://www.uvui.cn/components/addQQGroup.html" target="_blank">官方QQ群</a>

+ 224
- 0
uni_modules/uv-parse/static/app-plus/uv-parse/js/handler.js View File

@ -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(/&amp;/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)
}

+ 19
- 0
uni_modules/uv-parse/static/app-plus/uv-parse/js/uni.webview.min.js View File

@ -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
})))

+ 1
- 0
uni_modules/uv-parse/static/app-plus/uv-parse/local.html View File

@ -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>

Loading…
Cancel
Save