zoukankan      html  css  js  c++  java
  • 基于uni-app开发微信小程序__手牵手带你开发【懂你找图】项目

    前戏

    某一天的夜里,敲完了代码之后便直接倒在床上睡着了,醒来时只记得梦里的一句话:“想要成为高手,就必须要大量实践,大量做项目,必须要把自己不会的东西全部吃透,不要得过且过。”,猛然想起是一位大神前辈对我说的,工作之后每天加班,回家之后就不想学习了,总想着一把锤子搞定所有钉子,这样是不行的,于是我就下了几百G的项目实战视频,有Vue、React、Node.js、Angular、Flutter、各个框架源码分析。。。我计划今年把它们全部干完,每做一个项目我都会写一篇博客来记录开发过程和收获,我想着通过大量的项目练习来让自己变成熟练工种,然后再去看源码就会比较轻松;接下来就让我牵着同学们的小手带你们开发一款基于uni-app的微信小程序项目,项目名字叫【懂你找图】。

    项目介绍

    做这个项目之前,同学们最好写过2-3个移动端的页面,有一定的JS基础,比如map,forEach函数的使用,Promise的使用,掌握Vue的基本语法,基本的生命周期,什么是Watch?怎么使用一个Component?子传父 / 父传子的实现方式。

    没有基础的同学也不要担心,可以跟着把项目写完,然后把不理解的地方单独抽出来,逐个学习,然后再把项目独立做一遍就完事了。

    这个是项目做完之后的效果:

    这个项目我会带领同学们写完首页的模块,其他模块由于API接口还没有写好,暂时不做,等以后写好了,我会马上更新,接下来我们就进入正片环节。

    1.项目准备

    1.1开发方式

    uni-app为我们提供2种开发方式:

    1.使用DCloud公司提供HBuilderX工具来快速开发;

    2.使用脚手架来快速开发(我们这次项目使用此方式);

    1.2脚手架搭建项目

    1.全局安装,如果你以前安装过就不需要重复安装了。

    npm install -g @vue/cli

    2.创建项目。

    vue create -p dcloudio/uni-preset-vue dnpicture

    3.启动项目(微信小程序)。

    npm run dev:mp-weixin

    4.在微信小程序开发者工具导入项目。

     注意导入项目的路径。

    1.3搭建过程中可能遇到的问题

    容易出现 vue 和 vue-template-complier版本不一致的问题。

     根据提示重新安装对应的vue版本即可 npm install vue@2.6.10,然后再重新运行项目 npm run dev:mp-weixin。

    1.4安装sass依赖

    npm install sass-loader node-sass

    2.项目搭建

    2.1新增tabbar页面

    页面名称 路径
    首页 home/index.vue
    横屏 horizontal/index.vue
    精美视频 video/index.vue
    搜索 search/index.vue
    我的 mine/index.vue

    新建完页面之后,我们再去pages.json文件里面添加页面路径和tabbar对应的图片和样式。

    {
        "pages": [{
                "path": "pages/home/index",
                "style": {
                    "navigationBarTitleText": "首页"
                }
            },
            {
                "path": "pages/horizontal/index",
                "style": {
                    "navigationBarTitleText": "横屏"
                }
            },
            {
                "path": "pages/video/index",
                "style": {
                    "navigationBarTitleText": "精美视频"
                }
            },
            {
                "path": "pages/search/index",
                "style": {
                    "navigationBarTitleText": "搜索"
                }
            },
            {
                "path": "pages/mine/index",
                "style": {
                    "navigationBarTitleText": "我的"
                }
            }
        ],
        "globalStyle": {
            "navigationBarTextStyle": "black",
            "navigationBarTitleText": "uni-app",
            "navigationBarBackgroundColor": "#F8F8F8",
            "backgroundColor": "#F8F8F8"
        },
        "tabBar": {
            "color": "#8a8a8a",
            "selectedColor": "#d4237a",
            "backgroundColor": "#fff",
            "position": "bottom",
            "borderStyle": "black",
            "list": [{
                    "pagePath": "pages/home/index",
                    "text": "首页",
                    "iconPath": "./static/icon/_home.png",
                    "selectedIconPath": "./static/icon/home.png"
                },
                {
                    "pagePath": "pages/horizontal/index",
                    "text": "横屏",
                    "iconPath": "./static/icon/_img.png",
                    "selectedIconPath": "./static/icon/img.png"
                },
                {
                    "pagePath": "pages/video/index",
                    "text": "精美视频",
                    "iconPath": "./static/icon/_videocamera.png",
                    "selectedIconPath": "./static/icon/videocamera.png"
                },
                {
                    "pagePath": "pages/search/index",
                    "text": "搜索",
                    "iconPath": "./static/icon/_search.png",
                    "selectedIconPath": "./static/icon/search.png"
                },
                {
                    "pagePath": "pages/mine/index",
                    "text": "我的",
                    "iconPath": "./static/icon/_my.png",
                    "selectedIconPath": "./static/icon/my.png"
                }
            ]
        }
    
    }
    pages.json

    接下来我们需要在App.vue中全局引入字体图标文件。

    <script>
        export default {
            onLaunch: function() {
            },
            onShow: function() {
            },
            onHide: function() {
            }
        }
    </script>
    
    <style>
        @import "./styles/iconfont.wxss";
        @import "./styles/base.wxss";
    </style>
    App.vue

    引入成功之后,就可以看到如下效果啦。

     注意:要记得把icon和styles文件夹放到项目中去哦。

    styles文件加放到和App.vue同层级目录下,icon文件夹放入static文件夹里面。

    2.2 uni-ui介绍

    文档: https://uniapp.dcloud.io/component/README?id=uniui 

    uni-ui是DCloud提供的一个跨端ui库,它是基于vue组件的、flex布局的、无dom的跨全端ui框架。

     uni-ui不包括基础组件,它是基础组件的补充:

     数字角标、日历、卡片、折叠面板、倒计时、抽屉、悬浮按钮、收藏按钮、底部购物导航、宫格、图标、索引列表、列表、加载更多、自定义导航栏、通告栏、数字输入框、分页器、弹出层、评分、搜索栏、分段器、步骤条、滑动操作、轮播图指示点、标签。

    3.首页模块开发准备

    3.1 功能分析

    1.修改导航栏外观

    2.使用分段器组件搭建子页面

    3.封装自己的异步请求

    3.2 搭建子页面

    • 首页模块分为4个部分,分别是 推荐、分类、最新、专辑
    • 新建自定义组件来代替上述的4个页面
      • home-recommend
      • home-category
      • home-new
      • home-album

     3.2.1 分段器介绍

     分段器是指uni-ui中的一个组件,其实就是俗称的标签页,tab栏(https://ext.dcloud.net.cn/plugin?id=54)

    3.2.2 分段器使用

    <template>
      <view>
        <view>
          <uni-segmented-control
            :current="current"
            :values="items.map(v=>v.title)"
            @clickItem="onClickItem"
            style-type="text"
            active-color="#d21974"
          ></uni-segmented-control>
          <view class="content">
            <view v-if="current === 0">
            </view>
            <view v-if="current === 1">
            </view>
            <view v-if="current === 2">
            </view>
            <view v-if="current === 3">
            </view>
          </view>
        </view>
      </view>
    </template>
    
    <script>
    import { uniSegmentedControl } from "@dcloudio/uni-ui";
    export default {
      components: {
        uniSegmentedControl
      },
      data() {
        return {
          items: [
            { title: "推荐" },
            { title: "分类" },
            { title: "最新" },
            { title: "专辑" }
          ],
          current: 0
        };
      },
      methods: {
        onClickItem(e) {
          if (this.current !== e.currentIndex) {
            this.current = e.currentIndex;
          }
        }
      }
    };
    </script>
    
    <style lang="scss">
    
    </style>
    分段器的使用

    3.3 封装自己的异步请求

    为什么要封装?

    1. 原生的请求不支持promise;
    2. uni-api的请求不能够方便的添加请求中效果;
    3. uni-api的请求返回值是个数组,不方便取值;

    封装的思路

    1. 基于原生promise来封装;
    2. 挂载到Vue的原型上;
    3. 通过this.request的方式来使用;
    /* 
      基于原生promise封装request
      发请求之前显示'加载中...'
      请求完成之后隐藏'加载中...'
    */
    
    export default (params) => {
      uni.showLoading({
        title: '加载中'
      });
      return new Promise((resolve, reject)=>{
        wx.request({
          ...params,
          success(res) {
            resolve(res.data);
          },
          fail(err) {
            reject(err);
          },
          complete(){
            uni.hideLoading();
          }
        })
      })
    }
    request

    在main.js里面将request函数挂载到Vue的原型上

    4.首页-推荐模块开发

    4.1 功能介绍

    1. 数据动态渲染;
    2. moment.js的使用;
    3. 基于scroll-view的分页加载;

    4.2 实现过程

    首页推荐这个页面非常简单,没有任何技术含量。。。

    首先我们把静态页面写出来,然后发送请求获取数据然后使用v-for指令循环渲染数据,渲染图片的时候注意接口有没有带rule这个属性,如果有需要把thumb属性和rule进行拼接,这里约定好<Height>的值为300,然后注意一下图像要使用widthFix还是aspectFill,这些都是非常基础的知识,大家可以自行到微信小程序的官方开发文档里面找到,如果你不懂,还不愿意自己去找资料学习,那我也没办法啦。

    日期部分使用的是moment.js库,下面是他的文档地址:

    http://momentjs.cn/docs/#/displaying/

    接下来说一说分页,无非就是把最顶级的view 标签改成scroll-view标签,加上一个scroll-y属性,再加上一个触底事件@scrolltolower=“handleScrollToLower”即可,这些东西uni-app官网都有,由于这个项目是使用uni-app来开发,所以很多API和组件都需要在uni-app文档和微信小程序的文档穿插查找,搞技术嘛,就是要往上面砸时间,耐心点就完事了。

    分页部分的逻辑其实很简单:

    页面触底之后,发送请求获取数据,skip的值等于自身加上limit的值,然后limit的值加上30条,需要注意的是在触底之后发送请求之前要判断是否还有新的limit数据,可以在data里面设置一个状态,比如hasLimit:true,然后在请求函数里面判断一下是否还有新数据返回,如果没有的话就将hasLimit的值改为false并且提示用户。

    <template>
      <scroll-view
        scroll-y
        @scrolltolower="handleScrollToLower"
        class="container"
        v-if="recommentList.length!==0"
      >
        <!-- 推荐部分图片 start -->
        <view class="recomment-wrap">
          <view class="item" v-for="item in recommentList" :key="item.id">
            <image :src="item.thumb" mode="widthFix" />
          </view>
        </view>
        <!-- 推荐部分图片 end -->
    
        <!-- 月份图片部分 start -->
        <view class="month-wrap">
          <view class="month-title">
            <view class="month-info">
              <view class="month-time">
                <text class="day">{{monthList.day}} /</text>
                <text class="month">{{monthList.month}} 月</text>
              </view>
              <view class="month-text">{{monthList.title}}</view>
            </view>
            <view class="month-more">更多></view>
          </view>
          <view class="month-content">
            <view class="item" v-for="item in monthList.items" :key="item.id">
              <image :src="item.thumb + item.rule.replace('$<Height>',300)" mode="aspectFill" />
            </view>
          </view>
        </view>
        <!-- 月份图片部分 end -->
    
        <!-- 热门部分 start -->
        <view class="hot-wrap">
          <view class="hot-title">
            <text class="title-text">热门</text>
          </view>
          <view class="hot-content">
            <view class="hot-item" v-for="item in hotList" :key="item.id">
              <image :src="item.thumb" mode="widthFix" />
            </view>
          </view>
        </view>
        <!-- 热门部分 end -->
      </scroll-view>
    </template>
    
    <script>
    export default {
      data() {
        return {
          params: {
            limit: 30,
            order: "hot",
            skip: 0
          },
          recommentList: [],
          monthList: [],
          hotList: [],
          hasLimit: true
        };
      },
      mounted() {
        this.getList();
      },
      methods: {
        getList() {
          this.request({
            url: "http://157.122.54.189:9088/image/v3/homepage/vertical",
            data: this.params
          }).then(data => {
            if (this.monthList.length === 0) {
              this.recommentList = data.res.homepage[1].items;
              this.monthList = data.res.homepage[2];
              this.monthList.day = this.moment(this.monthList.stime).format("DD");
              this.monthList.month = this.moment(this.monthList.stime).format("MM");
            }
            if (data.res.vertical.length === 0) {
              this.hasLimit = false;
              uni.showToast({
                title: "没有更多数据啦",
                icon: "none"
              });
              return;
            }
            this.hotList = [...this.hotList, ...data.res.vertical];
            this.params.skip += this.params.limit;
            this.params.limit += 30;
          });
        },
        handleScrollToLower() {
          if (this.hasLimit) {
            this.getList();
          } else {
            uni.showToast({
              title: "没有更多数据啦",
              icon: "none"
            });
          }
        }
      }
    };
    </script>
    
    <style lang="scss">
    .container {
      height: calc(100vh - 35px);
    }
    /* 推荐图片部分*/
    .recomment-wrap {
      display: flex;
      flex-wrap: wrap;
      > .item {
         50%;
        image {
          border: 3rpx solid #fff;
        }
      }
    }
    
    /* 月份图片部分 */
    .month-wrap {
      .month-title {
        display: flex;
        justify-content: space-between;
        padding: 20rpx 20rpx;
        .month-info {
          display: flex;
          font-weight: bold;
          .month-time {
            color: $color;
            .day {
              font-size: 32rpx;
            }
    
            .month {
              font-size: 26rpx;
            }
          }
    
          .month-text {
            margin-left: 20rpx;
            color: #666;
            font-size: 32rpx;
          }
        }
    
        .month-more {
          font-size: 28rpx;
          color: $color;
        }
      }
    
      .month-content {
        display: flex;
        flex-wrap: wrap;
        .item {
           33.33%;
          image {
            border: 5rpx solid #fff;
          }
        }
      }
    }
    
    /* 热门部分 */
    .hot-wrap {
      .hot-title {
        padding: 20rpx;
        text.title-text {
          padding-left: 14rpx;
          color: $color;
          border-left: 10rpx solid $color;
          font-size: 28rpx;
          font-weight: bold;
        }
      }
    
      .hot-content {
        display: flex;
        flex-wrap: wrap;
        .hot-item {
           33.33%;
          image {
            border: 5rpx solid #fff;
          }
        }
      }
    }
    </style>
    首页推荐组件代码

    后续还要加上跳转功能,到时候会将跳转抽离成一个公共组件,到时在下文补充。

    5.首页-专辑模块开发

     5.1 功能介绍

    1. swiper轮播图部分
    2. 专辑列表部分

    5.2 实现过程

    轮播图的部分直接使用微信小程序官方提供的swiper组件,注意swiper组件默认宽度100%,高度是150px,而且swiper必须和swiper-item配对出现,否则会出问题,下面是小程序基础教程和官方文档:

    基础教程:https://www.cnblogs.com/replaceroot/p/11262929.html

    官方文档:https://developers.weixin.qq.com/miniprogram/dev/component/swiper.html

    搞定了轮播之后就很容易了,写一下静态页面,发下请求然后渲染数据,注意对分页数据的判断就行啦,对你们来说绝对是小菜一碟,代码如下:

    <template>
      <scroll-view scroll-y="true" @scrolltolower="handleScrollToLower" class="album-wrap">
        <!-- 轮播图部分 start -->
        <swiper class="swiper" indicator-dots="true" autoplay="true" interval="3000" circular="true">
          <swiper-item v-for="item in banner" :key="item.id">
            <image :src="item.thumb" mode="widthFix" />
          </swiper-item>
        </swiper>
        <!-- 轮播图部分 end -->
    
        <!-- 专辑列表部分 start -->
        <view class="album-list">
          <navigator
            :url="`/pages/album/index?id=${item.id}`"
            class="album-item"
            v-for="item in album"
            :key="item.id"
          >
            <view class="album-image">
              <image :src="item.cover" mode="aspectFill" />
            </view>
            <view class="album-info">
              <view class="alubm-name ellipsis">{{item.name}}</view>
              <view class="alubm-desc ellipsis">{{item.desc}}</view>
              <view class="attention">
                <view class="attention-btn">+关注</view>
              </view>
            </view>
          </navigator>
        </view>
        <!-- 专辑列表部分 end -->
    
      </scroll-view>
    </template>
    
    <script>
    export default {
      data() {
        return {
          banner: [],
          album: [],
          params: {
            limit: 30,
            skip: 0,
            order: "new"
          },
          hasLimit: true
        };
      },
      methods: {
        getList() {
          this.request({
            url: "http://157.122.54.189:9088/image/v1/wallpaper/album",
            data: this.params
          }).then(data => {
            if (this.album.length === 0) {
              this.banner = data.res.banner;
            }
            if (data.res.album.length === 0) {
              this.hasLimit = false;
              uni.showToast({
                title: "没有更多的数据啦!",
                icon: "none"
              });
              return;
            }
            this.album = [...this.album, ...data.res.album];
            this.params.skip += this.params.limit;
            this.params.limit += 30;
          });
        },
        handleScrollToLower() {
          if (this.hasLimit) {
            this.getList();
          } else {
            uni.showToast({
              title: "没有更多数据啦!",
              icon: "none"
            });
          }
        }
      },
      mounted() {
        this.getList();
      }
    };
    </script>
    
    <style lang="scss">
    /* 公共样式 */
    .ellipsis {
      text-overflow: ellipsis;
      overflow: hidden;
      white-space: nowrap;
    }
    
    /* 专辑轮播图部分 */
    .album-wrap {
      height: calc(100vh - 35px);
      .swiper {
        height: 320rpx;
        image {
        }
      }
    }
    
    /* 专辑列表部分 */
    .album-list {
      padding: 10rpx;
      .album-item {
        display: flex;
        padding: 10rpx;
        border-bottom: 3rpx solid #d5d5d5;
        .album-image {
          flex: 1;
          image {
             200rpx;
            height: 200rpx;
          }
        }
    
        .album-info {
          flex: 3;
          margin-left: 40rpx;
          overflow: hidden;
          .alubm-name {
            color: #000;
          }
    
          .alubm-desc {
            color: #666;
          }
    
          .attention {
            display: flex;
            justify-content: flex-end;
            margin-top: 10rpx;
            .attention-btn {
              padding: 0 5rpx;
              border: 3rpx solid $color;
              color: $color;
            }
          }
        }
      }
    }
    </style>
    home-album

    6.专辑详情模块开发

    6.1 功能分析

    1. 头部背景图部分
    2. 专辑详情列表图片部分

    6.2 实现过程

    实现的过程也非常简单,首先放一张image图片当作背景图片,图片里面的文字都知道怎么做吧,直接用定位就完事了。

    下面也是一样套路,先写静态页面,然后发请求,注意下图片的宽高,和mode模式就行了,具体的代码如下:

    <template>
      <view class="album-detail-wrap">
        <!-- 专辑详情背景部分 start -->
        <view class="album-background">
          <image :src="album.cover" mode="widthFix" />
          <view class="album-info">
            <view class="album-name">{{album.name}}</view>
            <view class="attention">
              <view class="attention-btn">关注专辑</view>
            </view>
          </view>
        </view>
        <!-- 专辑详情背景部分 end -->
    
        <!-- 列表部分 start -->
        <view class="album-list">
          <view class="album-title">
            <view class="author">
              <image :src="album.user.avatar" mode="aspectFill" />
              <text class="author-name">{{album.user.name}}</text>
            </view>
            <text class="album-desc">{{album.desc}}</text>
          </view>
          <view class="album-content">
            <view class="alubm-item" v-for="item in wallpaper" :key="item.id">
              <image :src="item.thumb + item.rule.replace('$<Height>',300)" mode="aspectFill" />
            </view>
          </view>
        </view>
        <!-- 列表部分 end -->
      </view>
    </template>
    
    <script>
    export default {
      data() {
        return {
          params: {
            limit: 30,
            skip: 0,
            order: "new",
            first: "1"
          },
          id: "",
          album: [],
          wallpaper: [],
          hasLimit: true
        };
      },
      methods: {
        getList() {
          if(this.album.length==0) {
            this.params.first = '1';
          }
          this.request({
            url: `http://157.122.54.189:9088/image/v1/wallpaper/album/${this.id}/wallpaper`,
            data: this.params
          }).then(data => {
            if(this.album.length === 0) {
              this.album = data.res.album;
            }
            if(data.res.wallpaper.length===0) {
              this.hasLimit = false;
              uni.showToast({
                title: '没有数据啦',
                icon: 'none'
              });
              return;
            }
            this.params.first = 0;
            this.wallpaper = [...this.wallpaper,...data.res.wallpaper];
            this.params.skip += this.params.limit;
            this.params.limit += 30;
          });
        }
      },
      onReachBottom() {
        if(this.hasLimit) {
          this.getList();
        }else {
          uni.showToast({
            title: '没有数据啦!',
            icon: 'none'
          });
        }
      },
      onLoad(options) {
        this.id = options.id;
        this.getList();
      }
    };
    </script>
    
    <style lang="scss">
    .album-detail-wrap {
      /* 专辑详情背景图部分 */
      .album-background {
        position: relative;
        image {
        }
    
        .album-info {
          position: absolute;
          bottom: 5%;
          display: flex;
          align-items: center;
          justify-content: space-between;
           100%;
          padding: 0 20rpx;
          .album-name {
            color: #fff;
            font-size: 32rpx;
          }
    
          .attention {
            display: flex;
            justify-content: center;
            align-items: center;
            .attention-btn {
              padding: 10rpx 15rpx;
              background-color: $color;
              color: #fff;
              border-radius: 10rpx;
              font-size: 26rpx;
            }
          }
        }
      }
    
      /* 专辑详情列表部分 */
      .album-list {
        .album-title {
          padding: 10rpx;
          .author {
            display: flex;
            image {
               60rpx;
              height: 60rpx;
            }
    
            text.author-name {
              line-height: 60rpx;
              margin-left: 5rpx;
              color: #000;
            }
          }
    
          text.album-desc {
            color: #666;
            font-size: 26rpx;
          }
        }
    
        .album-content {
          display: flex;
          flex-wrap: wrap;
          .alubm-item {
             33.33%;
            image {
              height: 200rpx;
              border: 3rpx solid #fff;
            }
          }
        }
      }
    }
    </style>
    album/index.vue

    最后有个小坑需要注意下,小程序里面的view标签不支持文本中的换行符,如果某些特殊场景中后台返回的文本里面包含换行符就直接使用text标签就完事了。

    7.图片详情模块开发

     7.1 功能分析

    1. 封装超链接组件
    2. 发送请求获取数据
    3. 使用moment.js处理特殊时间格式
    4. 封装手势滑动组件
    5. 调用API下载图片

    7.2 实现过程

    在components组件文件夹下面新建一个goDetail.vue的自定义组件

    <template>
      <view @click="handleClick">
        <slot></slot>
      </view>
    </template>
    
    <script>
    export default {
      props: {
        list: Array,
        index: Number
      },
      methods: {
        handleClick() {
          /* 
            1 将数据缓存下来 使用getApp()全局缓存方式
            2 实现点击跳转页面
          */
         getApp().globalData.imgList = this.list;
         getApp().globalData.imgIndex = this.index;
         uni.navigateTo({
            url: "/pages/imgDetail/index"
         });
        }
      }
    };
    </script>
    
    <style>
    </style>
    goDetail.vue

    这个地方用到了微信小程序的全局缓存数据的方法,我们把数据缓存在App.vue文件中,使用的时候直接通过getApp().globalData.属性的方法获取数据即可。

     具体发请求获取数据渲染页面的部分自行看代码学习吧。

    项目github地址:https://github.com/C4az6/dnpicture.git

    项目API文档:https://www.showdoc.cc/414855720281749?page_id=3678621017219602

    这个项目教程就此结束。

  • 相关阅读:
    多线程编程之线程死锁问题 转载
    线程的挂起和恢复 转载
    redis 内存库设置 教你怎么解决64位Windows版Redis狂占C盘的问题.
    MSSQL数据库分区表
    如何将桌面的路径定义到其它盘符,如d:users桌面
    sqlserver数据库脱机时发生异常:由于无法在数据库 'SMS' 上放置锁,ALTER DATABASE 失败。请稍后再试。 ALTER DATABASE 语句失败。 (.Net SqlClient Data Provider)
    JAVA代码反编译笔记
    SQLServer—系统中的内存配置
    .net remoting 客户端与服务端绑定事件,一部电脑当服务器,另一部当客户端,发布后没法接收远程错误信息。
    Java Socket 模拟HTTP请求
  • 原文地址:https://www.cnblogs.com/sauronblog/p/12389126.html
Copyright © 2011-2022 走看看