
nvue+uniapp模仿抖音APP实例|uni-app直播/短视频
之前有给大家分享一个electron-webosx仿MAC桌面管理系统。今天分享的是全端uniapp小视频/直播实例项目。
uniapp-ttLive短视频 一款基于uni-app+nvue+uview-ui等技术开发的多功能跨端直播+短视频项目。流畅的上下滑动体验、支持全景式悬浮沉浸效果。可暂停/播放小视频、直播送礼物、聊天互动等功能。
vue3.x+electron13+vite2模仿macui桌面管理框架
使用技术
- 编码器/技术:HbuilderX3.1.21+Uniapp+Nvue+Vuex+Uapopup
- UI组件库:uView-ui / uni-ui
- 矢量图标库:iconfont字体图标
- 弹窗组件:UApopup 基于uni-app封装跨端弹窗组件
- 自定义导航条+底部菜单栏
- 编译支持:H5+小程序+APP端
基于 HbuilderX3.1.21 编码器开发,搭配nvue原生页面解决video层级过高问题,自定义 ua-navbar ua-tabbar ua-popup 组件全面支持Nvue页面。搭配uview-ui和uni-ui组件库。可编译至h5+小程序+APP端。
项目结构目录
main.js配置
import Vue from 'vue'
import App from './App'
import uView from 'uview-ui'
Vue.use(uView)
// 引入状态管理
import Store from './store'
Vue.prototype.$store = Store
const app = new Vue({
...App
})
app.$mount()
全局获取状态栏高度
<script>
export default {
globalData: {
// 全局设置状态栏和导航栏高度
statusBarH: 0,
customBarH: 0,
},
onLaunch: function() {
console.log('App Launch')
let token = uni.getStorageSync('token')
if(!token) {
uni.navigateTo({
url: '/pages/auth/login'
})
}
uni.getSystemInfo({
success: (e) => {
// 获取手机状态栏高度
let statusBar = e.statusBarHeight
let customBar
// #ifndef MP
customBar = statusBar + (e.platform == 'android' ? 50 : 45)
// #endif
// #ifdef MP-WEIXIN
// 获取胶囊按钮的布局位置信息
let menu = wx.getMenuButtonBoundingClientRect()
// 导航栏高度 = 胶囊下距离 + 胶囊上距离 - 状态栏高度
customBar = menu.bottom + menu.top - statusBar
// #endif
// #ifdef MP-ALIPAY
customBar = statusBar + e.titleBarHeight
// #endif
// 兼容nvue写法(H5/小程序/APP/APP-Nvue)
this.globalData.statusBarH = statusBar
this.globalData.customBarH = customBar
}
})
},
onShow: function() {
console.log('App Show')
},
onHide: function() {
console.log('App Hide')
}
}
</script>
uni-app全端自定义组件
项目中用到的顶部导航栏、底部菜单栏及弹窗均是自定义组件来实现功能。
<ua-popup v-model="isVisibleConfirm" shadeClose="false" title="标题" xclose z-index="1001"
content="<div style='color:#ff557f;padding:20px 40px;'>预测未来的最好办法是自己亲手创造未来!</div>"
:btns="[
{text: '取消', click: handleCancel},
{text: '确定', style: 'color:#00aa00;', click: handleOk},
]"
/>
<script>
export default {
methods: {
handleOk() {
let $ua = this.$refs.uapopup
$ua.open({
content: '人生漫漫,且行且珍惜',
customStyle: {'background-color': 'rgba(170, 0, 127, 0.6)', 'color': '#fff'},
time: 3,
onClose() {
$ua.open({
type: 'android',
content: '<div style="color:#aa007f">一切都将一去杳然,任何人都无法将其捕获。</div>',
customStyle: {'width': '210px'},
btns: [
{
text: '关闭',
click() {
$ua.close()
}
},
{
text: '确定',
style: 'color:#00aa00;',
click() {
// ...
}
}
]
})
}
})
}
}
}
</script>
uniapp直播/短视频
项目中短视频页面,整体分为顶部导航栏、底部菜单栏、中间视频区域。
<view v-if="currentTab == 2" class="ua__tabcnt-recommend">
<swiper class="ua__vdplayer-swiper flex1" :current="currentVideo" vertical @change="handleSwipeVertical">
<swiper-item v-for="(item, index) in videoList" :key="index">
<!-- 视频模块 -->
<view class="ua__vdplayer-video flex1">
<video class="vdplayer" :id="'vdplayer' + index" :ref="'vdplayer' + index"
:src="item.src"
:controls="false" :loop="true" :show-center-play-btn="false" object-fit="fill"
:autoplay="index == currentVideo"
@play="isPlaying=true" @timeupdate="handleTimeUpdate"
:style="{'width': winWidth, 'height': winHeight}"
>
</video>
<view class="ua__vdplayer-playwrap" @click="handleVideoClicked"><view v-if="!isPlaying" class="ua__vdplayer-playbtn"><text class="iconfont">{{`\ue607`}}</text></view></view>
</view>
<!-- 信息模块 -->
<view class="ua__vdplayer-info flexbox flex-col">
<view class="flexbox flex-row flex-alignb">
<!-- //左侧信息 -->
<view class="vdinfo__left flex1">
<view class="ltitem uavatar flexbox flex-row">
<navigator url="#" class="flexbox flex-alignc flex-row"><image class="uimg" :src="item.avatar" /><text class="uname">{{item.author}}</text></navigator>
<view class="flexbox btn" :class="{'actived': item.isFollow}" @click="handleFollow(index)"><text class="btn-text">{{item.isFollow ? '已关注' : '关注'}}</text></view>
</view>
<view v-if="item.topic" class="ltitem flexbox flex-row">
<view class="kw" v-for="(kw, index2) in item.topic" :key="index2"><text class="lbl">#{{kw}}</text></view>
</view>
<view class="ltitem"><text class="desc">{{item.desc}}</text></view>
</view>
<!-- //右侧按钮 -->
<view class="vdinfo__right flexbox flex-col">
<view class="rtitem ball" v-if="item.goods&&item.goods.length > 0" @click="handleShowGoodsPopup(item.goods)"><text class="icon iconfont">{{`\ue734`}}</text></view>
<view class="rtitem" :class="{'isliked': item.isLike}" @click="handleLiked(index)"><text class="icon iconfont">{{`\ue635`}}</text><text class="num">{{item.likeNum+(item.isLike ? 1 : 0)}}</text></view>
<view class="rtitem" @click="showReplyPopup = true"><text class="icon iconfont">{{`\ue632`}}</text><text class="num">{{item.replyNum}}</text></view>
<view class="rtitem" @click="showSharePopup = true"><text class="icon iconfont">{{`\ue63b`}}</text><text class="num">{{item.shareNum}}</text></view>
</view>
</view>
</view>
</swiper-item>
</swiper>
<!-- 底部播放进度条 -->
<view class="ua__vdplayer-progress"><view class="bar" :style="{'width': progressBar+'px'}"></view></view>
</view>
<script>
const app = getApp()
import videoJSON from '@/mock/videolist.js'
export default {
data() {
return {
// 导航栏高度
customBarHeight: app.globalData.customBarH,
navbarBgcolor: '#21252b',
tabbarBgcolor: '#21252b',
tabNavLs: [
{label: '附近动态', badge: 5, lists: []},
{label: '关注', lists: []},
{label: '推荐', dot: true, lists: []},
],
// 当前选项卡
currentTab: 0,
// 当前视频索引
currentVideo: 0,
// 视频数据
videoList: videoJSON,
// 视频是否播放中
isPlaying: false,
// 点击次数
clickNum: 0,
// 视频播放进度条
progressBar: 0,
clickTimer: null,
// 屏幕宽高
winWidth: '',
winHeight: '',
popupGoodsList: [],
showGoodsPopup: false,
showReplyPopup: false,
showSharePopup: false,
}
},
watch: {
currentTab(val) {
this.changeTabPanel(val)
}
},
computed:{
customBarMargin() {
return `margin-top: ${this.customBarHeight}px`
}
},
created() {
// 引入iconfont字体
// #ifdef APP-NVUE
const domModule = weex.requireModule('dom')
domModule.addRule('fontFace', {
fontFamily: "nvueIcon",
'src': "url('/static/fonts/iconfont.ttf')"
});
// #endif
let wW = uni.getSystemInfoSync().windowWidth
let wH = uni.getSystemInfoSync().windowHeight
this.winWidth = `${wW}px`
this.winHeight = `${wH}px`
},
methods: {
// 长按动态
handleDynamicMenu(e) {
let points
// #ifndef APP-NVUE
points = [e.touches[0].clientX, e.touches[0].clientY]
// #endif
// #ifdef APP-NVUE
points = [e.touches[0].screenX, e.touches[0].screenY]
// #endif
this.$refs.uapopup.open({
type: 'contextmenu',
follow: points,
btns: [
{text: '不感兴趣'},
{text: '复制'},
{
text: '举报',
style: 'color:#f00;',
click: () => {
this.$refs.uapopup.close()
}
},
],
})
},
/* ++++++++++ { 视频播放模块 } ++++++++++ */
getVideoCtx() {
// return this.$refs['vdplayer' + this.currentVideo][0]
return uni.createVideoContext('vdplayer'+ this.currentVideo, this)
},
// 垂直滑动视频
handleSwipeVertical(e) {
let index = e.detail.current
this.progressBar = 0
this.isPlaying = false
let video = this.getVideoCtx()
if(!video) return
video.pause()
// 重新开始
video.seek(0)
this.currentVideo = index
// 自动播放
this.handlePlay()
},
handlePlay() {
let video = this.getVideoCtx()
if(!video) return
video.play()
this.isPlaying = true
},
handlePause() {
let video = this.getVideoCtx()
if(!video) return
video.pause()
this.isPlaying = false
},
// 点击视频(单击/双击)
handleVideoClicked() {
this.clickTimer && clearTimeout(this.clickTimer)
this.clickNum++
this.clickTimer = setTimeout(() => {
if(this.clickNum >= 2) {
console.log('你双击了')
}else {
console.log('你单击了')
if(this.isPlaying) {
this.handlePause()
}else {
this.handlePlay()
}
}
this.clickNum = 0
}, 250)
},
...
}
}
</script>
okey,基于uni-app开发原生短视频/直播项目就分享到这里。
链接:https://juejin.cn/post/7010195797174124557/
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
之前有给大家分享一个electron-webosx仿MAC桌面管理系统。今天分享的是全端uniapp小视频/直播实例项目。
uniapp-ttLive短视频 一款基于uni-app+nvue+uview-ui等技术开发的多功能跨端直播+短视频项目。流畅的上下滑动体验、支持全景式悬浮沉浸效果。可暂停/播放小视频、直播送礼物、聊天互动等功能。
vue3.x+electron13+vite2模仿macui桌面管理框架
使用技术
- 编码器/技术:HbuilderX3.1.21+Uniapp+Nvue+Vuex+Uapopup
- UI组件库:uView-ui / uni-ui
- 矢量图标库:iconfont字体图标
- 弹窗组件:UApopup 基于uni-app封装跨端弹窗组件
- 自定义导航条+底部菜单栏
- 编译支持:H5+小程序+APP端
基于 HbuilderX3.1.21 编码器开发,搭配nvue原生页面解决video层级过高问题,自定义 ua-navbar ua-tabbar ua-popup 组件全面支持Nvue页面。搭配uview-ui和uni-ui组件库。可编译至h5+小程序+APP端。
项目结构目录
main.js配置
import Vue from 'vue'
import App from './App'
import uView from 'uview-ui'
Vue.use(uView)
// 引入状态管理
import Store from './store'
Vue.prototype.$store = Store
const app = new Vue({
...App
})
app.$mount()
全局获取状态栏高度
<script>
export default {
globalData: {
// 全局设置状态栏和导航栏高度
statusBarH: 0,
customBarH: 0,
},
onLaunch: function() {
console.log('App Launch')
let token = uni.getStorageSync('token')
if(!token) {
uni.navigateTo({
url: '/pages/auth/login'
})
}
uni.getSystemInfo({
success: (e) => {
// 获取手机状态栏高度
let statusBar = e.statusBarHeight
let customBar
// #ifndef MP
customBar = statusBar + (e.platform == 'android' ? 50 : 45)
// #endif
// #ifdef MP-WEIXIN
// 获取胶囊按钮的布局位置信息
let menu = wx.getMenuButtonBoundingClientRect()
// 导航栏高度 = 胶囊下距离 + 胶囊上距离 - 状态栏高度
customBar = menu.bottom + menu.top - statusBar
// #endif
// #ifdef MP-ALIPAY
customBar = statusBar + e.titleBarHeight
// #endif
// 兼容nvue写法(H5/小程序/APP/APP-Nvue)
this.globalData.statusBarH = statusBar
this.globalData.customBarH = customBar
}
})
},
onShow: function() {
console.log('App Show')
},
onHide: function() {
console.log('App Hide')
}
}
</script>
uni-app全端自定义组件
项目中用到的顶部导航栏、底部菜单栏及弹窗均是自定义组件来实现功能。
<ua-popup v-model="isVisibleConfirm" shadeClose="false" title="标题" xclose z-index="1001"
content="<div style='color:#ff557f;padding:20px 40px;'>预测未来的最好办法是自己亲手创造未来!</div>"
:btns="[
{text: '取消', click: handleCancel},
{text: '确定', style: 'color:#00aa00;', click: handleOk},
]"
/>
<script>
export default {
methods: {
handleOk() {
let $ua = this.$refs.uapopup
$ua.open({
content: '人生漫漫,且行且珍惜',
customStyle: {'background-color': 'rgba(170, 0, 127, 0.6)', 'color': '#fff'},
time: 3,
onClose() {
$ua.open({
type: 'android',
content: '<div style="color:#aa007f">一切都将一去杳然,任何人都无法将其捕获。</div>',
customStyle: {'width': '210px'},
btns: [
{
text: '关闭',
click() {
$ua.close()
}
},
{
text: '确定',
style: 'color:#00aa00;',
click() {
// ...
}
}
]
})
}
})
}
}
}
</script>
uniapp直播/短视频
项目中短视频页面,整体分为顶部导航栏、底部菜单栏、中间视频区域。
<view v-if="currentTab == 2" class="ua__tabcnt-recommend">
<swiper class="ua__vdplayer-swiper flex1" :current="currentVideo" vertical @change="handleSwipeVertical">
<swiper-item v-for="(item, index) in videoList" :key="index">
<!-- 视频模块 -->
<view class="ua__vdplayer-video flex1">
<video class="vdplayer" :id="'vdplayer' + index" :ref="'vdplayer' + index"
:src="item.src"
:controls="false" :loop="true" :show-center-play-btn="false" object-fit="fill"
:autoplay="index == currentVideo"
@play="isPlaying=true" @timeupdate="handleTimeUpdate"
:style="{'width': winWidth, 'height': winHeight}"
>
</video>
<view class="ua__vdplayer-playwrap" @click="handleVideoClicked"><view v-if="!isPlaying" class="ua__vdplayer-playbtn"><text class="iconfont">{{`\ue607`}}</text></view></view>
</view>
<!-- 信息模块 -->
<view class="ua__vdplayer-info flexbox flex-col">
<view class="flexbox flex-row flex-alignb">
<!-- //左侧信息 -->
<view class="vdinfo__left flex1">
<view class="ltitem uavatar flexbox flex-row">
<navigator url="#" class="flexbox flex-alignc flex-row"><image class="uimg" :src="item.avatar" /><text class="uname">{{item.author}}</text></navigator>
<view class="flexbox btn" :class="{'actived': item.isFollow}" @click="handleFollow(index)"><text class="btn-text">{{item.isFollow ? '已关注' : '关注'}}</text></view>
</view>
<view v-if="item.topic" class="ltitem flexbox flex-row">
<view class="kw" v-for="(kw, index2) in item.topic" :key="index2"><text class="lbl">#{{kw}}</text></view>
</view>
<view class="ltitem"><text class="desc">{{item.desc}}</text></view>
</view>
<!-- //右侧按钮 -->
<view class="vdinfo__right flexbox flex-col">
<view class="rtitem ball" v-if="item.goods&&item.goods.length > 0" @click="handleShowGoodsPopup(item.goods)"><text class="icon iconfont">{{`\ue734`}}</text></view>
<view class="rtitem" :class="{'isliked': item.isLike}" @click="handleLiked(index)"><text class="icon iconfont">{{`\ue635`}}</text><text class="num">{{item.likeNum+(item.isLike ? 1 : 0)}}</text></view>
<view class="rtitem" @click="showReplyPopup = true"><text class="icon iconfont">{{`\ue632`}}</text><text class="num">{{item.replyNum}}</text></view>
<view class="rtitem" @click="showSharePopup = true"><text class="icon iconfont">{{`\ue63b`}}</text><text class="num">{{item.shareNum}}</text></view>
</view>
</view>
</view>
</swiper-item>
</swiper>
<!-- 底部播放进度条 -->
<view class="ua__vdplayer-progress"><view class="bar" :style="{'width': progressBar+'px'}"></view></view>
</view>
<script>
const app = getApp()
import videoJSON from '@/mock/videolist.js'
export default {
data() {
return {
// 导航栏高度
customBarHeight: app.globalData.customBarH,
navbarBgcolor: '#21252b',
tabbarBgcolor: '#21252b',
tabNavLs: [
{label: '附近动态', badge: 5, lists: []},
{label: '关注', lists: []},
{label: '推荐', dot: true, lists: []},
],
// 当前选项卡
currentTab: 0,
// 当前视频索引
currentVideo: 0,
// 视频数据
videoList: videoJSON,
// 视频是否播放中
isPlaying: false,
// 点击次数
clickNum: 0,
// 视频播放进度条
progressBar: 0,
clickTimer: null,
// 屏幕宽高
winWidth: '',
winHeight: '',
popupGoodsList: [],
showGoodsPopup: false,
showReplyPopup: false,
showSharePopup: false,
}
},
watch: {
currentTab(val) {
this.changeTabPanel(val)
}
},
computed:{
customBarMargin() {
return `margin-top: ${this.customBarHeight}px`
}
},
created() {
// 引入iconfont字体
// #ifdef APP-NVUE
const domModule = weex.requireModule('dom')
domModule.addRule('fontFace', {
fontFamily: "nvueIcon",
'src': "url('/static/fonts/iconfont.ttf')"
});
// #endif
let wW = uni.getSystemInfoSync().windowWidth
let wH = uni.getSystemInfoSync().windowHeight
this.winWidth = `${wW}px`
this.winHeight = `${wH}px`
},
methods: {
// 长按动态
handleDynamicMenu(e) {
let points
// #ifndef APP-NVUE
points = [e.touches[0].clientX, e.touches[0].clientY]
// #endif
// #ifdef APP-NVUE
points = [e.touches[0].screenX, e.touches[0].screenY]
// #endif
this.$refs.uapopup.open({
type: 'contextmenu',
follow: points,
btns: [
{text: '不感兴趣'},
{text: '复制'},
{
text: '举报',
style: 'color:#f00;',
click: () => {
this.$refs.uapopup.close()
}
},
],
})
},
/* ++++++++++ { 视频播放模块 } ++++++++++ */
getVideoCtx() {
// return this.$refs['vdplayer' + this.currentVideo][0]
return uni.createVideoContext('vdplayer'+ this.currentVideo, this)
},
// 垂直滑动视频
handleSwipeVertical(e) {
let index = e.detail.current
this.progressBar = 0
this.isPlaying = false
let video = this.getVideoCtx()
if(!video) return
video.pause()
// 重新开始
video.seek(0)
this.currentVideo = index
// 自动播放
this.handlePlay()
},
handlePlay() {
let video = this.getVideoCtx()
if(!video) return
video.play()
this.isPlaying = true
},
handlePause() {
let video = this.getVideoCtx()
if(!video) return
video.pause()
this.isPlaying = false
},
// 点击视频(单击/双击)
handleVideoClicked() {
this.clickTimer && clearTimeout(this.clickTimer)
this.clickNum++
this.clickTimer = setTimeout(() => {
if(this.clickNum >= 2) {
console.log('你双击了')
}else {
console.log('你单击了')
if(this.isPlaying) {
this.handlePause()
}else {
this.handlePlay()
}
}
this.clickNum = 0
}, 250)
},
...
}
}
</script>
okey,基于uni-app开发原生短视频/直播项目就分享到这里。
链接:https://juejin.cn/post/7010195797174124557/
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

