第四章:豆瓣项目之首页

使用功能技巧

获取当前定位

  1. 打开小程序官网文档:API->位置,可以查看使用示例:wx.getLocation
  2. 注意:使用定位需要在app.json添加permission字段

     "permission": {
         "scope.userLocation": {
           "desc": "你的位置信息将用于获取影院信息"
         }
     }
    

豆瓣API

  1. 豆瓣官方的 API 已经不对外开放,但有第三方代理
    1. https://douban.uieee.com
    2. https://douban-api.uieee.com
  2. 每个代理限流:10000 次 / 1 小时(豆瓣官方的限流),所有人共享这 10000 次
  3. API 文档:https://douban-api-docs.zce.me/
  4. 电影 API 文档:https://douban-api-docs.zce.me/movie.html

百度地图逆地理编码使用

  1. 创建百度地图应用、申请AK
    1. 首先要申请一个 AK :http://lbsyun.baidu.com/apiconsole/key/create
      1. 点击“创建应用”
      2. 输入“应用名称”(随便写,比如:CoderZHTestProject)
      3. 应用类型选择“浏览器端”
      4. 启用服务一定要勾选“逆地理编码”
      5. Referer白名单:输入“*”,代表什么请求网址都可以
      6. 点击“提交”
    2. 根据这个应用拿到AK值
  2. 逆地理编码 API
    1. 文档
      1. API地址
      2. 点击”服务文档”,可以看见使用方法:

         http://api.map.baidu.com/reverse_geocoding/v3/?ak=您的ak&output=json&coordtype=wgs84ll&location=31.225696563611,121.49884033194  //GET请求
        
    2. 即地址固定,只需要传入:AK与location经纬度即可

微信小程序的网络请求配置

  1. 微信小程序中如果要请求一个新的地址,那么首先需要将这个地址域名在微信后台注册
  2. 打开微信小程序官网->开发->开发设置->服务器域名->request合法域名
  3. 然后在这里面把域名添加进去,比如:https://api.map.baidu.com,就可以网络请求了
  4. 注意:如果配置了仍然不能访问:
    1. 看是否是https
    2. “微信开发者工具”重新登录一次

微信小程序弹框提示封装

  1. 打开小程序官网文档:API->界面->交互->wx.showToast
  2. 封装系统的toast
  3. 因为是全局使用所以放在app.js文件中

     //app.js
     App({
       // 小程序启动完毕调用
       onLaunch: function () {
         // 创建一个db对象
         wx.db={};
         this.initToast();
         wx.db.url = (url)=>{
           // 字符串拼接
           return `https://api.douban.com/${url}`;
         }
            
       },
       initToast:function(){
        
         // type = 0; 提示
         // type = 1, 成功
         // type = 2 失败
         let commonToast =(title,type,duration=1500)=>{
           let options ={
             title: title,
             icon: 'none',
             duration: duration,
             image: ''
           };
           if(type == 1){
             options.icon ='success';
           }else if(type == 2){
             options.image = '/assets/imgs/upsdk_cancel_normal.png'
           }
           wx.showToast(options);
         };
        
         wx.db.toast = (title,duration)=>{
           commonToast(title,0,duration);
         };
         // 2. 封装一个失败提示
         wx.db.toastError = (title,duration)=>{
           commonToast(title,1,duration);
         };
         // 3. 封装成功提示
         wx.db.toastSuccess = (title,duration)=>{
           commonToast(title,2,duration);
         };
         /*
         const tempDuration = 1500;
         // 1. 文本提示
         wx.db.toast = (title,duration=tempDuration)=>{
           wx.showToast({
             title: title,
             icon: 'none',
             duration: duration
           });
         };
         // 2. 封装一个失败提示
         wx.db.toastError = (title,duration=tempDuration)=>{
           wx.showToast({
             title: title,
             icon: 'none',
             image: '/assets/imgs/upsdk_cancel_normal.png',
             duration: duration
           });
         };
         // 3. 封装成功提示
         wx.db.toastSuccess = (title,duration=tempDuration)=>{
           wx.showToast({
             title: title,
             icon: 'success',
             duration: duration
           });
         };
         */
       },
       globalData: {
         userInfo: null
       }
     })  
    

