HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

uni-app提供开箱即用的SSR支持

ssr 服务端渲染 SEO

SSR(服务端渲染)可以给SPA站点带来两大核心优势:

  • 更好的SEO
  • 更快的首屏渲染

很多uni-app开发者都在积极尝试SSR,但大多没入门就放弃了,原因在于SSR颇高的技术门槛。

vue.js 官网在介绍 SSR 章节时的描述如下:

本指南将会非常深入,并且假设你已经熟悉 Vue.js 本身,并且具有 Node.js 和 webpack 的相当不错的应用经验。

故很多uni-app开发者在社区中呼吁DCloud官方提供更为简洁、开箱即用的 uni-app SSR 方案。

uni-app团队近期在完成 vue 3.0 的全平台升级后,基于vue 3.0 + uniCloud,发布了开箱即用的 SSR 支持,我们称其为uniCloud版SSR

这是一个uniCloud版的SSR示例:news.dcloud.io是基于uni-app & uniCloud 开发的新闻系统。通过审查元素会发现,新闻列表数据包含在服务端下发的源码中,而不是客户端Ajax请求所得。

uniCloud版的SSR实现的较为简单,且和HBuilderX做了深度集成,你可以按照如下步骤快速上手:

步骤一:调整代码适配服务端运行环境

  1. 生命周期:uni-app的生命周期钩子函数中,页面onLoad、组件beforeCreatecreated 会在服务器端渲染 (SSR) 过程中被调用,你需要检查原项目代码中获取数据的时机;
  2. 特定平台API:若直接使用了如 windowdocument,这类仅浏览器支持的全局变量,则会在云端 Node.js 中执行时抛出错误;
  3. 数据预取:<uniCloud-db>组件天然支持SSR,无需调整代码,推荐使用<uniCloud-db>查询数据库。如果你未使用<uniCloud-db>组件,则可使用serverPrefetch来实现服务器端数据获取,使用@dcloudio/uni-app提供的ssrRef或Vue.js官方的Vuex来实现状态同步;

更多详细信息及示例代码,参考:https://uniapp.dcloud.net.cn/collocation/ssr

步骤二:编译发行

通过HBuilderX的发行菜单->网站 PC-Web或手机H5、勾选ssr、勾选将编译后的资源部署在uniCloud前端网页托管

这个过程,对开发者来说很简单,只需要点击按钮即可,实际上HBuilderX在背后做了大量工作,包括:

  1. 编译uni-app项目,分别生成Server BundleClient Bundle
  2. Client Bundle上传到uniCloud前端网页托管中
  3. Server Bundle作为uni-ssr云函数资源,编译并上传到uniCloud服务空间

步骤三:配置 uni-ssr 云函数的URL化路径

uni-ssr云函数绑定自定义域名,然后在浏览器中访问该域名,你就可以获得服务端渲染的页面了。

至此,uniCloud版SSR开发部署工作结束,是不是比原来简单多了?

总结

Vue.js 官网及社区很多文档,介绍SSR时都会提到,SSR是把双刃剑,优点缺点都很明显(2优3缺,缺大于优)。但经过HBuilderX & uniCloud加持的SSR,则完美解决了SSR的部分缺点,将其由缺转优,变成4优1缺,实现优大于缺。具体对比如下:

欢迎大家使用uniCloud版SSR

关联阅读:uni-app官网ssr介绍

继续阅读 »

SSR(服务端渲染)可以给SPA站点带来两大核心优势:

  • 更好的SEO
  • 更快的首屏渲染

很多uni-app开发者都在积极尝试SSR,但大多没入门就放弃了,原因在于SSR颇高的技术门槛。

vue.js 官网在介绍 SSR 章节时的描述如下:

本指南将会非常深入,并且假设你已经熟悉 Vue.js 本身,并且具有 Node.js 和 webpack 的相当不错的应用经验。

故很多uni-app开发者在社区中呼吁DCloud官方提供更为简洁、开箱即用的 uni-app SSR 方案。

uni-app团队近期在完成 vue 3.0 的全平台升级后,基于vue 3.0 + uniCloud,发布了开箱即用的 SSR 支持,我们称其为uniCloud版SSR

这是一个uniCloud版的SSR示例:news.dcloud.io是基于uni-app & uniCloud 开发的新闻系统。通过审查元素会发现,新闻列表数据包含在服务端下发的源码中,而不是客户端Ajax请求所得。

uniCloud版的SSR实现的较为简单,且和HBuilderX做了深度集成,你可以按照如下步骤快速上手:

步骤一:调整代码适配服务端运行环境

  1. 生命周期:uni-app的生命周期钩子函数中,页面onLoad、组件beforeCreatecreated 会在服务器端渲染 (SSR) 过程中被调用,你需要检查原项目代码中获取数据的时机;
  2. 特定平台API:若直接使用了如 windowdocument,这类仅浏览器支持的全局变量,则会在云端 Node.js 中执行时抛出错误;
  3. 数据预取:<uniCloud-db>组件天然支持SSR,无需调整代码,推荐使用<uniCloud-db>查询数据库。如果你未使用<uniCloud-db>组件,则可使用serverPrefetch来实现服务器端数据获取,使用@dcloudio/uni-app提供的ssrRef或Vue.js官方的Vuex来实现状态同步;