uni-app跟rn和flutter没什么好比的,就比比quasar
uni-app的产品经理喜欢拿uni-app和rn以及flutter比较,社区里也有一篇:
https://ask.dcloud.net.cn/article/36083
其实对于开发者来讲根本是不同的东西。
对标rn的是weex,weex以前被阿里扔到apache没人管,现在好像又回到阿里那边维护了,但还是不上心,没有rn社区那种活跃。
对标flutter的是...目前还没有。
weex的不活跃也直接导致了uni-app的nvue案例或者nvue ui项目很少。
国外更接近uni-app的产品架构的是quasar framework。同样的基于vue,同样的自带ui框架。uni-app的优点就是国内各平台小程序支持,多了个weex编译,还有插件市场可以用,其他都落后。
quasar 在国内用的很少,如果要后端,后端用的是firebase插件,firebase同样没有对手,不过因为集成了一大堆谷歌服务,国内并不能用。
而且firebase这个理念和云计算的serverless又不太一样,等于是把所有的后端服务都集中优化了,和分开的云函数,云数据库,云存储比更省心。而且现在阿里云这边这三个云产品相互访问还都是通过外网,速度慢,动不动就报错。腾讯云这边仗着小程序的流量还急着收费。
uni-app良心的地方挺良心,腾讯云,阿里云的资源没怎么加价卖,也是为数不多的国内开源项目的小公司。不过uni-ad广告那一层网上有传闻是抽成而且不公开的,自己调用广告联盟会被报非法应用。这个问题吧,我觉得dcloud愿意公开讲清楚我别的都给你免费用,就广告里面抽多少%,我觉得也没什么大不了的,就像epic的虚幻引擎那样。不然刚开始我都怀疑dcloud到底该怎么样变现养活自己的。
国内有另外一家开源的小公司叫edusoho,专做教育培训的开源项目,他们的盈利方式是打包云服务卖,当然云服务相关的源代码是不开源的。现在也好好活着,而且依旧开源,我觉的这个比起暗暗的抽广告费要合理一些,既然都是阿里云,腾讯云的下游,自己的产品在集成的云服务上加点钱卖。
uni-app的产品经理喜欢拿uni-app和rn以及flutter比较,社区里也有一篇:
https://ask.dcloud.net.cn/article/36083
其实对于开发者来讲根本是不同的东西。
对标rn的是weex,weex以前被阿里扔到apache没人管,现在好像又回到阿里那边维护了,但还是不上心,没有rn社区那种活跃。
对标flutter的是...目前还没有。
weex的不活跃也直接导致了uni-app的nvue案例或者nvue ui项目很少。
国外更接近uni-app的产品架构的是quasar framework。同样的基于vue,同样的自带ui框架。uni-app的优点就是国内各平台小程序支持,多了个weex编译,还有插件市场可以用,其他都落后。
quasar 在国内用的很少,如果要后端,后端用的是firebase插件,firebase同样没有对手,不过因为集成了一大堆谷歌服务,国内并不能用。
而且firebase这个理念和云计算的serverless又不太一样,等于是把所有的后端服务都集中优化了,和分开的云函数,云数据库,云存储比更省心。而且现在阿里云这边这三个云产品相互访问还都是通过外网,速度慢,动不动就报错。腾讯云这边仗着小程序的流量还急着收费。
uni-app良心的地方挺良心,腾讯云,阿里云的资源没怎么加价卖,也是为数不多的国内开源项目的小公司。不过uni-ad广告那一层网上有传闻是抽成而且不公开的,自己调用广告联盟会被报非法应用。这个问题吧,我觉得dcloud愿意公开讲清楚我别的都给你免费用,就广告里面抽多少%,我觉得也没什么大不了的,就像epic的虚幻引擎那样。不然刚开始我都怀疑dcloud到底该怎么样变现养活自己的。
国内有另外一家开源的小公司叫edusoho,专做教育培训的开源项目,他们的盈利方式是打包云服务卖,当然云服务相关的源代码是不开源的。现在也好好活着,而且依旧开源,我觉的这个比起暗暗的抽广告费要合理一些,既然都是阿里云,腾讯云的下游,自己的产品在集成的云服务上加点钱卖。
收起阅读 »
nvue遇到text是块元素,解决文字嵌套问题
<!-- nvue不支持span 解决方案 -->
<view class='reply'>
<view class='reply-detail'>
<text class="reply-detail-username" v-for="i in '刘芸'">{{i}}</text>
<text class="reply-detail-content" v-for='i in ":"'>{{i+' '}}</text>
<text class="reply-detail-content" v-for='it in "都说JAVA开发很赚钱, 我苦心专研了两年,其中参加了不少的培训。"'>{{it}}</text>
</view>
</view>
<style lang='scss'>
.reply {
width: 595upx;
min-height: 75upx;
margin: 15upx 30upx 0upx 124upx;
padding: 22upx 24upx;
background-color: #F4F4F4;
border-radius: 4upx;
}
.reply-detail {
font-size: 29upx;
line-height: 48upx;
display: flex;
flex-direction: row;
color: #343434;
flex-wrap: wrap;
>.reply-detail-username{
color: #2199D1;
}
>.reply-detail-content{}
}
</style>
完美解决nvue中的文字无法嵌套