数据缓存

  1. 打开官方API文档,找到数据缓存
  2. 数据的缓存、获取、删除都有同步和异步之分
  3. 缓存后的数据可以通过“微信开发者工具”->调试器->storage 查看当前应用缓存的数据

清除授权缓存

  1. 场景:一旦授权,下次再如何才能再次弹框?
  2. 解决办法:打开“微信开发者工具” 点击上面的清缓存按钮->清除授权数据
  3. 再次刷新,就会重新弹出授权框

首页开发

效果如下:

pic

首页展示

  1. 初始化home模块:
    1. 用“微信开发者工具”打开,在pages目录下右击“新建目录”创建home文件夹,然后右击新建Page命名为home
    2. 在app.json文件中,将pages下的home提到第一个,即启动显示home页面

home.wxml

<!--pages/home/home.wxml-->
<view>
    <view class="search-wrapper">
        <view class="search">
            <image src="/assets/imgs/ic_search.png" />
            搜索
        </view>
    </view>
    <view class="main">
        <view wx:for="*{allMovies}}" wx:for-item="rowMovie" wx:key="unique" class="row">
            <view class="title-wrapper">
                <view class="title">*{rowMovie.title}}</view>
                <view class="more" data-index="*{index}}" bind:tap="viewMore">查看更多 <view class="arrow"></view></view>
            </view>
            <!-- 
                查看官方文档scroll-view的使用方法
                然后下载官方demo,查看使用方法
                设置横向滚动: scroll-x="true"
             -->
            <scroll-view class="items" scroll-x="true">
                <view class="item" wx:for="*{rowMovie.movies}}" wx:for-item="movie" wx:key="unique">
                    <image class="photo" src="*{movie.images.large}}" />
                    <view class="title">*{movie.title}}</view>
                    <view class="score">
                        <block wx:if="*{movie.stars}}">
                                <view class="stars">
                                    <!-- 展示星星 -->
                                    <!-- 方法1: stars为基本数据类型,数组 -->
                                    <!--block 仅仅用来循环,不显示控件 -->
                                    <!-- 根据条件判断是否显示控件 -->
                                    <!-- <block wx:for="*{movie.stars}}" wx:key="unique" wx:for-item="star">
                    
                                        <image class="star" wx:if="*{star == 1}}" src="/assets/imgs/rating_star_small_on.png" />
                                        <image class="star" wx:elif="*{star == 0.5}}" src="/assets/imgs/rating_star_small_half.png" />
                                        <image class="star" wx:else="*{star == 0.5}}" src="/assets/imgs/rating_star_small_off.png" />
                                    </block> -->

                                    <!-- 方法2:stars为对象 -->
                                    <image class="star" wx:for="*{movie.stars.on}}" wx:key="unique" src="/assets/imgs/rating_star_small_on.png" />
                                    <image class="star" wx:for="*{movie.stars.half}}" wx:key="unique" src="/assets/imgs/rating_star_small_half.png" />
                                    <image class="star" wx:for="*{movie.stars.off}}" wx:key="unique" src="/assets/imgs/rating_star_small_off.png" />
                                </view>
                            *{movie.rating.average}}
                        </block>
                        <block wx:else> 暂无评分 </block>
                          
                    </view>
                      
                </view>
            </scroll-view>
        </view>
    </view>
</view>

home.wxss

/* pages/home/home.wxss */

.search-wrapper{
    background-color: #42bd55;
    padding: 15rpx;
    height: 60rpx;
    margin-bottom: 30rpx;
}
.search {
    background-color: white;
    border-radius: 10rpx;
    /* 让当前控件的高度填充父类,否则该控件的高度为包裹内容高度 */
    height: 100%;
    /* 设置内容居中 */
    display: flex;
    align-items: center;
    justify-content: center;
}

.search image{
    /* 快速输入方法: w32rpx+h32rpx */
    height: 32rpx;
    width: 32rpx;
    margin-right: 10rpx;
}
.title-wrapper {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 0 30rpx;
    margin-bottom: 40rpx;
}
.title-wrapper .title { 
    color: black;
    font-weight: bold;
    font-size: 32rpx;
}

