uniapp实现豆瓣电影微信小程序(附源码)

演示

运行

基于本地代理1

npm run dev:proxy1

基于本地代理2

npm run dev:proxy2

基于nginx 代理

npm run dev:nginx

目录结构

|__ douban                                    # 本地代理
    |__ app.js                                  # 方式 1
    |__ proxy.js                                # 方式 2
|__ src
    |__ App.vue
    |__ components                            # 组件
      |__ movie-item.vue                        # 电影列表项
      |__ movie-list.vue                        # 电影列表
    |__ main.js
    |__ pages
      |__ board                               # 榜单
        |__ index.vue
        |__ main.js
      |__ item                                # 电影详情
        |__ index.vue
        |__ main.js
      |__ list                                # 电影列表
        |__ index.vue
        |__ main.js
      |__ profile                             # 关于我
        |__ index.vue
        |__ main.js
      |__ search                              # 电影搜索
        |__ index.vue
        |__ main.js
      |__ splash                              # 启动页面
        |__ index.vue
        |__ main.js
    |__ store                                 # vuex
      |__ index.js                              # 全局
      |__ modules                               # 模块
        |__ item.js                               # 电影详情->对应 pages/item
      |__ mutations-type.js                     # mutations 常量
    |__ utils                                 # 工具
      |__ api.js                                # 豆瓣 api
      |__ index.js                              # 工具方法
      |__ request.js                            # flyio 配置
      |__ wechat.js                             # 微信小程序 api
      |__ wx.js                                 # wx
|__ static                                    # 静态资源
    |__ .gitkeep
    |__ images                                  # 图片
      |__ *.{png,jpg,gif,jpeg}

页面

tabBar包含榜单、搜索、我的

tabBar: {
      color: '#666666',
      selectedColor: '#000000',
      borderStyle: 'white',
      backgroundColor: '#f8f9fb',
      list: [
        {
          text: '榜单',
          pagePath: 'pages/board/main',
          iconPath: 'static/images/board.png',
          selectedIconPath: 'static/images/board-actived.png'
        },
        {
          text: '搜索',
          pagePath: 'pages/search/main',
          iconPath: 'static/images/search.png',
          selectedIconPath: 'static/images/search-actived.png'
        },
        {
          text: '我的',
          pagePath: 'pages/profile/main',
          iconPath: 'static/images/profile.png',
          selectedIconPath: 'static/images/profile-actived.png'
        }
      ]
    }

榜单

<template>
  <div class="md-board">
    <view class="md-board__slide">
      <swiper class="md-board__swiper" :indicator-dots="true" :autoplay="true" :interval="3000" :duration="1000">
        <swiper-item v-for="(movie, index) in movies" :key="index">
          <image class="md-board__slide-image" :src="movie.images.large" mode="aspectFill"/>
        </swiper-item>
      </swiper>
    </view>
    <view class="md-board__list" :scroll-y="true">
      <block v-for="(item, index) in boards" :key="item.key">
        <view class="md-board__item">
          <navigator :url="'../list/main?type=' + item.key + '&title=' + item.title" hover-class="none">
            <view class="md-board__title">
              <text class="md-board__title-text">{{ item.title }}</text>
              <image class="md-board__title-image" src="../../../static/images/arrowright.png" mode="aspectFill"/>
            </view>
          </navigator>
          <scroll-view class="md-board__content" :scroll-x="true">
            <view class="md-board__inner" v-if="item.key !== 'us_box'">
              <navigator v-for="(movie, i) in item.movies" :key="movie.id + index + i" :url="'../item/main?id=' + movie.id">
                <view class="md-board__movie">
                  <image class="md-board__movie-image" :src="movie.images.large" mode="aspectFill"/>
                  <text class="md-board__movie-text">{{ movie.title }}</text>
                </view>
              </navigator>
            </view>
            <view class="md-board__inner" v-else>
              <navigator v-for="(movie, i) in item.movies" :key="movie.rank + index + i" :url="'../item/main?id=' + movie.subject.id">
                <view class="md-borad__movie">
                  <image class="md-board__movie-image" :src="movie.subject.images.large" mode="aspectFill"/>
                  <text class="md-board__movie-text">{{ movie.subject.title }}</text>
                </view>
              </navigator>
            </view>
          </scroll-view>
        </view>
      </block>
    </view>
  </div>