更多详细信息及示例代码,参考:https://uniapp.dcloud.net.cn/collocation/ssr

步骤二:编译发行

通过HBuilderX的发行菜单->网站 PC-Web或手机H5、勾选ssr、勾选将编译后的资源部署在uniCloud前端网页托管

这个过程,对开发者来说很简单,只需要点击按钮即可,实际上HBuilderX在背后做了大量工作,包括:

  1. 编译uni-app项目,分别生成Server BundleClient Bundle
  2. Client Bundle上传到uniCloud前端网页托管中
  3. Server Bundle作为uni-ssr云函数资源,编译并上传到uniCloud服务空间

步骤三:配置 uni-ssr 云函数的URL化路径

uni-ssr云函数绑定自定义域名,然后在浏览器中访问该域名,你就可以获得服务端渲染的页面了。

至此,uniCloud版SSR开发部署工作结束,是不是比原来简单多了?

总结

Vue.js 官网及社区很多文档,介绍SSR时都会提到,SSR是把双刃剑,优点缺点都很明显(2优3缺,缺大于优)。但经过HBuilderX & uniCloud加持的SSR,则完美解决了SSR的部分缺点,将其由缺转优,变成4优1缺,实现优大于缺。具体对比如下:

欢迎大家使用uniCloud版SSR

关联阅读:uni-app官网ssr介绍

收起阅读 »

云打包是真慢啊 官方能不能好好优化下体验啊

云打包

[HBuilder] 12:28:27.014 时间: 2021-09-29 11:59:09 类型: Android 公共测试证书 队列中

[HBuilder] 12:28:27.014 时间: 2021-09-29 11:59:09 类型: Android 公共测试证书 队列中

普通项目使用uniCloud

uniCloud

uinCloud admin项目实在是太丑,而且很多功能不全,而一般的项目不能直接用,毕竟有些后台还不是用vue,用云函数URL化太麻烦,而且不能用<unicloud-db> 组件,发挥不了uniCloud的优势。
这里我找到一个后门,可以让普通项目也用上uniCloud的功能,如直接在项目中直接用 uniCloud.callFunction()、uniCloud.database()、uniCloud.uploadFile()、直接引用<unicloud-db>,这样相当于是用到了uniCloud的优势,而对本身的项目又不需要做过多的修改就能直接兼容。
具体的方法就是建一个uni-app项目,在main.js文件中,只加上一句,window.uniCloud=uniCloud,然后打包,一般会生成三个JS文件,可以上传到CDN也可以直接放到项目里直接引用。
这里有几点要说一下,用VUE3打包,生成的是模块形式,代码较小,用VUE2打包生成文件会较大。还有就是一定要在项目开始前就引入文件。生面的文件可以适当的修改。
到此为止, uniCloud.callFunction()、uniCloud.database()、uniCloud.uploadFile()就可以直接在项目中使用。
<unicloud-db>则需要手动加一下。到uni-app的源码里面直接搜unicloud-db 找到对应的VUE文件,搜uni.强窗什么的全部删除或自己根据项目重写,可直接添加到全局也可以局部引用。对于VUE3项目注意一下销毁的生命周期。

继续阅读 »

uinCloud admin项目实在是太丑,而且很多功能不全,而一般的项目不能直接用,毕竟有些后台还不是用vue,用云函数URL化太麻烦,而且不能用<unicloud-db> 组件,发挥不了uniCloud的优势。
这里我找到一个后门,可以让普通项目也用上uniCloud的功能,如直接在项目中直接用 uniCloud.callFunction()、uniCloud.database()、uniCloud.uploadFile()、直接引用<unicloud-db>,这样相当于是用到了uniCloud的优势,而对本身的项目又不需要做过多的修改就能直接兼容。
具体的方法就是建一个uni-app项目,在main.js文件中,只加上一句,window.uniCloud=uniCloud,然后打包,一般会生成三个JS文件,可以上传到CDN也可以直接放到项目里直接引用。
这里有几点要说一下,用VUE3打包,生成的是模块形式,代码较小,用VUE2打包生成文件会较大。还有就是一定要在项目开始前就引入文件。生面的文件可以适当的修改。
到此为止, uniCloud.callFunction()、uniCloud.database()、uniCloud.uploadFile()就可以直接在项目中使用。
<unicloud-db>则需要手动加一下。到uni-app的源码里面直接搜unicloud-db 找到对应的VUE文件,搜uni.强窗什么的全部删除或自己根据项目重写,可直接添加到全局也可以局部引用。对于VUE3项目注意一下销毁的生命周期。

收起阅读 »

针对升级到HbuilderX-3.2.9中 Android 推送权限判断是否开启失效的解决

新版本 升级更新

该文章是针对 升级到3.2.9后该推送权限功能失效的问题
前提代码:可以查看是否有相应匹配的代码

