一、项目结构

  1. |- index.html
  2. |- package.json
  3. |- webpack.config.js
  4. src
  5. index.js
  6. utils.js
  7. 原型图.png
  8. ├─api
  9. home.js
  10. session.js
  11. └─config
  12. index.js
  13. ├─common
  14. index.less
  15. ├─component
  16. ├─Aleart
  17. Alert.js
  18. index.less
  19. ├─Header
  20. Header.js
  21. index.less
  22. ├─Loading
  23. index.less
  24. Loading.js
  25. └─TabBar
  26. index.less
  27. TabBar.js
  28. ├─container
  29. ├─Home
  30. Home.js
  31. HomeHeader.js
  32. HomeList.js
  33. HomeSlider.js
  34. index.less
  35. ├─Lesson
  36. Lesson.js
  37. ├─Login
  38. index.less
  39. Login.js
  40. ├─Profile
  41. index.less
  42. Profile.js
  43. └─Reg
  44. index.less
  45. Reg.js
  46. ├─images
  47. default.png
  48. login_bg.png
  49. logo.png
  50. profile.png
  51. └─store
  52. action-types.js
  53. index.js
  54. ├─actions
  55. home.js
  56. session.js
  57. └─reducers
  58. home.js
  59. index.js
  60. session.js

二、项目代码

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