</template>

<script>
import { mapState, mapActions } from 'vuex'

export default {
  computed: {
    ...mapState('board', {
      boards: state => state.boards,
      movies: state => state.movies
    })
  },

  methods: {
    ...mapActions('board', [
      'getBoards'
    ]),
    async getBoardData () {
      await this.getBoards()
    }
  },

  mounted () {
    this.getBoardData()
  }
}
</script>

<style lang="scss">
@include c('board') {

  @include e('swiper') {
    height: 480rpx;
  }

  @include e('slide-image') {
    height: 100%;
    width: 100%;
  }

  @include e('list') {
    box-sizing: border-box;
    background-color: #f8f9fb;
  }

  @include e('item') {
    display: flex;
    flex-direction: column;
    cursor: pointer;
    font-size: 20rpx;
    margin: 40rpx 0;
    padding: 20rpx;
    background-color: #fff;
  }

  @include e('title') {
    display: flex;
    margin-bottom: 10rpx;
    width: 100%;
  }

  @include e('title-text') {
    flex: 1;
  }

  @include e('title-image') {
    height: 20rpx;
    width: 20rpx;
  }

  @include e('content') {
    height: 300rpx;
  }

  @include e('inner') {
    display: flex;
    flex-direction: row;
    height: 300rpx;
    width: 900rpx;
  }

  @include e('movie') {
    display: flex;
    flex-direction: column;
    width: 180rpx;
    margin: 10rpx;
  }

  @include e('movie-image') {
    width: 180rpx;
    height: 250rpx;
  }

  @include e('movie-text') {
    text-align: center;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
  }
}

</style>

搜索

<template>
  <div class="md-search">
    <view class="md-search__header">
      <input class="md-search__input" v-model="q" :placeholder="subtitle" placeholder-class="md-search__placeholder" auto-focus @change="handleSearch"/>
    </view>
    <movie-list v-if="movies.length" :movies="movies" :has-more="hasMore"></movie-list>
  </div>
</template>

<script>
import { mapState, mapActions, mapMutations } from 'vuex'
import MovieList from '@/components/movie-list'
import { LIST_CLEAR_STATE } from '@/store/mutations-type'

export default {
  components: {
    'movie-list': MovieList
  },

  data () {
    return {
      q: '',
      subtitle: '请在此输入搜索内容'
    }
  },

  computed: {
    ...mapState('list', ['movies', 'hasMore', 'type'])
  },

  methods: {
    ...mapMutations('list', {
      clearState: LIST_CLEAR_STATE
    }),
    ...mapActions('list', [
      'getMovies'
    ]),
    async getSearchData () {
      await this.getMovies({type: 'search', search: this.q})
    },

    resetData () {
      this.clearState()
    },

    handleSearch () {
      this.resetData()
      this.getSearchData()
    }
  },

  onReachBottom () { // 上拉加载
    this.getSearchData()
  },

  onHide () { // 清空状态
    this.resetData()
  }
}
</script>

<style lang="scss">
@include c('search') {

  @include e('header') {
    display: flex;
    justify-content: center;
    border-bottom: 1rpx solid #ccc;
  }

  @include e('input') {
    width: 100%;
    padding: 20rpx 40rpx;
    color: #666;
    font-size: 38rpx;
    text-align: center;
  }

  @include e('placeholder') {
    color: #ccc;
    font-size: 38rpx;
    text-align: center;
  }
}

</style>

我的

