一、项目结构
|- 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 │ └─reducers home.js index.js session.js
二、项目代码
{ "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" }}
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' }) ]}
<!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>
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'))
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) } })}
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
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'
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
import { combineReducers } from 'redux'import home from './home'import session from './session'export default combineReducers({ home, session})
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
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
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)
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 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; } }}