<!-- nvue不支持span 解决方案 -->
<view class='reply'>
<view class='reply-detail'>
<text class="reply-detail-username" v-for="i in '刘芸'">{{i}}</text>
<text class="reply-detail-content" v-for='i in ":"'>{{i+' '}}</text>
<text class="reply-detail-content" v-for='it in "都说JAVA开发很赚钱, 我苦心专研了两年,其中参加了不少的培训。"'>{{it}}</text>
</view>
</view>
<style lang='scss'>
.reply {
width: 595upx;
min-height: 75upx;
margin: 15upx 30upx 0upx 124upx;
padding: 22upx 24upx;
background-color: #F4F4F4;
border-radius: 4upx;
}
.reply-detail {
font-size: 29upx;
line-height: 48upx;
display: flex;
flex-direction: row;
color: #343434;
flex-wrap: wrap;
>.reply-detail-username{
color: #2199D1;
}
>.reply-detail-content{}
}
</style>
完美解决nvue中的文字无法嵌套

引用超级全局组件方案
vue-inset-loader
编译阶段在sfc模板指定位置插入自定义内容,适用于webpack构建的vue应用,常用于小程序需要全局引入组件的场景。(由于小程序没有开放根标签,没有办法在根标签下追加全局标签,所以要使用组件必须在当前页面引入组件标签)
github:https://github.com/1977474741/vue-inset-loader
用得上顺便点个star
第一步 安装
npm install vue-inset-loader --save-dev
第二步 vue.config.js注入loader
module: {
rules: [
{
test: /\.vue$/,
use:{
loader: "vue-inset-loader"
// // 针对Hbuilder工具创建的uni-app项目
// loader: path.resolve(__dirname,"./node_modules/vue-inset-loader")
}
}
]
},
// 支持自定义pages.json文件路径
// options: {
// pagesPath: path.resolve(__dirname,'./src/pages.json')
// }
第三步 pages.json配置文件中添加insetLoader
"insetLoader": {
"config":{
"confirm": "<BaseConfirm ref='confirm'></BaseConfirm>",
"abc": "<BaseAbc ref='BaseAbc'></BaseAbc>"
},
// 全局配置
"label":["confirm"],
"rootEle":"div"
},
"pages": [
{
"path": "pages/tabbar/index/index",
"style": {
"navigationBarTitleText": "测试页面",
// 单独配置,用法跟全局配置一致,优先级高于全局
"label": ["confirm","abc"],
"rootEle":"div"
}
},
]
配置说明
config
(default:{}
)
定义标签名称和内容的键值对label
(default:[]
)
需要全局引入的标签,打包后会在所有页面引入此标签-
rootEle
(default:"div"
)
根元素的标签类型,缺省值为div,支持正则,比如匹配任意标签 ".*"✔
label
和rootEle
支持在单独页面的style里配置,优先级高于全局配置
vue-inset-loader
编译阶段在sfc模板指定位置插入自定义内容,适用于webpack构建的vue应用,常用于小程序需要全局引入组件的场景。(由于小程序没有开放根标签,没有办法在根标签下追加全局标签,所以要使用组件必须在当前页面引入组件标签)
github:https://github.com/1977474741/vue-inset-loader
用得上顺便点个star
第一步 安装
npm install vue-inset-loader --save-dev
第二步 vue.config.js注入loader
module: {
rules: [
{
test: /\.vue$/,
use:{
loader: "vue-inset-loader"
// // 针对Hbuilder工具创建的uni-app项目
// loader: path.resolve(__dirname,"./node_modules/vue-inset-loader")
}
}
]
},
// 支持自定义pages.json文件路径
// options: {
// pagesPath: path.resolve(__dirname,'./src/pages.json')
// }
第三步 pages.json配置文件中添加insetLoader
"insetLoader": {
"config":{
"confirm": "<BaseConfirm ref='confirm'></BaseConfirm>",
"abc": "<BaseAbc ref='BaseAbc'></BaseAbc>"
},
// 全局配置
"label":["confirm"],
"rootEle":"div"
},
"pages": [
{
"path": "pages/tabbar/index/index",
"style": {
"navigationBarTitleText": "测试页面",
// 单独配置,用法跟全局配置一致,优先级高于全局
"label": ["confirm","abc"],
"rootEle":"div"
}
},
]
配置说明
config
(default:{}
)
定义标签名称和内容的键值对label
(default:[]
)
需要全局引入的标签,打包后会在所有页面引入此标签-
rootEle
(default:"div"
)
根元素的标签类型,缺省值为div,支持正则,比如匹配任意标签 ".*"✔
label
和rootEle
支持在单独页面的style里配置,优先级高于全局配置