.title-wrapper .more {
    color: #0db019;
    display: flex;
    align-items: center;
}
.title-wrapper .arrow { 
    width: 15rpx;
    height: 15rpx;
    border: 4rpx solid #0db019;
    border-left: none;
    border-top: none;
    transform: rotate(-45deg);
    margin-left: 6rpx;
}
.item  .photo{
    width: 100%;
    height: 223rpx;
    margin-bottom: 10rpx;
    border-radius: 10rpx;
    border: 4rpx solid #f2f2f2;
}
.item .title {
    /* 限制文字的宽度 */
    width: 100%;
    /* 超出部分隐藏 */
    overflow: hidden;
    /* 超出部分用...显示 */
    text-overflow: ellipsis;
    font-weight: bold;
    margin-bottom: 6rpx;

}
.score {
    display: flex;
    align-items: center;
    /* background-color: coral; */
    color: gray;
}
.score .stars {
    display: flex;
    margin-right: 4rpx;
}
.stars .star {
    width: 24rpx;
    height: 24rpx;
}

.items {
    /* 实现方法1:父容器为View  */
    /* display: flex;
    //web的方法设置滚动
    overflow-x: auto;
    */
    /* padding-left: 30rpx; */
    /* padding-right: 30rpx;  */

    /* 实现方法2:父容器为scroll-view */
    /* 这个属性非常重要,让item横向显示: 不换行 */
    white-space: nowrap;
}
.items .item {
    /* margin: 0 15rpx; */
    /* display: inline-block; */
    display: inline-flex;
    flex-direction: column;
    margin-left: 30rpx;
    width: 160rpx;
}

/* 拿到items的最后一个item */
.items .item:last-of-type {
    margin-right: 30rpx;
}
.row {
    margin-bottom: 50rpx;
    /* 占位高度,图片没加载出来之前的高度 */
    height: 382rpx;
}

home.js

