演示
运行
基于本地代理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.js
或 node proxy.js
源码截图:
说明
如果本项目对您有帮助,欢迎 “点赞,关注” 支持一下 谢谢~
源码获取关注公众号「码农园区」,回复 【uniapp源码】