uniapp开发抖音团购模板webpack插件
uniapp-to-group
uniapp-to-group
是 uni-app
开发字节小程序的时候,使用抖音团购模板的 webpack
转换插件
HBuilder X
同样适用,编辑器需要在 3.2.3.20210825 及以上
示例
HBuilder X
在项目跟目录添加一个 vue.config.js
文件
npm i uniapp-to-group -D
or
yarn add uniapp-to-group -D
// vue.config.js
const UniappToGroup = require('uniapp-to-group')
module.exports = {
configureWebpack: {
plugins: [
new UniappToGroup({
// 对应 package.json 中引入插件步骤
package: {
'ttPlugins': {
'dependencies': {
'poi-group-buy-plugin': {
'version': '1.0.0',
'isDynamic': true
}
}
}
},
app: {
'pages': [
// 手动配置商品插件中的商品详情页面
'ext://poi-group-buy-plugin/detail'
],
'fallbackPluginPages': {
// 这里的 key 是小程序原来的商品详情页(下面只是示意),value 可以就用这个值
'product/detail/index': 'ext://poi-group-buy-plugin/detail'
}
}
})
]
}
}
uniapp-to-group
uniapp-to-group
是 uni-app
开发字节小程序的时候,使用抖音团购模板的 webpack
转换插件
HBuilder X
同样适用,编辑器需要在 3.2.3.20210825 及以上
示例
HBuilder X
在项目跟目录添加一个 vue.config.js
文件
npm i uniapp-to-group -D
or
yarn add uniapp-to-group -D
// vue.config.js
const UniappToGroup = require('uniapp-to-group')
module.exports = {
configureWebpack: {
plugins: [
new UniappToGroup({
// 对应 package.json 中引入插件步骤
package: {
'ttPlugins': {
'dependencies': {
'poi-group-buy-plugin': {
'version': '1.0.0',
'isDynamic': true
}
}
}
},
app: {
'pages': [
// 手动配置商品插件中的商品详情页面
'ext://poi-group-buy-plugin/detail'
],
'fallbackPluginPages': {
// 这里的 key 是小程序原来的商品详情页(下面只是示意),value 可以就用这个值
'product/detail/index': 'ext://poi-group-buy-plugin/detail'
}
}
})
]
}
}
收起阅读 »

