一、项目结构
|- index.html|- package.json|- webpack.config.jssrc│ 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│└─reducershome.jsindex.jssession.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 } = eleclearTimeout(ele.timer)ele.timer = setTimeout(() => {// ?? 盒子模型关系if (scrollTop + offsetHeight + 20 > scrollHeight) {cb()}}, 30)}, false)}export const pullRefresh = (ele, cb) => {// 当前元素的 offsetTop 偏移量,如果正在下拉,触发无效let offsetTop = ele.offsetToplet distance = 0ele.addEventListener('touchstart', (e) => {let startY = e.touches[0].pageYconsole.log(startY)let touchmove = function (e) {// 计算手指移动的距离let moveY = e.touches[0].pageY// console.log(moveY - startY)if (moveY - startY > 0) { // 正在下来刷新// 确保向下拉distance = moveY - startYif (distance > 50) {distance = 50return 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 = nullif (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 = storeexport 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().homeif (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 = 150const 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 reactSwipeEllet that = thislet opts = {continuous: true,auto: 1000,callback: (index) => {// console.log(index)// this.setState({index})}}return (<div className="home-slider"><ReactSwipeclassName="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;}}}
【发上等愿,结中等缘,享下等福,择高处立,寻平处住,向宽处行】