<template>
  <div class="md-profile">
    <!-- <view class="md-profile__header">
      <text class="md-profile__title">{{ title }}</text>
    </view> -->
    <button open-type="getUserInfo">授权访问</button>
    <view class="md-profile__user" @click="getUserInfo">
      <image class="md-profile__user-avatar" :src="userInfo.avatarUrl" mode="aspectFit"/>
      <text class="md-profile__user-nickname">{{ userInfo.nickName }}</text>
      <text :hidden="!userInfo.city">{{ userInfo.city }}, {{ userInfo.province }}</text>
      <text :hidden="!userInfo.city"> Thanks~ </text>
    </view>
  </div>
</template>

<script>
import { login, getUserInfo } from '@/utils/wechat'
export default {
  data () {
    return {
      title: '关于',
      userInfo: {
        wechat: 'SG',
        nickName: 'https://github.com/mini-mpvue/mpvue-douban',
        avatarUrl: '/static/images/qrcode-sg.png'
      }
    }
  },

  methods: {
    async getUserInfo () {
      const data = await getUserInfo()
      this.userInfo = data.userInfo
    }
  },

  mounted () {
    login().then(res => {
      console.log(res)
    })
  }
}
</script>

<style lang="scss">
@include c('profile') {

  @include e('header') {
    display: flex;
    justify-content: center;
    border-bottom: 1rpx solid #ccc;
  }

  @include e('title') {
    padding: 40rpx;
    color: #999;
    font-size: 148rpx;
    text-align: center;
  }

  @include e('user') {
    display: flex;
    flex-direction: column;
    align-items: center;
  }

  @include e('user-avatar') {
    width: 100%;
    height: 620rpx;
    margin: 40rpx;
  }

  @include e('user-nickname') {
    color: #aaa;
    font-size: 30rpx;
    margin-bottom: 30rpx;
  }
}

</style>

启动页

<template>
  <div class="md-splash">
    <swiper class="md-splash__swiper" indicator-dots>
      <swiper-item class="md-splash__item"  v-for="(item, index) in movies" :for-index="index" :key="item.id">
        <image :src="item.images.large" class="md-splash__image" mode="aspectFill"/>
        <button class="md-splash__start" @click="handleStart" v-if="index === movies.length - 1">立即体验</button>
      </swiper-item>
    </swiper>
  </div>
</template>

<script>
import { getStorage, setStorage } from '@/utils/wechat'
import { getBoardData } from '@/utils/api'
const LAST_SPLASH_DATA = 'LAST_SPLASH_DATA'

export default {
  data () {
    return {
      movies: []
    }
  },

  methods: {
    async getCache () {
      try {
        let res = await getStorage(LAST_SPLASH_DATA)
        const { movies, expires } = res.data
        // 有缓存,判断是否过期
        if (movies && expires > Date.now()) {
          return res.data
        }
        // 已经过期
        console.log('uncached')
        return null
      } catch (error) {
        return null
      }
    },

    handleStart () {
      // TODO: 访问历史的问题
      wx.switchTab({
        url: '../board/main'
      })
    },

    async getInitData () {
      let cache = await this.getCache()
      if (cache) {
        this.movies = cache.movies
        return
      }
      let data = await getBoardData({board: 'coming_soon', page: 1, count: 3})
      this.movies = data.subjects
      await setStorage(LAST_SPLASH_DATA, {
        movies: data.subjects,
        expires: Date.now() + 1 * 24 * 60 * 60 * 1000
      })
    }
  },

  mounted () {
    this.getInitData()
  }
}
</script>

<style lang="scss">
page {
  height: 100%;
}

@include c('splash') {
  height: 100%;

  @include e('swiper') {
    height: 100%;
  }

  @include e('item') {
    flex: 1;
  }

  @include e('image') {
    position: absolute;
    height: 100%;
    width: 100%;
    opacity: .9;
  }

  @include e('start') {
    position: absolute;
    bottom: 200rpx;
    left: 50%;
    width: 300rpx;
    margin-left: -150rpx;
    background-color: rgba(64, 88, 109, .4);
    color: #fff;
    border: 1rpx solid rgba(64, 88, 109, .8);
    border-radius: 200rpx;
    font-size: 40rpx;
  }
}