uni-app 应用换肤功能
应用换肤这个 “毫无卵用” 又浪费时间的的功能却是许多产品经理们喜欢玩的(某信国民应用都不做的功能)。
首先我坦白,我TM就从没用应用换皮这种功能,其次我 氪金 跟有没有换皮这个功能毫无关系,真是受够了这种沙雕产品,虽然我在产品经理提出这个需求之前已经解决了产品经理本人。但是在这里先解救一下正在受难中的卑微的程序员鄙视链底端的 web前端。
实现方式:CSS变量 + Vuex
具体的源码下载请前往我的 uni-app 插件主页下载
源码地址:https://ext.dcloud.net.cn/plugin?id=6215
在线体验:https://mydarling.gitee.io/uniapp-extend
效果预览
扫码预览 H5 版
我的博客原文:https://blog.csdn.net/u013350495/article/details/120343941
应用换肤这个 “毫无卵用” 又浪费时间的的功能却是许多产品经理们喜欢玩的(某信国民应用都不做的功能)。
首先我坦白,我TM就从没用应用换皮这种功能,其次我 氪金 跟有没有换皮这个功能毫无关系,真是受够了这种沙雕产品,虽然我在产品经理提出这个需求之前已经解决了产品经理本人。但是在这里先解救一下正在受难中的卑微的程序员鄙视链底端的 web前端。
实现方式:CSS变量 + Vuex
具体的源码下载请前往我的 uni-app 插件主页下载
源码地址:https://ext.dcloud.net.cn/plugin?id=6215
在线体验:https://mydarling.gitee.io/uniapp-extend
效果预览
扫码预览 H5 版
我的博客原文:https://blog.csdn.net/u013350495/article/details/120343941
收起阅读 »
【简单原理】满足swiper组件的高度自适应,随内容<图片>高度变化而变化
项目中遇到,研究一下,特此记录,不喜勿喷!
此方法适用于swiper图片宽度铺满屏幕,可根据需求自定义调整
原理:获取图片高度,根据图片宽高比例,取得swiper在宽度100vw情况下的高度,实现swiper高度根据内容图片高度适应
template:
...
<swiper class="swiper" :style="`height:${swiperheight}`">
<swiper-item class="swiperitem" v-for="(item,index) in imgList" :key="index" >
</swiper-item>
</swiper>
...
js
export default {
data() {
return {
swiperheight:"0px",
imgList:[...]
}
},
onLoad() {
this.getswiperImageInfo()
},
methods: {
getswiperImageInfo(src){
uni.getImageInfo({
src: this.imgList[0].img,
success: (res) => {
console.log(res)
let endwidth = res.width/res.height
this.swiperheight = 100/endwidth+"vw"
},
fail: (err) => {
console.log(err)
}
});
}
}
}
css
.swiper{
width: 100vw;
}
项目中遇到,研究一下,特此记录,不喜勿喷!
此方法适用于swiper图片宽度铺满屏幕,可根据需求自定义调整
原理:获取图片高度,根据图片宽高比例,取得swiper在宽度100vw情况下的高度,实现swiper高度根据内容图片高度适应
template:
...
<swiper class="swiper" :style="`height:${swiperheight}`">
<swiper-item class="swiperitem" v-for="(item,index) in imgList" :key="index" >
</swiper-item>
</swiper>
...
js
export default {
data() {
return {
swiperheight:"0px",
imgList:[...]
}
},
onLoad() {
this.getswiperImageInfo()
},
methods: {
getswiperImageInfo(src){
uni.getImageInfo({
src: this.imgList[0].img,
success: (res) => {
console.log(res)
let endwidth = res.width/res.height
this.swiperheight = 100/endwidth+"vw"
},
fail: (err) => {
console.log(err)
}
});
}
}
}
css
.swiper{
width: 100vw;
}
收起阅读 »