var isOn = undefined;  
var main = plus.android.runtimeMainActivity();  
var pkName = main.getPackageName();  
var uid = main.getApplicationInfo().plusGetAttribute("uid");  
var NotificationManagerCompat = plus.android.importClass("androidx.core.app.NotificationManagerCompat");  
// var NotificationManagerCompat = plus.android.importClass("android.support.v4.app.NotificationManagerCompat");  
isOn = NotificationManagerCompat.from(main).areNotificationsEnabled();

3.2.9更新中有一条:【重要】Android平台 新增 Android Support Library 升级迁移到 AndroidX

所以将 "android.support.v4.app.NotificationManagerCompat" 替换为 "androidx.core.app.NotificationManagerCompat",就可以正常运行

继续阅读 »

该文章是针对 升级到3.2.9后该推送权限功能失效的问题
前提代码:可以查看是否有相应匹配的代码

var isOn = undefined;  
var main = plus.android.runtimeMainActivity();  
var pkName = main.getPackageName();  
var uid = main.getApplicationInfo().plusGetAttribute("uid");  
var NotificationManagerCompat = plus.android.importClass("androidx.core.app.NotificationManagerCompat");  
// var NotificationManagerCompat = plus.android.importClass("android.support.v4.app.NotificationManagerCompat");  
isOn = NotificationManagerCompat.from(main).areNotificationsEnabled();

3.2.9更新中有一条:【重要】Android平台 新增 Android Support Library 升级迁移到 AndroidX

所以将 "android.support.v4.app.NotificationManagerCompat" 替换为 "androidx.core.app.NotificationManagerCompat",就可以正常运行

收起阅读 »

nvue+uniapp模仿抖音APP实例|uni-app直播/短视频

uni_app项目 uniapp插件 uniapp

之前有给大家分享一个electron-webosx仿MAC桌面管理系统。今天分享的是全端uniapp小视频/直播实例项目。

uniapp-ttLive短视频 一款基于uni-app+nvue+uview-ui等技术开发的多功能跨端直播+短视频项目。流畅的上下滑动体验、支持全景式悬浮沉浸效果。可暂停/播放小视频、直播送礼物、聊天互动等功能。

vue3.x+electron13+vite2模仿macui桌面管理框架

img

使用技术

  • 编码器/技术:HbuilderX3.1.21+Uniapp+Nvue+Vuex+Uapopup
  • UI组件库:uView-ui / uni-ui
  • 矢量图标库:iconfont字体图标
  • 弹窗组件:UApopup 基于uni-app封装跨端弹窗组件
  • 自定义导航条+底部菜单栏
  • 编译支持:H5+小程序+APP端

img

基于 HbuilderX3.1.21 编码器开发,搭配nvue原生页面解决video层级过高问题,自定义 ua-navbar ua-tabbar ua-popup 组件全面支持Nvue页面。搭配uview-ui和uni-ui组件库。可编译至h5+小程序+APP端。

img

项目结构目录

img

img

img

img

img

img

img

img

img

img

img

img

img

img

img

img

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全端自定义组件

img

img

项目中用到的顶部导航栏、底部菜单栏及弹窗均是自定义组件来实现功能。

<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自定义组件之导航栏+菜单栏

uniapp全端弹窗插件|uni-app模态弹框

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桌面管理框架

img

使用技术

  • 编码器/技术:HbuilderX3.1.21+Uniapp+Nvue+Vuex+Uapopup
  • UI组件库:uView-ui / uni-ui
  • 矢量图标库:iconfont字体图标
  • 弹窗组件:UApopup 基于uni-app封装跨端弹窗组件
  • 自定义导航条+底部菜单栏
  • 编译支持:H5+小程序+APP端

img

基于 HbuilderX3.1.21 编码器开发,搭配nvue原生页面解决video层级过高问题,自定义 ua-navbar ua-tabbar ua-popup 组件全面支持Nvue页面。搭配uview-ui和uni-ui组件库。可编译至h5+小程序+APP端。

img

项目结构目录

img

img

img

img

img

img

img

img

img

img

img

img

img

img

img

img

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全端自定义组件

img

img

项目中用到的顶部导航栏、底部菜单栏及弹窗均是自定义组件来实现功能。

<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自定义组件之导航栏+菜单栏

uniapp全端弹窗插件|uni-app模态弹框

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

DCloud uniapp

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,专做教育培训的开源项目,他们的盈利方式是打包云服务卖,当然云服务相关的源代码是不开源的。现在也好好活着,而且依旧开源,我觉的这个比起暗暗的抽广告费要合理一些,既然都是阿里云,腾讯云的下游,自己的产品在集成的云服务上加点钱卖。

收起阅读 »

快手支付 担保支付

快手小程序

用的还是微信H5支付

用的还是微信H5支付

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+'&nbsp;'}}</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+'&nbsp;'}}</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,支持正则,比如匹配任意标签 ".*"

    labelrootEle 支持在单独页面的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,支持正则,比如匹配任意标签 ".*"

    labelrootEle 支持在单独页面的style里配置,优先级高于全局配置

收起阅读 »

uniapp开发抖音团购模板webpack插件

uniapp

uniapp-to-group

uniapp-to-groupuni-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-groupuni-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

收起阅读 »