</style>

item详情

<template>
  <div class="md-item">
    <image v-if="movie.images" class="md-item__background" :src="movie.images.large" mode="aspectFill"/>
    <block v-if="movie.title">
      <view class="md-item__meta">
        <image class="md-item__poster" :src="movie.images.large" mode="aspectFit"/>
        <text class="md-item__title">{{ movie.title }}({{ movie.year }})</text>
        <text class="md-item__info">评分:{{ movie.rating.average }}</text>
        <text class="md-item__info">导演:<block v-for="director in movie.directors" :key="director.id"> {{ director.name }} </block></text>
        <text class="md-item__info">主演:<block v-for="cast in movie.casts" :key="cast.id"> {{ cast.name }} </block></text>
      </view>
      <view class="md-item__summary">
        <text class="md-item__label">摘要:</text>
        <text class="md-item__content">{{ movie.summary }}</text>
      </view>
    </block>
  </div>
</template>

<script>
import { mapState, mapActions, mapMutations } from 'vuex'
import { ITEM_CLEAR_MOVIE } from '@/store/mutations-type'
import wx from '@/utils/wx'

export default {
  data () {
    return {
      id: null
    }
  },

  computed: {
    ...mapState('item', {
      movie: state => state.movie
    })
  },

  methods: {
    ...mapActions('item', [
      'getMovie'
    ]),
    ...mapMutations('item', {
      clearMovie: ITEM_CLEAR_MOVIE
    }),
    async getMovieData (id) {
      await this.getMovie({ id })
      wx.setNavigationBarTitle({ title: this.movie.title + ' « 电影 « 豆瓣' })
    }
  },

  mounted () {
    const id = this.$root.$mp.query.id
    if (!id) {
      return wx.navigateBack()
    }
    this.id = id
    this.getMovieData(id)
  },

  onUnload () {
    this.clearMovie()
  }
}
</script>

<style lang="scss">
@include c('item') {

  @include e('background') {
    position: fixed;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
    height: 100%;
    width: 100%;
    z-index: -1000;
    opacity: .1;
  }

  @include e('meta') {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 50rpx 40rpx;
  }

  @include e('poster') {
    width: 100%;
    height: 800rpx;
    margin: 20rpx;
  }

  @include e('title') {
    font-style: 42rpx;
    color: #444;
  }

  @include e('info') {
    font-size: 24rpx;
    color: #888;
    margin-top: 20rpx;
    width: 80%;
  }

  @include e('summary') {
    width: 80%;
    margin: 30rpx auto;
  }

  @include e('label') {
    display: block;
    font-size: 30rpx;
    margin-bottom: 30rpx;
  }

  @include e('content') {
    color: #666;
    font-size: 22rpx;
    padding: 2em;
  }
}

</style>

构建

# 安装依赖
npm install

# 开发
npm run dev

# 基于本地代理1 开发
npm run dev:proxy1

# 基于本地代理2 开发
npm run dev:proxy2

# 基于nginx 代理开发
npm run dev:nginx

# 生产
npm run build

# 生产分析图表
npm run build --report

# 启动本地代理1
npm run proxy1

# 启动本地代理2
npm run proxy2

代理

Nginx 代理:

src/utils/request.js

request.config.baseURL = 'https://movie.douban.gusaifei.com/v2/movie'

随着应用一起启动

本地代理:

douban/app.js

npm run proxy1

douban/proxy.js

npm run proxy2

需要借助 npm scripts 启动,或者进入到 douban 目录,运行 node app.jsnode proxy.js

源码截图:

在这里插入图片描述

说明

如果本项目对您有帮助,欢迎 “点赞,关注” 支持一下 谢谢~

源码获取关注公众号「码农园区」,回复 【uniapp源码】
在这里插入图片描述

你可能感兴趣的:(Uniapp,小程序,uni-app,微信小程序,notepad++)