新安装hbuilder内置浏览器运行默认demo报错 [Vue warn]: Unknown custom element: <App>
最近在一个电脑上新安装了hbuilderX,下面是问题表现
-
新建了一个demo,点击内置浏览器运行就直接报错
[Vue warn]: Unknown custom element: <App> - did you register the component correctly? For recursive components, make sure to provide the "name" option. -
. 然后选择运行在微信开发者工具也会报错:
ERROR TypeError: Cannot read property 'id' of undefined
00:01:20.814 TypeError: Cannot read property 'id' of undefined
00:01:20.818 at generateComponent (C:\Users\Administrator\Desktop\HBuilderX\plugins\uniapp-cli\node_modules\@dcloudio\webpack-uni-mp-loader\lib\plugin\generate-component.js:71:110) -
选择运行在真机上就会报无法独立运行此app的错误。
然后就非常奇怪,网上搜了很多例子,看到有一些人也跟我出现了同样的问题,但是都没有解决。新建任何demo运行都是这个错误,并没有人能够解决成功。
后来我试了各种方式都不行。这个demo也能够在正确的位置找到,这就奇了怪了。
试了各种网上说的奇怪的方法之后都没有效果,但是并没有放弃,我又新建了一个官方demo,突发奇想的把新建demo时候选择project的位置改了一下,改到了桌面上,这次一运行竟然神奇的成功了。
我个人推测可能是跟我使用过360的c盘搬家有关系,因为后来我看到hbuilder里面打印的log有360的某某路径指向了360的c盘搬家的位置(这个位置里面有Administrator的那个文件夹用来放用户文件的,而hbuilder默认创建项目的文件夹名字就是这个名字)。可能就是这样子,导致hbuilder运行项目的时候被指向了一个奇怪的路径,导致运行失败了。
最近在一个电脑上新安装了hbuilderX,下面是问题表现
-
新建了一个demo,点击内置浏览器运行就直接报错
[Vue warn]: Unknown custom element: <App> - did you register the component correctly? For recursive components, make sure to provide the "name" option. -
. 然后选择运行在微信开发者工具也会报错:
ERROR TypeError: Cannot read property 'id' of undefined
00:01:20.814 TypeError: Cannot read property 'id' of undefined
00:01:20.818 at generateComponent (C:\Users\Administrator\Desktop\HBuilderX\plugins\uniapp-cli\node_modules\@dcloudio\webpack-uni-mp-loader\lib\plugin\generate-component.js:71:110) -
选择运行在真机上就会报无法独立运行此app的错误。
然后就非常奇怪,网上搜了很多例子,看到有一些人也跟我出现了同样的问题,但是都没有解决。新建任何demo运行都是这个错误,并没有人能够解决成功。
后来我试了各种方式都不行。这个demo也能够在正确的位置找到,这就奇了怪了。
试了各种网上说的奇怪的方法之后都没有效果,但是并没有放弃,我又新建了一个官方demo,突发奇想的把新建demo时候选择project的位置改了一下,改到了桌面上,这次一运行竟然神奇的成功了。
我个人推测可能是跟我使用过360的c盘搬家有关系,因为后来我看到hbuilder里面打印的log有360的某某路径指向了360的c盘搬家的位置(这个位置里面有Administrator的那个文件夹用来放用户文件的,而hbuilder默认创建项目的文件夹名字就是这个名字)。可能就是这样子,导致hbuilder运行项目的时候被指向了一个奇怪的路径,导致运行失败了。
收起阅读 »
Facebook登录开通指南
开通条件
1.海外网络环境
2.Facebook账号(Facebook登录页面)
创建应用
1.打开Facebook开发者中心
2.点击右上角"我的应用"
3.进入应用管理界面,点击"创建应用"
4.根据需要选择应用产品的类型(应用类型详见"详细了解应用类型"),然后点击继续
5.填写应用信息
6.创建完成后即可获取应用的应用编号(即appID)
7.为应用添加登录功能
设置登录-iOS
1.我的应用--设置--基本,选择添加平台,选择iOS
2.填写信息保存即可
设置登录-Android
我的应用--设置--基本,选择添加平台
选择android平台,应用商店选择Google Play
填写必要的包名和散列信息,类名是固定的。如图
散列的获取方法,参考文档:
https://developers.facebook.com/docs/facebook-login/android 第六小节
如果获取到的散列位数不对,需要找台linux/mac 计算机。
使用下面的命令获取
keytool -exportcert -alias hbuilder -keystore ./HBuilder.keystore | openssl dgst -sha1 -binary | openssl base64
应用权限
使用Facebook登录需开启"public_profile"以及"email"的访问权限
点击"应用审核"-"权限和功能",开启"public_profile"以及"email"的高级访问权限
开通条件
1.海外网络环境
2.Facebook账号(Facebook登录页面)
创建应用
1.打开Facebook开发者中心
2.点击右上角"我的应用"
3.进入应用管理界面,点击"创建应用"
4.根据需要选择应用产品的类型(应用类型详见"详细了解应用类型"),然后点击继续
5.填写应用信息
6.创建完成后即可获取应用的应用编号(即appID)
7.为应用添加登录功能
设置登录-iOS
1.我的应用--设置--基本,选择添加平台,选择iOS
2.填写信息保存即可
设置登录-Android
我的应用--设置--基本,选择添加平台
选择android平台,应用商店选择Google Play
填写必要的包名和散列信息,类名是固定的。如图
散列的获取方法,参考文档:
https://developers.facebook.com/docs/facebook-login/android 第六小节
如果获取到的散列位数不对,需要找台linux/mac 计算机。
使用下面的命令获取
keytool -exportcert -alias hbuilder -keystore ./HBuilder.keystore | openssl dgst -sha1 -binary | openssl base64
应用权限
使用Facebook登录需开启"public_profile"以及"email"的访问权限
点击"应用审核"-"权限和功能",开启"public_profile"以及"email"的高级访问权限