// pages/home/home.js
Page({

  /**
   * 页面的初始数据
   */
  data: {
    // 数据源
    // movies :[],
    allMovies:[
      {
        title: '影院热映',
        url: 'v2/movie/in_theaters',
        movies:[]
      },
      {
        title: '新片榜',
        url: 'v2/movie/new_movies',
        movies:[]
      },
      {
        title: '口碑榜',
        url: 'v2/movie/weekly',
        movies:[]
      },
      {
        title: '北美票房榜',
        url: 'v2/movie/us_box',
        movies:[]
      },
      {
        title: 'Top250',
        url: 'v2/movie/top250',
        movies:[]
      },
    ],
      
  },

  /**
   * 生命周期函数--监听页面加载
   * 页面加载完毕
   */
  onLoad: function (options) {
      
      // 1. 获取当前城市信息
      // 传参为函数
      /*
      this.loadCity((city)=>{
        console.log('获取城市信息:'+city);
        this.loadData(city);
      });
      */
    //  等价于上面
    // 影院热映
      // this.loadCity(this.loadData);
      
      //1. 先加载本地数据
      this.loadLocalData();
      //2. 再去请求网络数据
      // this.loadCity((city)=>{
      //   this.loadNewData(0,city);
      // })
      // // 新片榜
      // this.loadNewData(1);
      // // 口碑榜
      // this.loadNewData(2);
      // // 北美票房榜
      // this.loadNewData(3);
      // // top250
      // this.loadNewData(4);
  },
  loadLocalData: function () {
    for (let index = 0; index < this.data.allMovies.length; index++) {
      // 同步获取缓存数据
      let obj = this.data.allMovies[index];
      obj.movies = wx.getStorageSync(obj.title);
    }
    this.setData(this.data);
  },

  loadNewData: function (idx,para){
    var param = {
      start: 0,
      count: 25,
      apikey:'0df993c66c0c636e29ecbb5344252a4a'
    };
    if(idx == 0){
      param.city = para;
    }

      const obj = this.data.allMovies[idx];
      var reqTask = wx.request({
        url: wx.db.url(obj.url),
        data: param,
        header: { 'content-type':'json'},
        success: (res) => {
          console.log(res);
          const movies = res.data.subjects;
          // 清空数组
          obj.movies = [];
          for (let index = 0; index < movies.length; index++) {
            // 因为这几条数据返回的结构不一样,所以这做适配
            // 经典写法,如果左边存在,直接计算左边,不用计算右边
            let movie = movies[index].subject || movies[index];
            this.updateMovie(movie);
            // 更新数据源
            obj.movies.push(movie);
          }
          // 缓存数据
          // 同步存储数据
          // wx.setStorageSync(key, data);

          // 异步存储数据,将movies缓存到本地
          wx.setStorage({
            key: obj.title,
            data: obj.movies,
            success: (result) => {
              
            },
            fail: () => {}
          });
            
            
            
          // 刷新页面
          this.setData(this.data);
        },
        fail: () => {
          wx.db.toast(`获取${obj.title}失败`);
        }
      });
        
  },
  /**
   * 获取当前城市
   * success就是一个函数参数
   * 类型由调用者决定
   */
  loadCity: function (success){
    /**
     * 1. 获取经纬度
     * 打开小程序官网文档:API->位置
     * 使用定位需要在app.json添加permission字段
     */
    wx.getLocation({
      type: 'wgs84',
      success: (res) => {
        // 获取经纬度
        console.log(res.latitude,res.longitude);
        /**
         * 2. 通过百度地图API的逆地理编码拿到城市
         * 打开小程序官网文档:API->网络
         * 输入wx-request回车
         * 查看百度官方文档,如何使用逆地理编码
         * 到微信后台注册该域名
         */
        var reqTask = wx.request({
          url: 'https://api.map.baidu.com/reverse_geocoding/v3/',
          data: {
            ak: 'nfAoebyL0zLMfhW0F7snbTrYP8p3V4DD',
            output:'json',
            coordtype:'wgs84ll',
            location: res.latitude + ',' + res.longitude 
          },
          header: {'content-type':'application/json'},
          method: 'GET',
          dataType: 'json',
          responseType: 'text',
          success: (res) => {
            // console.log(res.data.result.addressComponent.city);
            // 郑州市
            let city = res.data.result.addressComponent.city;
            // 截掉字符串中的“市”
            city = city.substring(0,city.length-1);
            // 将城市回调出去
            // if(success) success(city);
            // 等价于if,经典写法
            success && success(city);
          },
          fail: () => {
            wx.db.toastError('获取城市失败');
          },
          complete: () => {}
        });
          
      },
      fail: () => {
        console.log("获取位置失败");
        wx.db.toastError('获取位置失败');
      }
    });
  },
  // 废弃
  loadData: function(city){
    var reqTask = wx.request({
      // url: 'https://api.douban.com/v2/movie/in_theaters',
      url: wx.db.url('v2/movie/in_theaters'),
      data: {
        start: 0,
        count: 25,
        city: city,
        apikey:'0df993c66c0c636e29ecbb5344252a4a'
      },
      // 注意这里必须设置为json,不能为application/json
      header: { 'content-type':'json'},
      success: (res) => {
        console.log(res.data.subjects);
        //数据中间加工处理,stars
        let movies = res.data.subjects;
        for (let index = 0; index < movies.length; index++) {
          this.updateMovie(movies[index]);
        }
        this.data.allMovies[0].movies = movies;
        this.setData(this.data);
        // this.setData({ movies : movies});


        /**
         * 将获取的数据设置到数据源
         * this.setData() 这个函数本质就是刷新UI页面
         * ()内部就是数据源
         * {} 使用来包装表达式的
         */
        // 方式1: 
        // 2. 渲染页面
        // this.setData({
        //   //1. 设置数据源
        //   movies: res.data.subjects,
        // });
        //方式2: 
        // 1. 设置数据源
        // this.data.movies = res.data.subjects;
        // 2. 渲染页面
        // this.setData(this.data);
      },
      fail: () => {
        console.log("获取热映失败");
        wx.db.toastError('获取热映失败');
      },
      complete: () => {}
    });
      
  },
  // 数据中间加工处理
  updateMovie :function (movie) {
    // 将stars字符串转为整数
    let stars = parseInt(movie.rating.stars);
    // 展示星星数据整合
    // movie模型本来没有starts属性,给movie(model)添加辅助属性stars,跟iOS一样 !!!!!
    // 方式1; stars为基本数据类型,数组
    // movie.stars = [1,1,1,0.5,0];
    // 方式2: stars为对象
    //  stars为对象 的on、half、off的3个属性分别代表满星几个,半星几个,无星几个
    if(stars == 0) return;
    movie.stars = {};
    // movie.stars.on = 3;
    // movie.stars.half = 1;
    // movie.stars.off = 1;
    // 转整形
    movie.stars.on = parseInt(stars/10);
    movie.stars.half = (stars - (movie.stars.on)*10 ) >0 ? 1 : 0;
    movie.stars.off = 5 - movie.stars.on - movie.stars.half;

  },
  // 点击查看更多
  viewMore: function (evt){
    const index = evt.currentTarget.dataset.index;
    const obj = this.data.allMovies[index];
    // 查看官方API路由,找到navigateTo,查看传参:path?key=value&key2=value2
    wx.navigateTo({
      url: `/pages/list/list?title=${obj.title}&url=${obj.url}`
    });
  }
})

