一、项目结构
|- index.html
|- package.json
|- webpack.config.js
src
│ index.js
│ utils.js
│ 原型图.png
│
├─api
│ │ home.js
│ │ session.js
│ │
│ └─config
│ index.js
│
├─common
│ index.less
│
├─component
│ ├─Aleart
│ │ Alert.js
│ │ index.less
│ │
│ ├─Header
│ │ Header.js
│ │ index.less
│ │
│ ├─Loading
│ │ index.less
│ │ Loading.js
│ │
│ └─TabBar
│ index.less
│ TabBar.js
│
├─container
│ ├─Home
│ │ Home.js
│ │ HomeHeader.js
│ │ HomeList.js
│ │ HomeSlider.js
│ │ index.less
│ │
│ ├─Lesson
│ │ Lesson.js
│ │
│ ├─Login
│ │ index.less
│ │ Login.js
│ │
│ ├─Profile
│ │ index.less
│ │ Profile.js
│ │
│ └─Reg
│ index.less
│ Reg.js
│
├─images
│ default.png
│ login_bg.png
│ logo.png
│ profile.png
│
└─store
│ action-types.js
│ index.js
│
├─actions
│ home.js
│ session.js
│
└─reducers
home.js
index.js
session.js
二、项目代码
- package.json
{
"name": "day4",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack-dev-server",
"build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"babel-core": "^6.26.3",
"babel-loader": "7.1.5",
"babel-preset-env": "^1.7.0",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.24.1",
"css-loader": "^3.2.0",
"file-loader": "^4.2.0",
"html-webpack-plugin": "^3.2.0",
"less": "^3.10.2",
"less-loader": "^5.0.0",
"style-loader": "^1.0.0",
"url-loader": "^2.1.0",
"webpack": "^4.39.2",
"webpack-cli": "^3.3.7",
"webpack-dev-server": "^3.8.0"
},
"dependencies": {
"axios": "^0.19.0",
"babel-preset-es2015": "^6.24.1",
"react": "^16.9.0",
"react-dom": "^16.9.0",
"react-redux": "^7.1.0",
"react-router-dom": "^5.0.1",
"react-swipe": "^6.0.4",
"react-transition-group": "^4.2.2",
"redux": "^4.0.4",
"redux-logger": "^3.0.6",
"redux-promise": "^0.6.0",
"redux-thunk": "^2.3.0",
"swipe-js-iso": "^2.1.5"
}
}
- webpack.config.js
let path = require('path')
let HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/index',
output: {
filename: 'bundle.js',
path: path.resolve('./dist')
},
devServer: {
open: true
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
use: {
loader: 'babel-loader',
options: {
presets: ['react', 'env', 'stage-0'] // react env state-0 没有stage-0 class 中不能用箭头函数
}
},
exclude: /node_modules/
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.less/,
use: ['style-loader', 'css-loader', 'less-loader']
},
{
test: /\.(jpg|png|gif)$/,
use: 'file-loader'
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './index.html'
})
]
}
- index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="//at.alicdn.com/t/font_528226_1jsbl7286t7qfr.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
- src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import Home from './container/Home/Home'
import Lesson from './container/Lesson/Lesson'
import Profile from './container/Profile/Profile'
import Login from './container/Login/Login'
import TabBar from './component/TabBar/TabBar'
import Reg from './container/Reg/Reg'
import './common/index.less'
import { HashRouter, Route, Switch } from 'react-router-dom'
import store from './store'
import { Provider } from 'react-redux'
ReactDOM.render(<Provider store={store}>
<HashRouter>
<div>
<Switch>
<Route path='/home' component={Home} />
<Route path='/lesson' component={Lesson} />
<Route path='/profile' component={Profile} />
<Route path='/login' component={Login} />
<Route path='/reg' component={Reg} />
</Switch>
<TabBar />
</div>
</HashRouter>
</Provider>, document.getElementById('root'))
- utils.js
export const loadMore = (ele, cb) => {
ele.addEventListener('scroll', (e) => {
let { offsetHeight, scrollTop, scrollHeight } = ele
clearTimeout(ele.timer)
ele.timer = setTimeout(() => {
// ?? 盒子模型关系
if (scrollTop + offsetHeight + 20 > scrollHeight) {
cb()
}
}, 30)
}, false)
}
export const pullRefresh = (ele, cb) => {
// 当前元素的 offsetTop 偏移量,如果正在下拉,触发无效
let offsetTop = ele.offsetTop
let distance = 0
ele.addEventListener('touchstart', (e) => {
let startY = e.touches[0].pageY
console.log(startY)
let touchmove = function (e) {
// 计算手指移动的距离
let moveY = e.touches[0].pageY
// console.log(moveY - startY)
if (moveY - startY > 0) { // 正在下来刷新
// 确保向下拉
distance = moveY - startY
if (distance > 50) {
distance = 50
return ele.style.top = offsetTop + distance + 'px'
}
if (distance > 10) {
ele.style.top = offsetTop + distance + 'px'
}
} else {
ele.removeEventListener('touchmove', touchmove)
ele.removeEventListener('touchend', touchend)
}
}
let touchend = function (e) {
let timer = null
if (distance !== 50) return ele.style.top = offsetTop + 'px'
timer = setInterval(() => {
distance--
if (distance <= 0) {
clearInterval(timer)
cb()
}
ele.style.top = offsetTop + distance + 'px'
}, 6)
ele.removeEventListener('touchmove', touchmove)
ele.removeEventListener('touchend', touchend)
}
console.log(ele.offsetTop, offsetTop)
console.log(ele.scrollTop === 0)
if (ele.offsetTop === offsetTop && ele.scrollTop === 0) {
ele.addEventListener('touchmove', touchmove)
ele.addEventListener('touchend', touchend)
} else {
ele.removeEventListener('touchmove', touchmove)
ele.removeEventListener('touchend', touchend)
}
})
}
- /store/index.js
import { createStore, applyMiddleware } from 'redux'
import reducer from './reducers'
import reduxLogger from 'redux-logger'
import reduxThunk from 'redux-thunk'
import reduxPromise from 'redux-promise'
let store = createStore(reducer, applyMiddleware(reduxLogger, reduxThunk, reduxPromise))
window.__store = store
export default store
- /store/actions-types.js
export const SET_CURRENT_LESSON = 'SET_CURRENT_LESSON'
// 获取轮播图 获取轮播图之前 获取成功
export const GET_SLIDERS = 'GET_SLIDERS'
export const GET_SLIDERS_SUCCESS = 'GET_SLIDERS_SUCCESS'
// 获取课程列表
export const GET_LESSONS = 'GET_LESSONS'
export const GET_LESSONS_SUCCESS = 'GET_LESSONS_SUCCESS'
// 清除课程
export const CLEAR_LESSON = 'CLEAR_LESSON'
// 设置用户信息:
export const SET_USER_INFO = 'SET_USER_INFO'
- store/reducer/home.js
import * as Types from '../action-types'
let initState = {
currentLesson: '1',
slider: {
loading: false,
list: []
},
lesson: {
loading: false,
hasMore: true,
offset: 0,
limit: 5,
list: []
}
}
function home(state = initState, action) {
switch (action.type) {
case Types.SET_CURRENT_LESSON:
return {
...state,
currentLesson: action.currentLesson
}
case Types.GET_SLIDERS:
return {
...state,
slider: {
...state.slider,
loading: true
}
}
case Types.GET_SLIDERS_SUCCESS:
return {
...state,
slider: {
list: action.payload,
loading: false
}
}
case Types.GET_LESSONS:
return {
...state,
lesson: {
...state.lesson,
loading: true
}
}
case Types.GET_LESSONS_SUCCESS:
return {
...state,
lesson: {
...state.lesson,
loading: false,
hasMore: action.payload.hasMore,
list: [
...state.lesson.list,
...action.payload.list
],
offset: state.lesson.offset + action.payload.list.length
}
}
case Types.CLEAR_LESSON:
return {
...state,
lesson: {
...state.lesson,
offset: 0,
list: [],
loading: false,
hasMore: true
}
}
}
return state
}
export default home
- store/reducers/session.js
import * as Types from '../action-types'
let initState = {
msg: '',
err: 0,
user: null
}
function reducer(state = initState, action) {
switch (action.type) {
case Types.SET_USER_INFO:
return {...action.payload}
}
return state
}
export default reducer
- store/reducer/index.js
import { combineReducers } from 'redux'
import home from './home'
import session from './session'
export default combineReducers({
home,
session
})
- store/actions/home.js
import * as Types from '../action-types'
import { getSliders, getLessons } from "../../api/home";
let action = {
setCurrentLess (currentLesson) {
return (dispatch, getState) => {
dispatch({
type: Types.SET_CURRENT_LESSON,
currentLesson
})
dispatch({
type: Types.CLEAR_LESSON
}) // 清除原有课程信息
// 按照最新信息去筛选
action.setLessons()(dispatch, getState)
}
},
refresh () {
return (dispatch, getState) => {
dispatch({
type: Types.CLEAR_LESSON
})
action.setLessons()(dispatch, getState)
}
},
setSliders () {
return (dispatch) => {
dispatch({type: Types.GET_SLIDERS}) // 将 redux 中的数据改变成正在加载
dispatch({
type: Types.GET_SLIDERS_SUCCESS,
payload: getSliders()
}) // 将 redux 中的数据改变成正在加载
}
},
setLessons () {
return (dispatch, getState) => {
let {currentLesson, lesson: {limit, offset, hasMore, loading}} = getState().home
if (hasMore && !loading) {
dispatch({type: Types.GET_LESSONS})
dispatch({
type: Types.GET_LESSONS_SUCCESS,
payload: getLessons(limit, offset, currentLesson)
})
}
}
}
}
export default action
- store/actions/session.js
import * as Types from '../action-types'
import { reg, login, validate } from "../../api/session";
let actions = {
toReg (userInfo, push) {
return (dispatch) => {
reg(userInfo).then((res) => {
dispatch({
type: Types.SET_USER_INFO,
payload: res
})
if (res.code === 0) {
push('/login')
}
})
}
},
toLogin (userInfo, push) {
return (dispatch) => {
login(userInfo).then((res) => {
dispatch({
type: Types.SET_USER_INFO,
payload: res
})
if (res.code === 0) {
push('/profile')
}
})
}
},
toValidate () {
return (dispatch) => {
dispatch({
type: Types.SET_USER_INFO,
payload: validate()
})
}
}
}
export default actions
- container/Home/Home.js
import React, { Component } from 'react'
import { connect } from 'react-redux'
import actions from '../../store/actions/home'
import './index.less'
import HomeHeader from "./HomeHeader";
import HomeSlider from "./HomeSlider";
import HomeList from "./HomeList";
import { loadMore, pullRefresh } from "../../utils";
import Loading from "../../component/Loading/Loading";
class Home extends Component {
changeType = (value) => {
// console.log(value)
this.props.setCurrentLess(value)
}
componentDidMount () {
// 页面一加载就去请求轮播图
this.props.setSliders()
this.props.setLessons() // 获取课程列表
loadMore(this.el, this.props.setLessons)
pullRefresh(this.el, this.props.refresh)
}
render () {
return (<div>
<HomeHeader changeType={this.changeType} />
<div className="content" ref={(el) => this.el = el}>
{
this.props.slider.loading ? <Loading /> : <HomeSlider list={this.props.slider.list}/>
}
<div className="container">
<h3>
<i className='iconfont icon-wode_kecheng'></i>
我的课程
</h3>
<HomeList list={this.props.lesson.list} />
{
this.props.lesson.loading ? <Loading /> : null
}
<button onClick={() => {
this.props.setLessons()
}}>加载更多</button>
</div>
</div>
</div>)
}
}
export default connect(state => ({...state.home}), actions)(Home)
- container/HomeHeader.js
import React, {Component} from 'react'
import logo from '../../images/logo.png'
import {Transition} from 'react-transition-group'
const duration = 150
const defaultStyle = {
transition: `opacity ${duration}ms ease-in-out`,
opacity: 0,
display: 'none'
}
const transitionStyles = {
entering: {opacity: 0},
entered: {opacity: 1}
// entering: {opacity: 0, display: 'block'},
// entered: {opacity: 1, display: 'block'}
}
export default class HomeHeader extends Component {
constructor (props, context) {
super ()
this.state = {
isShow: false
}
}
changeShow = () => {
this.setState({
isShow: !this.state.isShow
})
}
changeType = (e) => {
// console.log(e.target.dataset.type)
this.props.changeType(e.target.dataset.type)
this.changeShow()
}
render () {
return (<div className='home-header'>
<div className="home-header-logo">
<img src={logo} alt=""/>
<div className="home-header-btn" onClick={this.changeShow}>
{
this.state.isShow
? <i className="iconfont icon-guanbi"></i>
: <i className="iconfont icon-liebiao"></i>
}
</div>
</div>
<Transition in={this.state.isShow}
onEnter={(node) => (node.style.display = 'block')}
onExit={(node) => (node.style.display = 'none')}
timeout={duration}>
{
state => (<ul className="home-header-list" style={{
...defaultStyle,
...transitionStyles[state]
}} onClick={this.changeType}>
<li data-type="0">全部课程</li>
<li data-type="1">React 课程</li>
<li data-type="2">Vue 课程</li>
</ul>)
}
</Transition>
</div>)
}
}
- container/Home/HomeList.js
import React, { Component } from 'react'
export default class HomeList extends Component {
render () {
return (<div className='home-list'>
<ul>
{
this.props.list.map((item, index) => {
return <li key={index}>
<img src={item.url} alt=""/>
<p>{item.title}</p>
<span>{item.price}</span>
</li>
})
}
</ul>
</div>)
}
}
- container/Home/HomeSlider.js
import React, { Component } from 'react'
import ReactSwipe from 'react-swipe'
export default class HomeSlider extends Component {
constructor () {
super()
this.state = {
index: 0
}
}
render () {
let reactSwipeEl
let that = this
let opts = {
continuous: true,
auto: 1000,
callback: (index) => {
// console.log(index)
// this.setState({index})
}
}
return (<div className="home-slider">
<ReactSwipe
className="carousel"
swipeOptions={opts}
ref={el => (reactSwipeEl = el)}
>
{
this.props.list.map((item, index) => {
return <div key={index}>
<img src={item} alt=""/>
</div>
})
}
</ReactSwipe>
<ul className='home-slider-dots'>
{
this.props.list.map((img, i) => {
return <li key={i} className={this.state.index === i ? 'active' : '' }></li>
})
}
</ul>
</div>)
}
}
- container/Home/index.less
.home-header {
background: #2a2a2a;
color: #fff;
height: 56px;
line-height: 56px;
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 9999;
.home-header-logo {
img {
width: 105px;
height: 30px;
margin-top: 13px;
margin-left: 7px;
}
.home-header-btn {
float: right;
margin-right: 11px;
}
}
.home-header-list {
position: absolute;
top: 50px;
left: 0;
width: 100%;
li {
width: 100%;
line-height: 46px;
background: #2a2a2a;
border-top: 1px solid #464646;
text-align: center;
}
}
}
.home-slider {
height: 170px;
position: relative;
text-align: center;
div {
height: 100%;
img {
width: 100%;
height: 100%;
}
}
.home-slider-dots {
position: absolute;
bottom: 10px;
width: 100%;
text-align: center;
li {
display: inline-block;
margin-right: 5px;
width: 10px;
height: 10px;
border-radius: 50%;
background: #fff;
&.active {
background: red;
}
}
}
}
h3 {
line-height: 53px;
color: #3b3b3b;
}
.home-list {
text-align: center;
li {
box-shadow: 1px 1px 3px #c1c1c1, 1px 1px 3px 2px #c1c1c1;
margin-bottom: 17px;
img {
width: 100%;
height: 170px;
}
p {
color: #777;
line-height: 40px;
}
span {
color: red;
line-height: 30px;
}
}
}
【发上等愿,结中等缘,享下等福,择高处立,寻平处住,向宽处行】