详情列表

list.wxml

<!--pages/list/list.wxml-->
<view class="container">
    <view class="item" wx:for="*{movies}}" wx:for-item="movie" wx:key="unique">
        <image class="photo" src="*{movie.images.large}}" />
        <view class="title">*{movie.title}}</view>
        <view class="score">
            <block wx:if="*{movie.stars}}">
                    <view class="stars">
                        <!-- 方法2:stars为对象 -->
                        <image class="star" wx:for="*{movie.stars.on}}" wx:key="unique" src="/assets/imgs/rating_star_small_on.png" />
                        <image class="star" wx:for="*{movie.stars.half}}" wx:key="unique" src="/assets/imgs/rating_star_small_half.png" />
                        <image class="star" wx:for="*{movie.stars.off}}" wx:key="unique" src="/assets/imgs/rating_star_small_off.png" />
                    </view>
                *{movie.rating.average}}
            </block>
            <block wx:else> 暂无评分 </block>
                
        </view>
                             
    </view>
    <!-- 空模块填充 -->
     <view wx:if="*{movies.length%3 > 0 }}" class="item hide" >              
    </view>
    <view wx:if="*{movies.length%3 ==1 }}" class="item hide" >                
    </view>
</view>

list.wxss

.container {
    display: flex;
    justify-content: space-around;
    /* 换行 */
    flex-wrap: wrap;
    padding-top: 30rpx;
}
/* pages/list/list.wxss */
.item  .photo{
    width: 100%;
    height: 223rpx;
    margin-bottom: 10rpx;
    border-radius: 10rpx;
    border: 4rpx solid #f2f2f2;
}
.item .title {
    /* 限制文字的宽度 */
    width: 100%;
    /* 超出部分隐藏 */
    overflow: hidden;
    /* 超出部分用...显示 */
    text-overflow: ellipsis;
    font-weight: bold;
    margin-bottom: 6rpx;
    text-align: center;
    /* 不换行 */
    white-space: nowrap;
}
.score {
    display: flex;
    align-items: center;
    /* background-color: coral; */
    color: gray;
}
.score .stars {
    display: flex;
    margin-right: 4rpx;
}
.stars .star {
    width: 24rpx;
    height: 24rpx;
}


.item {
    display: inline-flex;
    flex-direction: column;
    width: 160rpx;
    margin: 0 30rpx 30rpx;
}

/* 拿到items的最后一个item */
.item:last-of-type {
    margin-right: 30rpx;
}
.row {
    margin-bottom: 30rpx;
    /* 占位高度,图片没加载出来之前的高度 */
    height: 382rpx;
}

.hide {
    visibility: hidden;
}

list.js

// pages/list/list.js
Page({

  /**
   * 页面的初始数据
   */
  data: {
    movies:[]
  },

  /**
   * 生命周期函数--监听页面加载
   * options: 其他页面传过来的参数
   */
  onLoad: function (options) {
      console.log(options);
      //1. 动态置导航栏,查看API文档,界面-导航栏
      wx.setNavigationBarTitle({
        title: options.title
      });
      //2. 取缓存
       wx.getStorage({
         key: options.title,
         success: (result) => {
           console.log(result.data);
           this.setData({
             movies: result.data
           });
         },
         fail: () => {}
       });
  }
})
Table of Contents