├── .gitignore
├── app
├── config
│ ├── README.md
│ └── localStorageKey.jsx
├── containers
│ ├── Home
│ │ ├── README.md
│ │ ├── subPage
│ │ │ ├── style.scss
│ │ │ ├── Reco.jsx
│ │ │ └── GuessInterest.jsx
│ │ └── index.jsx
│ ├── City
│ │ ├── README.md
│ │ ├── style.scss
│ │ └── index.jsx
│ ├── Detail
│ │ ├── README.md
│ │ └── index.jsx
│ ├── Search
│ │ ├── README.md
│ │ ├── index.jsx
│ │ └── subPage
│ │ │ └── SearchList.jsx
│ ├── README.md
│ ├── User
│ │ └── index.jsx
│ ├── 404.jsx
│ └── index.jsx
├── fetch
│ ├── README.md
│ ├── get.jsx
│ ├── Home
│ │ └── index.jsx
│ ├── Search
│ │ └── index.jsx
│ └── post.jsx
├── util
│ ├── README.md
│ └── localStorage.jsx
├── components
│ ├── CityList
│ │ ├── README.md
│ │ ├── style.scss
│ │ └── index.jsx
│ ├── Category
│ │ ├── README.md
│ │ ├── style.scss
│ │ └── index.jsx
│ ├── Header
│ │ ├── README.md
│ │ ├── style.scss
│ │ └── index.jsx
│ ├── HomeReco
│ │ ├── README.md
│ │ ├── style.scss
│ │ └── index.jsx
│ ├── LoadMore
│ │ ├── README.md
│ │ ├── style.scss
│ │ └── index.jsx
│ ├── SearchInput
│ │ ├── README.md
│ │ ├── style.scss
│ │ └── index.jsx
│ ├── CurrentCity
│ │ ├── README.md
│ │ ├── style.scss
│ │ └── index.jsx
│ ├── GoodsList
│ │ ├── README.md
│ │ ├── index.jsx
│ │ ├── Item
│ │ │ └── index.jsx
│ │ └── style.scss
│ ├── SearchHeader
│ │ ├── README.md
│ │ ├── style.scss
│ │ └── index.jsx
│ └── HomeHeader
│ │ ├── README.md
│ │ ├── style.scss
│ │ └── index.jsx
├── router
│ ├── README.md
│ └── routerMap.jsx
├── reducers
│ ├── README.md
│ ├── index.jsx
│ └── userInfo.jsx
├── actions
│ ├── README.md
│ └── userInfo.jsx
├── constants
│ ├── README.md
│ └── userInfo.jsx
├── static
│ ├── fonts
│ │ ├── iconfont.eot
│ │ ├── iconfont.ttf
│ │ ├── iconfont.woff
│ │ └── iconfont.svg
│ └── scss
│ │ ├── common.scss
│ │ ├── reset.scss
│ │ └── font.scss
├── store
│ └── configureStore.jsx
├── index.html
└── index.jsx
├── mock
├── README.md
├── Home
│ ├── reco.js
│ └── guessInterest.js
├── Search
│ └── searchList.js
└── server.js
├── postcss.config.js
├── .babelrc
├── .eslintrc
├── README.md
├── package.json
├── webpack.config.js
└── webpack.config.production.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
--------------------------------------------------------------------------------
/app/config/README.md:
--------------------------------------------------------------------------------
1 | 常用变量名的缩写配置
--------------------------------------------------------------------------------
/app/containers/Home/README.md:
--------------------------------------------------------------------------------
1 | 主页
--------------------------------------------------------------------------------
/app/fetch/README.md:
--------------------------------------------------------------------------------
1 | get / post 数据
--------------------------------------------------------------------------------
/app/util/README.md:
--------------------------------------------------------------------------------
1 | 全站需要的一些工具函数
--------------------------------------------------------------------------------
/mock/README.md:
--------------------------------------------------------------------------------
1 | 模拟get / post 数据
--------------------------------------------------------------------------------
/app/components/CityList/README.md:
--------------------------------------------------------------------------------
1 | 城市列表
--------------------------------------------------------------------------------
/app/containers/City/README.md:
--------------------------------------------------------------------------------
1 | 选择城市页面
--------------------------------------------------------------------------------
/app/containers/Detail/README.md:
--------------------------------------------------------------------------------
1 | 详细页
--------------------------------------------------------------------------------
/app/containers/Search/README.md:
--------------------------------------------------------------------------------
1 | 搜索页
--------------------------------------------------------------------------------
/app/components/Category/README.md:
--------------------------------------------------------------------------------
1 | 轮播图形式的类别组件
--------------------------------------------------------------------------------
/app/components/Header/README.md:
--------------------------------------------------------------------------------
1 | 全站Header公共组件
--------------------------------------------------------------------------------
/app/components/HomeReco/README.md:
--------------------------------------------------------------------------------
1 | 首页广告推荐位组件
--------------------------------------------------------------------------------
/app/components/LoadMore/README.md:
--------------------------------------------------------------------------------
1 | 加载更多组件
--------------------------------------------------------------------------------
/app/components/SearchInput/README.md:
--------------------------------------------------------------------------------
1 | 搜索框
--------------------------------------------------------------------------------
/app/router/README.md:
--------------------------------------------------------------------------------
1 | 这是路由map
2 | 所有的路由都在这里
--------------------------------------------------------------------------------
/app/components/CurrentCity/README.md:
--------------------------------------------------------------------------------
1 | 显示当前城市的大白块组件
--------------------------------------------------------------------------------
/app/components/GoodsList/README.md:
--------------------------------------------------------------------------------
1 | Home页面的猜你喜欢组件
--------------------------------------------------------------------------------
/app/components/SearchHeader/README.md:
--------------------------------------------------------------------------------
1 | 搜索页的Header
--------------------------------------------------------------------------------
/app/reducers/README.md:
--------------------------------------------------------------------------------
1 | 存放react-redux的readucer
--------------------------------------------------------------------------------
/app/components/HomeHeader/README.md:
--------------------------------------------------------------------------------
1 | Home页面用的Header
--------------------------------------------------------------------------------
/app/actions/README.md:
--------------------------------------------------------------------------------
1 | react-redux的actions
2 | 和reducer一一对应
--------------------------------------------------------------------------------
/app/constants/README.md:
--------------------------------------------------------------------------------
1 | react-redux的常用变量名映射
2 | 与reducer一一对应
--------------------------------------------------------------------------------
/app/containers/README.md:
--------------------------------------------------------------------------------
1 | 一个文件夹对应一个页面
2 | subpage中为该页面需要的smart组件
--------------------------------------------------------------------------------
/app/config/localStorageKey.jsx:
--------------------------------------------------------------------------------
1 | export const CITYNAME = 'USER_CURRENT_CITY_NAME';
--------------------------------------------------------------------------------
/app/constants/userInfo.jsx:
--------------------------------------------------------------------------------
1 | export const USERINFO_UPDATE_CITYNAME = 'USERINFO_UPDATE_CITYNAME';
--------------------------------------------------------------------------------
/app/components/SearchHeader/style.scss:
--------------------------------------------------------------------------------
1 | .searchHeader {
2 | .searchInput {
3 | margin-left: 30px;
4 | }
5 | }
--------------------------------------------------------------------------------
/app/static/fonts/iconfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeLittlePrince/react-webapp-spa/HEAD/app/static/fonts/iconfont.eot
--------------------------------------------------------------------------------
/app/static/fonts/iconfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeLittlePrince/react-webapp-spa/HEAD/app/static/fonts/iconfont.ttf
--------------------------------------------------------------------------------
/app/static/fonts/iconfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeLittlePrince/react-webapp-spa/HEAD/app/static/fonts/iconfont.woff
--------------------------------------------------------------------------------
/app/components/LoadMore/style.scss:
--------------------------------------------------------------------------------
1 | .loadMore {
2 | font-size: 18px;
3 | line-height: 40px;
4 | color: #999;
5 | text-align: center;
6 | }
--------------------------------------------------------------------------------
/app/containers/City/style.scss:
--------------------------------------------------------------------------------
1 | .city {
2 | .header {
3 | h3 {
4 | font-size: 16px;
5 | padding: 5px 0;
6 | }
7 | }
8 | }
--------------------------------------------------------------------------------
/app/reducers/index.jsx:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 |
3 | import userInfo from './userInfo';
4 |
5 | export default combineReducers({
6 | userInfo
7 | });
--------------------------------------------------------------------------------
/app/actions/userInfo.jsx:
--------------------------------------------------------------------------------
1 | import * as actionTypes from 'constants/userInfo';
2 |
3 | export function updateUserCity(cityName) {
4 | return { type: actionTypes.USERINFO_UPDATE_CITYNAME, cityName };
5 | }
--------------------------------------------------------------------------------
/app/components/CurrentCity/style.scss:
--------------------------------------------------------------------------------
1 | .currentCity {
2 | color: #333;
3 | padding: 30px;
4 | text-align: center;
5 | font-size: 30px;
6 | background-color: #fff;
7 | border-bottom: 1px solid #e0e0e0;
8 | }
--------------------------------------------------------------------------------
/app/containers/User/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class User extends React.PureComponent {
4 | render() {
5 | return (
6 |
User
7 | );
8 | }
9 | }
10 |
11 | export default User;
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require('autoprefixer')
4 | ],
5 | // 配置autoprefix
6 | browsers: [
7 | "iOS >= 8",
8 | "Firefox >= 20",
9 | "Android > 4.4",
10 | "ie >= 9"
11 | ]
12 | }
--------------------------------------------------------------------------------
/app/containers/404.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class NotFound extends React.PureComponent {
4 | render() {
5 | return (
6 |
7 |
Not Found~~~
8 |
9 | );
10 | }
11 | }
12 |
13 | export default NotFound;
--------------------------------------------------------------------------------
/app/fetch/get.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * 封装fetch的get请求
3 | * @param {*string} url 请求地址
4 | * @return fetch返回的数据
5 | */
6 | export default function get(url) {
7 | return fetch(url, {
8 | headers: {
9 | 'Accept': 'application/json, text/plain, */*',
10 | }
11 | });
12 | }
--------------------------------------------------------------------------------
/app/containers/Home/subPage/style.scss:
--------------------------------------------------------------------------------
1 | .home-recommend {
2 | margin-top: 15px;
3 | }
4 |
5 | .home-guessInterest {
6 | margin-top: 15px;
7 | h3 {
8 | background-color: #fff;
9 | font-size: 18px;
10 | border-bottom: 1px solid #eee;
11 | padding: 10px;
12 | }
13 | }
--------------------------------------------------------------------------------
/app/store/configureStore.jsx:
--------------------------------------------------------------------------------
1 | import { createStore } from 'redux';
2 | import Reducers from 'reducers';
3 |
4 | export default function () {
5 | return createStore(
6 | Reducers,
7 | window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() // 这句是为了chrome redux插件用的
8 | );
9 | }
--------------------------------------------------------------------------------
/app/components/CurrentCity/index.jsx:
--------------------------------------------------------------------------------
1 | import './style';
2 | import React from 'react';
3 |
4 | class CurrentCity extends React.PureComponent {
5 | render() {
6 | return (
7 | {this.props.cityName}
8 | );
9 | }
10 | }
11 |
12 | export default CurrentCity;
--------------------------------------------------------------------------------
/app/fetch/Home/index.jsx:
--------------------------------------------------------------------------------
1 | import get from '../get';
2 |
3 | export function getRecoData() {
4 | return get('/api/home/reco');
5 | }
6 |
7 | export function getGuessInterestData(city, page) {
8 | return get('api/home/guessInterest/'
9 | + encodeURIComponent(city)
10 | + '/'
11 | + page);
12 | }
--------------------------------------------------------------------------------
/app/containers/Detail/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | class Detail extends React.Component {
3 | render() {
4 | return (
5 |
6 | This is Detail! And the param is {this.props.match.params.id}
7 |
8 | );
9 | }
10 | }
11 |
12 | export default Detail;
--------------------------------------------------------------------------------
/app/static/scss/common.scss:
--------------------------------------------------------------------------------
1 | @import './reset.scss';
2 | @import './font.scss';
3 |
4 | body {
5 | background-color: #f6f6f6;
6 | line-height: 1.2;
7 | }
8 |
9 | .l-clearfix:after {
10 | display: table;
11 | content: '';
12 | clear: both;
13 | }
14 | .l-left {
15 | float: left;
16 | }
17 | .l-right {
18 | float: right;
19 | }
--------------------------------------------------------------------------------
/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | react-webapp
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/components/Header/style.scss:
--------------------------------------------------------------------------------
1 | .header {
2 | position: relative;
3 | background-color: #323232;
4 | padding: 10px;
5 | color: #fff;
6 | font-size: 16px;
7 | line-height: 1;
8 | text-align: center;
9 | .icon-arrow-left {
10 | position: absolute;
11 | left: 10px;
12 | font-size: 20px;
13 | margin: 5px 0;
14 | }
15 | }
--------------------------------------------------------------------------------
/app/reducers/userInfo.jsx:
--------------------------------------------------------------------------------
1 | const initState = {};
2 |
3 | export default function userInfo(state = initState, action) {
4 | switch (action.type) {
5 | case 'USERINFO_UPDATE_CITYNAME':
6 | return {
7 | ...state,
8 | cityName: action.cityName
9 | };
10 | default:
11 | return state;
12 | }
13 | }
--------------------------------------------------------------------------------
/app/fetch/Search/index.jsx:
--------------------------------------------------------------------------------
1 | import get from '../get';
2 |
3 | export function getSearchListData(city, page, category, keywords) {
4 | let url = 'api/search/SearchList/'
5 | + encodeURIComponent(city) + '/'
6 | + page + '/'
7 | + encodeURIComponent(category);
8 | if (keywords) {
9 | url += '/' + encodeURIComponent(keywords);
10 | }
11 | return get(url);
12 | }
--------------------------------------------------------------------------------
/app/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import RouterMap from './router/routerMap';
4 | import { Provider } from 'react-redux';
5 | import configureStore from './store/configureStore';
6 |
7 | const store = configureStore();
8 |
9 | ReactDOM.render(
10 |
11 |
12 | ,
13 | document.getElementById('app')
14 | );
--------------------------------------------------------------------------------
/app/components/HomeHeader/style.scss:
--------------------------------------------------------------------------------
1 | .home-header {
2 | background-color: #323232;
3 | padding: 10px;
4 | color: #fff;
5 | font-size: 16px;
6 | line-height: 1;
7 | .location {
8 | color: #fff;
9 | width: 60px;
10 | text-align: left;
11 | margin: 5px 0;
12 | }
13 | .searchInput {
14 | width: auto;
15 | margin: 0 30px 0 60px;
16 | }
17 | .user {
18 | margin: 5px 0;
19 | }
20 | }
--------------------------------------------------------------------------------
/app/components/HomeReco/style.scss:
--------------------------------------------------------------------------------
1 | .home-recommend {
2 | background-color: #fff;
3 | h3 {
4 | font-size: 18px;
5 | border-bottom: 1px solid #eee;
6 | padding: 10px;
7 | }
8 | ul {
9 | padding: 10px;
10 | font-size: 0;
11 | li {
12 | width: 33.3%;
13 | display: inline-block;
14 | padding: 5px;
15 | img {
16 | width: 100%;
17 | }
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/app/components/Header/index.jsx:
--------------------------------------------------------------------------------
1 | import './style';
2 | import React from 'react';
3 |
4 | class Header extends React.PureComponent {
5 |
6 | /**
7 | * 返回上一页
8 | */
9 | clickHandler() {
10 | window.history.back();
11 | }
12 |
13 | render() {
14 | return (
15 |
19 | );
20 | }
21 | }
22 |
23 | export default Header;
--------------------------------------------------------------------------------
/app/components/CityList/style.scss:
--------------------------------------------------------------------------------
1 | .cityList {
2 | color: #333;
3 | padding: 15px 15px 20px;
4 | background-color: #fff;
5 | h3 {
6 | font-size: 18px;
7 | }
8 | li {
9 | float: left;
10 | width: 33.3%;
11 | margin-top: 20px;
12 | text-align: center;
13 | span {
14 | display: inline-block;
15 | width: 90%;
16 | font: 14px;
17 | line-height: 2;
18 | color: #fff;
19 | background-color: #ff6fa2;
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "react",
4 | [
5 | "env",
6 | {
7 | "targets": {
8 | "browsers": [
9 | "> 1%",
10 | "last 2 versions",
11 | "not ie <= 8"
12 | ]
13 | },
14 | "useBuiltIns": true
15 | }
16 | ]
17 | ],
18 | "plugins": [
19 | "transform-object-rest-spread",
20 | "react-html-attrs",
21 | "syntax-dynamic-import"
22 | ]
23 | }
--------------------------------------------------------------------------------
/app/components/SearchInput/style.scss:
--------------------------------------------------------------------------------
1 | .searchInput {
2 | .search {
3 | background-color: #fff;
4 | border-radius: 15px;
5 | overflow: hidden;
6 | padding: 5px 10px;
7 | i {
8 | font-size: 16px;
9 | color: #ccc;
10 | vertical-align: middle;
11 | }
12 | input {
13 | width: 90%;
14 | padding-left: 5px;
15 | font-size: 14px;
16 | outline: none;
17 | border: none;
18 | font-weight: normal;
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/app/containers/Search/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import SearchHeader from 'components/SearchHeader';
3 | import SearchList from './subPage/SearchList';
4 |
5 | class Search extends React.PureComponent {
6 | render() {
7 | const params = this.props.match.params;
8 | return (
9 |
10 |
11 |
12 |
13 | );
14 | }
15 | }
16 |
17 | export default Search;
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "commonjs": true,
5 | "es6": true
6 | },
7 | "extends": "eslint:recommended",
8 | "parserOptions": {
9 | "ecmaFeatures": {
10 | "experimentalObjectRestSpread": true,
11 | "jsx": true
12 | },
13 | "sourceType": "module"
14 | },
15 | "plugins": [
16 | "react"
17 | ],
18 | "parser": "babel-eslint",
19 | "rules": {
20 | "indent": 0,
21 | "linebreak-style": 0,
22 | "quotes": 0,
23 | "no-extra-semi": 0,
24 | "no-unused-expressions": 0,
25 | "no-unused-vars": 0,
26 | "no-console": 0,
27 | "no-mixed-spaces-and-tabs": 0,
28 | "no-cond-assign": 0
29 | }
30 | }
--------------------------------------------------------------------------------
/app/components/GoodsList/index.jsx:
--------------------------------------------------------------------------------
1 | import './style';
2 | import React from 'react';
3 | import Item from './Item';
4 | import PropTypes from 'prop-types';
5 |
6 | class GoodsList extends React.PureComponent {
7 | render() {
8 | return (
9 |
10 |
11 | {
12 | this.props.data.map((item, index) => {
13 | return
14 | })
15 | }
16 |
17 |
18 | );
19 | }
20 | }
21 |
22 | GoodsList.propTypes = {
23 | data: PropTypes.array
24 | };
25 |
26 | GoodsList.defaultProps = {
27 | data: []
28 | };
29 |
30 | export default GoodsList;
--------------------------------------------------------------------------------
/app/util/localStorage.jsx:
--------------------------------------------------------------------------------
1 | export default {
2 | getItem: key => {
3 | let value;
4 | try {
5 | value = localStorage.getItem(key)
6 | } catch (ex) {
7 | // 开发环境下提示error
8 | if (__DEV__) {
9 | console.error('localStorage.getItem报错, ', ex.message)
10 | }
11 | }
12 | return value;
13 | },
14 | setItem: (key, value) => {
15 | try {
16 | // ios safari 无痕模式下,直接使用 localStorage.setItem 会报错
17 | localStorage.setItem(key, value)
18 | } catch (ex) {
19 | // 开发环境下提示 error
20 | /*global __DEV__*/
21 | if (__DEV__) {
22 | console.error('localStorage.setItem报错, ', ex.message)
23 | }
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/app/components/Category/style.scss:
--------------------------------------------------------------------------------
1 | .home-category {
2 | background-color: #333;
3 | padding-bottom: 10px;
4 | .items {
5 | width: 100%;
6 | height: auto;
7 | text-align: center;
8 | font-size: 0;
9 | color: #111;
10 | img {
11 | width: 100%;
12 | }
13 | }
14 | .index{
15 | margin-top: 10px;
16 | width: 100%;
17 | height: auto;
18 | text-align: center;
19 | li {
20 | list-style: none;
21 | display: inline-block;
22 | height: 8px;
23 | width: 8px;
24 | border-radius: 4px;
25 | background-color: #fff;
26 | margin: 0 3px;
27 | &.active {
28 | background-color: #ff6fa2;
29 | }
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/app/containers/Home/index.jsx:
--------------------------------------------------------------------------------
1 | import './subPage/style';
2 | import React from 'react';
3 | import { connect } from 'react-redux';
4 | import HomeHeader from 'components/HomeHeader';
5 | import Category from 'components/Category';
6 | import Reco from './subPage/Reco';
7 | import GuessInterest from './subPage/GuessInterest';
8 |
9 | class Home extends React.PureComponent {
10 | render() {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 | }
21 |
22 | function mapStateToProps(state) {
23 | return {
24 | cityName: state.userInfo.cityName
25 | };
26 | }
27 |
28 | export default connect(mapStateToProps)(Home);
--------------------------------------------------------------------------------
/app/static/scss/reset.scss:
--------------------------------------------------------------------------------
1 | *, *:before, *:after {
2 | /* suppressing the tap highlight */
3 | -webkit-tap-highlight-color: rgba(0,0,0,0);
4 |
5 | /* this is a personal preference */
6 | box-sizing: border-box;
7 | padding: 0;
8 | margin: 0;
9 | -webkit-font-smoothing: antialiased;
10 | }
11 |
12 | *:focus {
13 | /* the default outline doesn't play well with a mobile application,
14 | I usually start without it,
15 | but don't forget to research further to make your mobile app accessible. */
16 | outline: 0;
17 | }
18 |
19 | input {
20 | border-radius: 0;
21 | }
22 |
23 | html, body {
24 | /* we don't want to allow users to select text everywhere,
25 | you can enable it on the places you think appropriate */
26 | user-select: none;
27 | }
28 |
29 | /* 个人需要 */
30 | a {
31 | text-decoration: none;
32 | }
33 | li {
34 | list-style: none;
35 | }
--------------------------------------------------------------------------------
/app/components/SearchHeader/index.jsx:
--------------------------------------------------------------------------------
1 | import './style';
2 | import React from 'react';
3 | import Header from 'components/Header';
4 | import SearchInput from 'components/SearchInput';
5 |
6 | class SearchHeader extends React.PureComponent {
7 | submitHandler(keywords) {
8 | let category = this.props.category;
9 | // https://github.com/ReactTraining/react-router/issues/1982 解决人:PFight
10 | // 解决react-router v4改变查询参数并不会刷新或者说重载组件的问题
11 | this.props.history.push('/empty');
12 | setTimeout(() => {
13 | this.props.history.replace('/search/' + category + '/' + encodeURIComponent(keywords));
14 | });
15 | }
16 | render() {
17 | return (
18 |
23 | );
24 | }
25 | }
26 |
27 | export default SearchHeader;
--------------------------------------------------------------------------------
/app/components/GoodsList/Item/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | class Item extends React.PureComponent {
5 | render() {
6 | let { item } = this.props;
7 | return (
8 |
9 |
10 |
11 |

12 |
13 |
14 |
15 |
{item.title}
16 | {item.created}
17 |
18 |
{item.desc}
19 |
¥{item.price}
20 |
21 |
22 |
23 | );
24 | }
25 | }
26 |
27 | Item.propTypes = {
28 | item: PropTypes.object.isRequired
29 | };
30 |
31 | export default Item;
--------------------------------------------------------------------------------
/app/components/HomeReco/index.jsx:
--------------------------------------------------------------------------------
1 | import './style';
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 |
5 | class HomeReco extends React.PureComponent {
6 | render() {
7 | return (
8 |
9 |
{this.props.title}
10 |
22 |
23 | );
24 | }
25 | }
26 |
27 | HomeReco.propTypes = {
28 | title: PropTypes.string,
29 | recoData: PropTypes.array
30 | };
31 |
32 | HomeReco.defaultProps = {
33 | title: '',
34 | recoData: []
35 | };
36 |
37 | export default HomeReco;
--------------------------------------------------------------------------------
/mock/Home/reco.js:
--------------------------------------------------------------------------------
1 | module.exports = [
2 | {
3 | title: '单肩包',
4 | img: 'http://ou41niivx.bkt.clouddn.com/bag.png?imageView2/2/w/200',
5 | link: 'https://bcy.net/zhipin/detail/7883'
6 | },
7 | {
8 | title: '明信片',
9 | img: 'http://ou41niivx.bkt.clouddn.com/postcard.png?imageView2/2/w/200',
10 | link: 'https://bcy.net/zhipin/detail/8179'
11 | },
12 | {
13 | title: '隔热杯',
14 | img: 'http://ou41niivx.bkt.clouddn.com/cup.png?imageView2/2/w/200',
15 | link: 'https://bcy.net/zhipin/detail/7495'
16 | },
17 | {
18 | title: '抱枕',
19 | img: 'http://ou41niivx.bkt.clouddn.com/pillow.png?imageView2/2/w/200',
20 | link: 'https://bcy.net/zhipin/detail/3507'
21 | },
22 | {
23 | title: '手机壳',
24 | img: 'http://ou41niivx.bkt.clouddn.com/phoneshell.png?imageView2/2/w/200',
25 | link: 'https://bcy.net/zhipin/detail/7358'
26 | },
27 | {
28 | title: '框画',
29 | img: 'http://ou41niivx.bkt.clouddn.com/picFrame.png?imageView2/2/w/200',
30 | link: 'https://bcy.net/zhipin/detail/7015'
31 | }
32 | ]
--------------------------------------------------------------------------------
/app/fetch/post.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * 将对象转换为a=1&b=2这样的字符串
3 | * @param {*object} obj 需要转换的对象
4 | * @return {*string} 返回转换结果
5 | */
6 | function obj2params(obj) {
7 | var result = '';
8 | var item;
9 | for (item in obj) {
10 | result += '&' + item + '=' + encodeURIComponent(obj[item]);
11 | }
12 | if (result) {
13 | result = result.slice(1);
14 | }
15 | return result;
16 | }
17 | /**
18 | * 对fetch的post封装
19 | * @param {*string} url 请求地址
20 | * @param {*object} paramsObj 请求附带的参数
21 | * @return fetch返回的数据
22 | */
23 | export default function post(url, paramsObj) {
24 | return fetch(url, {
25 | credentials: 'include',
26 | /*
27 | Credentials' Description:
28 | omit: Never send cookies. (default)
29 | same-origin: Send cookies if the URL is on the same origin as the calling script.
30 | include: Always send cookies, even for cross- origin calls.
31 | */
32 | headers: {
33 | 'Accept': 'application/json, text/plain, */*',
34 | 'Content-Type': 'application/x-www-form-urlencoded'
35 | },
36 | body: obj2params(paramsObj)
37 | })
38 | }
--------------------------------------------------------------------------------
/app/components/HomeHeader/index.jsx:
--------------------------------------------------------------------------------
1 | import './style';
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | import { Link } from 'react-router-dom';
5 | import SearchInput from 'components/SearchInput';
6 |
7 | class HomeHeader extends React.PureComponent {
8 |
9 | submitHandler(keywords) {
10 | this.props.history.push('/search/' + 'all/' + encodeURIComponent(keywords))
11 | }
12 |
13 | render() {
14 | return (
15 |
26 | );
27 | }
28 | }
29 |
30 | HomeHeader.propTypes = {
31 | cityName: PropTypes.string
32 | };
33 |
34 | HomeHeader.defaultProps = {
35 | cityName: '杭州'
36 | };
37 |
38 | export default HomeHeader;
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 如何运行
2 | 1. 开发环境:先运行npm run mock开启本地模拟数据,然后npm run dev
3 | 1. 发布环境:运行npm run build
4 |
5 | # react-webapp-spa
6 | #### 目前已完成:
7 | 1. webpack v3 前端工程:dev、mock、build
8 | - dev为本地开发环境,使用eslint、postcss、webpack-dev-server等工具
9 | - mock为本地模拟数据,通过koa2来处理本地的前端请求
10 | - build是发布环境,npm run build以后会生成build目录及相关文件。会将package.json里的dependencies打包成vendor.[hash].js,页面中js代码打包为app.[hash].js,scss打包为app.[hash].css,给图片和font加hash,然后压缩CSS、JS、图片。
11 | 1. react + react-redux + react-router v4 实现页面首页、城市页、搜索结果页、轮播图、下拉加载更多、搜索等功能。
12 | 1. react 热更新
13 | 1. 使用dynamic import将JS按页面代码分离,加速了首屏显示
14 | 1. scope hosting
15 |
16 | #### 之后要做:
17 | 1. SSR,为了SEO和防止一开始白屏
18 | 1. 移植项目到react-naive
19 | 1. 发布几篇详细的文章
20 |
21 | #### 预览图
22 |
23 | #### 1
24 |
25 |
26 | #### 2
27 |
29 |
30 | #### 3
31 |
33 |
34 | #### 4
35 |
37 |
--------------------------------------------------------------------------------
/app/containers/Home/subPage/Reco.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import HomeReco from 'components/HomeReco';
3 | import { getRecoData } from 'fetch/Home';
4 |
5 | class AD extends React.PureComponent {
6 | constructor(props) {
7 | super(props);
8 | this.state = {
9 | recoData: []
10 | }
11 | }
12 |
13 | /**
14 | * fetch推荐数据,然后更新state
15 | */
16 | _getRecoData() {
17 | getRecoData()
18 | .then(res => {
19 | return res.json();
20 | })
21 | .then(json => {
22 | this.setState({
23 | recoData: json
24 | });
25 | })
26 | .catch(ex => {
27 | /*global __DEV__*/
28 | if (__DEV__) {
29 | // 虽然这个项目中数据是前端定的,几乎不可能报错
30 | // 但是,如果proxy到线下后端的服务器拿模拟数据,就会可能出现数据结构不符等问题
31 | // 所以,fetch数据一定要catch一下
32 | console.error('首页半次元周边推荐数据报错:', ex.message);
33 | }
34 | });
35 | }
36 |
37 | componentDidMount() {
38 | this._getRecoData();
39 | }
40 |
41 | render() {
42 | return (
43 |
44 | {this.state.recoData.length
45 | ?
46 | :
加载中...
47 | }
48 |
49 | );
50 | }
51 | }
52 |
53 | export default AD;
--------------------------------------------------------------------------------
/app/components/SearchInput/index.jsx:
--------------------------------------------------------------------------------
1 | import './style';
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 |
5 | class SearchInput extends React.PureComponent {
6 | constructor(props) {
7 | super(props);
8 | this.state = {
9 | value: ''
10 | };
11 | }
12 |
13 | // 点击searchInput内,都focus输入框
14 | clickHandler() {
15 | this.refs.input.focus();
16 | }
17 |
18 | // 输入回车后,submit
19 | keyUpHandler(e) {
20 | e.preventDefault();
21 | this.props.onSubmit(this.state.value);
22 | }
23 |
24 | // 受控组件处理
25 | changeHandler(e) {
26 | let value = e.target.value;
27 | this.setState({
28 | value
29 | });
30 | }
31 |
32 | render() {
33 | return (
34 |
44 | );
45 | }
46 | }
47 |
48 | SearchInput.propTypes = {
49 | onSubmit: PropTypes.func.isRequired
50 | };
51 |
52 | export default SearchInput;
--------------------------------------------------------------------------------
/app/containers/City/index.jsx:
--------------------------------------------------------------------------------
1 | import './style';
2 | import React from 'react';
3 | import { connect } from 'react-redux';
4 | import Header from 'components/Header';
5 | import CurrentCity from 'components/CurrentCity';
6 | import CityList from 'components/CityList';
7 | import { updateUserCity } from 'actions/userInfo';
8 | import localStorage from 'util/localStorage';
9 |
10 | class City extends React.PureComponent {
11 |
12 | /**
13 | * 更新Redux userInfo的cityName,以及localStorage的城市名
14 | * @param {*string} newCityName 新的城市名,由子组件回调提供
15 | */
16 | updateCityHandler(newCityName) {
17 | // 更新redux
18 | this.props.onUpdateCity(newCityName);
19 | // 更新localStorage
20 | localStorage.setItem('cityName', newCityName);
21 | // 返回主页
22 | this.props.history.push('/');
23 | }
24 |
25 | render() {
26 | return (
27 |
28 |
31 |
32 |
33 |
34 | );
35 | }
36 | }
37 |
38 | function mapStateToProps(state) {
39 | return {
40 | cityName: state.userInfo.cityName
41 | };
42 | }
43 |
44 | function mapDispatchToProps(dispatch) {
45 | return {
46 | onUpdateCity: cityName => {
47 | dispatch(updateUserCity(cityName))
48 | }
49 | };
50 | }
51 |
52 | export default connect(mapStateToProps, mapDispatchToProps)(City);
--------------------------------------------------------------------------------
/mock/Search/searchList.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | hasMore: true,
3 | data: [
4 | {
5 | title: '小林家的龙女仆 康娜 马克杯',
6 | img: 'http://ou41niivx.bkt.clouddn.com/search-1.png?imageView2/2/w/200',
7 | price: '38.00',
8 | created: '2017-2-22',
9 | link: 'https://bcy.net/zhipin/detail/5574',
10 | desc: '小林家的龙女仆康娜马克杯,38块,你买不了吃亏,38块,你买不了上当~'
11 | },
12 | {
13 | title: '作品集锦2016.4-2017.6',
14 | img: 'http://ou41niivx.bkt.clouddn.com/search-2.png?imageView2/2/w/200',
15 | price: '48.00',
16 | created: '2017-2-20',
17 | link: 'https://bcy.net/zhipin/detail/7951',
18 | desc: '很棒的作品集锦!!!'
19 | },
20 | {
21 | title: '扑克少女隔热杯',
22 | img: 'http://ou41niivx.bkt.clouddn.com/search-3.png?imageView2/2/w/200',
23 | price: '48.00',
24 | created: '2017-1-22',
25 | link: 'https://bcy.net/zhipin/detail/7495',
26 | desc: '清仓大甩卖,清仓大甩卖'
27 | },
28 | {
29 | title: '史迪仔鼠标垫',
30 | img: 'http://ou41niivx.bkt.clouddn.com/search-4.png?imageView2/2/w/200',
31 | price: '28.00',
32 | created: '2017-1-18',
33 | link: 'https://bcy.net/zhipin/detail/7753',
34 | desc: '史迪仔强势来袭,你懂的~'
35 | },
36 | {
37 | title: '王者荣耀法师联盟A徽章套装(4个/套)',
38 | img: 'http://ou41niivx.bkt.clouddn.com/search-5.png?imageView2/2/w/200',
39 | price: '28.00',
40 | created: '2017-1-17',
41 | link: 'https://bcy.net/zhipin/detail/8988',
42 | desc: '王者荣耀!王者农药!药药药!'
43 | }
44 | ]
45 | };
46 |
--------------------------------------------------------------------------------
/mock/Home/guessInterest.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | hasMore: true,
3 | data: [
4 | {
5 | title: '干物妹小埋手机壳',
6 | img: 'http://ou41niivx.bkt.clouddn.com/interest-1.png?imageView2/2/w/200',
7 | price: '38.00',
8 | created: '2017-2-22',
9 | link: 'https://bcy.net/zhipin/detail/1020',
10 | desc: '超萌的干物妹小埋手机壳,38块,你买不了吃亏,38块,你买不了上当~'
11 | },
12 | {
13 | title: '唯美插画书签',
14 | img: 'http://ou41niivx.bkt.clouddn.com/interest-2.png?imageView2/2/w/200',
15 | price: '38.00',
16 | created: '2017-2-20',
17 | link: 'https://bcy.net/zhipin/detail/8080',
18 | desc: '很棒的唯美插画书签~'
19 | },
20 | {
21 | title: 'MogGen-猫将徽章套装(4个/套)',
22 | img: 'http://ou41niivx.bkt.clouddn.com/interest-3.png?imageView2/2/w/200',
23 | price: '28.00',
24 | created: '2017-1-22',
25 | link: 'https://bcy.net/zhipin/detail/8030',
26 | desc: '清仓大甩卖,清仓大甩卖'
27 | },
28 | {
29 | title: '埃罗芒阿老师等身抱枕',
30 | img: 'http://ou41niivx.bkt.clouddn.com/interest-4.png?imageView2/2/w/200',
31 | price: '168.00',
32 | created: '2017-1-18',
33 | link: 'https://bcy.net/zhipin/detail/8338',
34 | desc: '埃罗芒阿老师等身抱枕,你懂的~'
35 | },
36 | {
37 | title: '神烦催更超大鼠标垫-绘师款',
38 | img: 'http://ou41niivx.bkt.clouddn.com/interest-5.png?imageView2/2/w/200',
39 | price: '58.00',
40 | created: '2017-1-17',
41 | link: 'https://bcy.net/zhipin/detail/2031',
42 | desc: '还不投稿?勾搭了吗?小黄本看了?还不投稿?'
43 | }
44 | ]
45 | };
46 |
--------------------------------------------------------------------------------
/app/containers/index.jsx:
--------------------------------------------------------------------------------
1 | import 'static/scss/common.scss';
2 | import React from 'react';
3 | import { connect } from 'react-redux';
4 | import { withRouter } from 'react-router-dom';
5 | import get from 'fetch/get';
6 | import LocalStorage from 'util/localStorage';
7 | import { CITYNAME } from 'config/localStorageKey';
8 | import * as userInfoActions from 'actions/userInfo';
9 |
10 | class App extends React.Component {
11 | constructor(props) {
12 | super(props);
13 | this.state = {
14 | isInitDone: false
15 | }
16 | }
17 |
18 | /**
19 | * @return 返回获取到的城市名字
20 | */
21 | _getCityName() {
22 | let cityName = LocalStorage.getItem(CITYNAME);
23 | if (cityName == null) {
24 | cityName = '上海';
25 | }
26 | return cityName;
27 | }
28 |
29 | componentDidMount() {
30 | // 从localStorage中获取本地城市数据
31 | let cityName = this._getCityName();
32 | // 数据需要放到redux中
33 | this.props.onUpdateCity(cityName);
34 | // 更新加载中状态
35 | this.setState({
36 | isInitDone: true
37 | });
38 | }
39 |
40 | render() {
41 | return (
42 |
43 | {this.state.isInitDone
44 | ? this.props.children
45 | :
加载中...
46 | }
47 |
48 | );
49 | }
50 | }
51 |
52 | const mapStateToProps = state => {
53 | return {
54 | cityName: state.cityName
55 | }
56 | }
57 |
58 | const mapDispatchToProps = dispatch => {
59 | return {
60 | onUpdateCity: cityName => {
61 | dispatch(userInfoActions.updateUserCity(cityName));
62 | }
63 | }
64 | }
65 |
66 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(App));
--------------------------------------------------------------------------------
/app/components/CityList/index.jsx:
--------------------------------------------------------------------------------
1 | import './style';
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 |
5 | class CityList extends React.PureComponent {
6 |
7 | /**
8 | * 选择城市以后,更新城市数据
9 | * @param {*object} e 事件event
10 | */
11 | ClickHandler (e) {
12 | let { target } = e;
13 | // 事件代理
14 | if (target.nodeName === 'SPAN') {
15 | let newCityName = target.textContent;
16 | if (this.props.onUpdateCity) {
17 | this.props.onUpdateCity(newCityName);
18 | }
19 | }
20 | }
21 |
22 | render() {
23 | return (
24 |
25 |
热门城市
26 |
27 | - 北京
28 | - 上海
29 | - 深圳
30 | - 杭州
31 | - 广州
32 | - 厦门
33 | - 珠海
34 | - 天津
35 | - 河北
36 | - 金华
37 | - 宁波
38 | - 三亚
39 | - 海口
40 | - 苏州
41 | - 长沙
42 | - 新疆
43 | - 内蒙
44 | - 舟山
45 | - 台北
46 | - 香港
47 | - 澳门
48 |
49 |
50 | );
51 | }
52 | }
53 |
54 | CityList.propTypes = {
55 | onUpdateCity: PropTypes.func
56 | };
57 |
58 | export default CityList;
--------------------------------------------------------------------------------
/app/components/GoodsList/style.scss:
--------------------------------------------------------------------------------
1 | .goodsList {
2 | background-color: #fff;
3 |
4 | ul {
5 | padding: 10px 10px 0;
6 | li {
7 | padding: 5px;
8 | border-bottom: 1px solid #f1f1f1;
9 | &:after {
10 | display: table;
11 | content: '';
12 | clear: both;
13 | }
14 | a {
15 | color: #333;
16 | }
17 | .left {
18 | margin-right: 10px;
19 | float: left;
20 | width: 120px;
21 | img {
22 | width: 100%;
23 | }
24 | line-height: 1;
25 | }
26 | .right {
27 | .head {
28 | height: 24px;
29 | &:after {
30 | display: table;
31 | content: '';
32 | clear: both;
33 | }
34 | h4 {
35 | max-width: 40%;
36 | float: left;
37 | font-size: 16px;
38 | line-height: 1.2;
39 | overflow: hidden;
40 | text-overflow: ellipsis;
41 | white-space: nowrap;
42 | word-wrap: normal;
43 | }
44 | span {
45 | float: right;
46 | font-size: 12px;
47 | line-height: 16px;
48 | }
49 | }
50 | .desc {
51 | font-size: 14px;
52 | line-height: 1.4;
53 | }
54 | .price {
55 | margin-top: 5px;
56 | color: #ff6fa2;
57 | font-size: 20px;
58 | font-weight: 600;
59 | }
60 | }
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-webapp",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "mock": "node ./mock/server.js",
8 | "dev": "NODE_ENV=dev webpack-dev-server --progress --colors --open",
9 | "build": "NODE_ENV=production webpack -p --config ./webpack.config.production.js --progress --colors"
10 | },
11 | "author": "Luozi",
12 | "license": "ISC",
13 | "devDependencies": {
14 | "autoprefixer": "^7.1.2",
15 | "babel-core": "^6.26.3",
16 | "babel-eslint": "^7.2.3",
17 | "babel-loader": "^7.1.1",
18 | "babel-plugin-react-html-attrs": "^2.0.0",
19 | "babel-plugin-syntax-dynamic-import": "^6.18.0",
20 | "babel-plugin-transform-object-rest-spread": "^6.23.0",
21 | "babel-polyfill": "^6.26.0",
22 | "babel-preset-env": "^1.7.0",
23 | "babel-preset-react": "^6.24.1",
24 | "boom": "^5.2.0",
25 | "clean-webpack-plugin": "^0.1.16",
26 | "css-loader": "^0.28.4",
27 | "eslint": "^4.19.1",
28 | "eslint-loader": "^1.9.0",
29 | "eslint-plugin-react": "^7.1.0",
30 | "extract-text-webpack-plugin": "^3.0.0",
31 | "file-loader": "^0.11.2",
32 | "html-webpack-plugin": "^2.29.0",
33 | "image-webpack-loader": "^3.3.1",
34 | "koa": "^2.5.1",
35 | "koa-bodyparser": "^4.2.0",
36 | "koa-router": "^7.2.1",
37 | "node-sass": "^4.9.2",
38 | "postcss": "^6.0.8",
39 | "postcss-loader": "^2.0.6",
40 | "react-addons-perf": "^15.4.2",
41 | "react-hot-loader": "^1.3.1",
42 | "sass-loader": "^6.0.6",
43 | "style-loader": "^0.18.2",
44 | "url-loader": "^0.5.9",
45 | "webpack": "^3.4.1",
46 | "webpack-dev-server": "^2.6.1"
47 | },
48 | "dependencies": {
49 | "history": "^4.6.3",
50 | "prop-types": "^15.5.10",
51 | "react": "^15.6.1",
52 | "react-dom": "^15.6.1",
53 | "react-loadable": "^4.0.4",
54 | "react-redux": "^5.0.5",
55 | "react-router-dom": "^4.1.2",
56 | "react-swipe": "^5.0.8",
57 | "redux": "^3.7.2",
58 | "swipe-js-iso": "^2.0.3"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/app/components/Category/index.jsx:
--------------------------------------------------------------------------------
1 | import './style';
2 | import React from 'react';
3 | import ReactSwipe from 'react-swipe';
4 |
5 | class Category extends React.PureComponent {
6 | constructor(props) {
7 | super(props);
8 | this.state = {
9 | index: 0
10 | };
11 | }
12 | render() {
13 | const settings = {
14 | auto: 2500,
15 | callback: index => {
16 | this.setState({ index: index }); // 更新当前轮播图的index
17 | }
18 | };
19 | return (
20 |
21 |
22 |
23 |

24 |
25 |
26 |

27 |
28 |
29 |

30 |
31 |
32 |

33 |
34 |
35 |

36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | );
47 | }
48 | }
49 |
50 | export default Category;
--------------------------------------------------------------------------------
/app/components/LoadMore/index.jsx:
--------------------------------------------------------------------------------
1 | import './style';
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 |
5 | let handlerID; // 为了清除window上的scroll事件
6 |
7 | class LoadMore extends React.PureComponent {
8 | /**
9 | * 监听滚动事件,并处理回调
10 | */
11 | _loadMore() {
12 | let timeoutID;
13 | window.addEventListener('scroll', this._scrollHandler(timeoutID, this));
14 | }
15 |
16 | /**
17 | * scroll事件的处理
18 | * @param {*number} timeoutID 计时器的ID,为了可以clear
19 | * @param {*object} context 上下文
20 | */
21 | _scrollHandler(timeoutID, context) {
22 | return handlerID = e => {
23 | // 如果处于正在获取数据的状态,则不调用获取数据
24 | if (context.props.isLoading) {
25 | return;
26 | }
27 | // 如果hasMore为false了,说明没数据了,清除scroll的事件
28 | if (!context.props.hasMore) {
29 | window.removeEventListener('scroll', handlerID);
30 | }
31 | if (timeoutID) {
32 | clearTimeout(timeoutID)
33 | }
34 | // 节流
35 | timeoutID = setTimeout(this._callBack.bind(context), 50);
36 | }
37 | }
38 |
39 | /**
40 | * 当LoadMore元素出现在页面可视范围中,则调用获取数据
41 | */
42 | _callBack() {
43 | let loadMoreNode = this.refs.loadMoreNode;
44 | let top = loadMoreNode.getBoundingClientRect().top,
45 | screenHeight = window.screen.height;
46 | if (top && top < screenHeight) {
47 | this.props.onLoadMore();
48 | }
49 | }
50 |
51 | componentDidMount() {
52 | this._loadMore();
53 | }
54 |
55 | componentWillUnmount() {
56 | window.removeEventListener('scroll', handlerID);
57 | }
58 |
59 |
60 | render() {
61 | let text;
62 | if (!this.props.hasMore) {
63 | text = '没有更多了~';
64 | } else if (this.props.isLoading) {
65 | text = '加载中...';
66 | } else {
67 | text = '上拉加载更多';
68 | }
69 | return (
70 |
71 | {text}
72 |
73 | );
74 | }
75 | }
76 |
77 | LoadMore.propTypes = {
78 | isLoading: PropTypes.bool.isRequired,
79 | onLoadMore: PropTypes.func.isRequired
80 | };
81 |
82 | export default LoadMore;
--------------------------------------------------------------------------------
/app/router/routerMap.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { HashRouter as Router, Route, Switch } from 'react-router-dom';
3 | import createHistory from 'history/createBrowserHistory';
4 | const history = createHistory();
5 |
6 | import App from 'containers';
7 |
8 | // 按路由拆分代码
9 | import Loadable from 'react-loadable';
10 | const MyLoadingComponent = ({ isLoading, error }) => {
11 | // Handle the loading state
12 | if (isLoading) {
13 | return Loading...
;
14 | }
15 | // Handle the error state
16 | else if (error) {
17 | return Sorry, there was a problem loading the page.
;
18 | }
19 | else {
20 | return null;
21 | }
22 | };
23 | const AsyncHome = Loadable({
24 | loader: () => import('../containers/Home'),
25 | loading: MyLoadingComponent
26 | });
27 | const AsyncCity = Loadable({
28 | loader: () => import('../containers/City'),
29 | loading: MyLoadingComponent
30 | });
31 | const AsyncDetail = Loadable({
32 | loader: () => import('../containers/Detail'),
33 | loading: MyLoadingComponent
34 | });
35 | const AsyncSearch = Loadable({
36 | loader: () => import('../containers/Search'),
37 | loading: MyLoadingComponent
38 | });
39 | const AsyncUser = Loadable({
40 | loader: () => import('../containers/User'),
41 | loading: MyLoadingComponent
42 | });
43 | const AsyncNotFound = Loadable({
44 | loader: () => import('../containers/404'),
45 | loading: MyLoadingComponent
46 | });
47 |
48 | // 路由配置
49 | class RouteMap extends React.Component {
50 | render() {
51 | return (
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | );
66 | // 说明
67 | // empty Route
68 | // https://github.com/ReactTraining/react-router/issues/1982 解决人:PFight
69 | // 解决react-router v4改变查询参数并不会刷新或者说重载组件的问题
70 | }
71 | }
72 |
73 | export default RouteMap;
--------------------------------------------------------------------------------
/mock/server.js:
--------------------------------------------------------------------------------
1 | const Koa = require('koa');
2 |
3 | // 使用router
4 | const Router = require('koa-router');
5 | const Boom = require('boom');
6 | const app = new Koa();
7 | const router = new Router();
8 | app.use(router.routes());
9 | app.use(router.allowedMethods({
10 | throw: true,
11 | notImplemented: () => new Boom.notImplemented(),
12 | methodNotAllowed: () => new Boom.methodNotAllowed()
13 | }));
14 |
15 | // 使用bodyparser 解析get,post的参数
16 | const bodyParser = require('koa-bodyparser');
17 | app.use(bodyParser());
18 |
19 | /* 首页数据 */
20 |
21 | // 推荐制品周边数据
22 | const recoData = require('./Home/reco.js');
23 | router.get('/api/home/reco', async(ctx, next) => {
24 | ctx.body = recoData;
25 | });
26 | // 猜你喜欢数据
27 | const guessData = require('./Home/guessInterest.js');
28 | router.get('/api/home/guessInterest/:city/:page', async(ctx, next) => {
29 | // 看上去数据的判断有点简单?
30 | // 其实,实际项目也是这么做的,只不过后端接口在联调阶段会proxy到线下后端的数据服务器接口
31 | // 我们只需要传给后端city和page这两个参数即可,复杂的判断后端会处理
32 | // 不过我们需要在开发的时候考虑到,mock数据中如果hasMore为false的情况
33 | console.log('当前城市:' + ctx.params.city);
34 | console.log('当前页码:' + ctx.params.page);
35 | // 假设请求page5就没有更多了
36 | guessData.hasMore = true;
37 | if (ctx.params.page == 5) {
38 | console.log('nani');
39 |
40 | guessData.hasMore = false;
41 | }
42 | ctx.body = guessData;
43 | await next();
44 | });
45 |
46 | // 搜索结果页数据 【无关键字】
47 | const searchListData1 = require('./Search/searchList.js');
48 | router.get('/api/search/searchList/:city/:page/:category', async (ctx, next) => {
49 | console.log('当前城市:' + ctx.params.city);
50 | console.log('当前页码:' + ctx.params.page);
51 | console.log('当前类别:' + ctx.params.category);
52 | // 假设请求page5就没有更多了
53 | searchListData1.hasMore = true;
54 | if (ctx.params.page == 5) {
55 | searchListData1.hasMore = false;
56 | }
57 | ctx.body = searchListData1;
58 | await next();
59 | });
60 |
61 | // 搜索结果页数据 【有关键字】
62 | const searchListData2 = require('./Search/searchList.js');
63 | router.get('/api/search/searchList/:city/:page/:category/:keywords', async (ctx, next) => {
64 | console.log('当前城市:' + ctx.params.city);
65 | console.log('当前页码:' + ctx.params.page);
66 | console.log('当前类别:' + ctx.params.category);
67 | console.log('当前关键字:' + ctx.params.keywords);
68 | // 假设请求page5就没有更多了
69 | searchListData2.hasMore = true;
70 | if (ctx.params.page == 5) {
71 | searchListData2.hasMore = false;
72 | }
73 | ctx.body = searchListData2;
74 | await next();
75 | });
76 |
77 | // log error
78 | app.on('error', (err, ctx) => {
79 | console.log('server error', err, ctx);
80 | });
81 |
82 | app.listen(7777);
--------------------------------------------------------------------------------
/app/containers/Home/subPage/GuessInterest.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { getGuessInterestData } from 'fetch/Home';
3 | import { connect } from 'react-redux';
4 | import GoodsList from 'components/GoodsList';
5 | import LoadMore from 'components/LoadMore';
6 |
7 | class GuessInterest extends React.PureComponent {
8 | constructor(props) {
9 | super(props);
10 | this.state = {
11 | data: [],
12 | page: 0,
13 | hasMore: true,
14 | isLoading: false
15 | };
16 | }
17 |
18 | /**
19 | * 获取猜你喜欢的数据,并更新state
20 | */
21 | _getGuessInterestData() {
22 | // 记录状态
23 | this.setState({
24 | isLoading: true
25 | })
26 | // 获取数据
27 | let city = this.props.cityName;
28 | let page = this.state.page;
29 | let result = getGuessInterestData(city, page);
30 | this._processResult(result);
31 | }
32 |
33 | _processResult(result) {
34 | // 增加 page 计数
35 | const page = this.state.page;
36 | this.setState({
37 | page: page + 1
38 | })
39 | // 处理 fetch返回结果
40 | result.then(res => {
41 | return res.json();
42 | })
43 | .then(json => {
44 | this.setState({
45 | hasMore: json.hasMore,
46 | data: this.state.data.concat(json.data)
47 | });
48 | // 更新状态
49 | this.setState({
50 | isLoading: false
51 | });
52 | })
53 | .catch(ex => {
54 | /*global __DEV__*/
55 | if (__DEV__) {
56 | console.error('主页获取猜你喜欢数据出错:', ex.message);
57 | }
58 | // 更新状态
59 | this.setState({
60 | isLoading: false
61 | });
62 | });
63 | }
64 |
65 | componentDidMount() {
66 | this._getGuessInterestData();
67 | }
68 |
69 | // 处理加载更多的回调
70 | loadMoreHandler() {
71 | this._getGuessInterestData()
72 | }
73 |
74 | render() {
75 | return (
76 |
77 |
猜你喜欢
78 |
79 |
82 |
83 | );
84 | }
85 | }
86 |
87 | function mapStateToProps(state) {
88 | return {
89 | cityName: state.userInfo.cityName
90 | };
91 | }
92 |
93 | export default connect(mapStateToProps)(GuessInterest);
--------------------------------------------------------------------------------
/app/containers/Search/subPage/SearchList.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { getSearchListData } from 'fetch/Search';
3 | import { connect } from 'react-redux';
4 | import GoodsList from 'components/GoodsList';
5 | import LoadMore from 'components/LoadMore';
6 |
7 | class SearchList extends React.PureComponent {
8 | constructor(props) {
9 | super(props);
10 | this.state = {
11 | data: [],
12 | page: 0,
13 | hasMore: true,
14 | isLoading: false
15 | };
16 | }
17 |
18 | /**
19 | * 获取猜你喜欢的数据,并更新state
20 | */
21 | _getSearchListData() {
22 | // 记录状态
23 | this.setState({
24 | isLoading: true
25 | });
26 | // 获取数据
27 | let city = this.props.cityName,
28 | page = this.state.page,
29 | category = this.props.category || 'all',
30 | keywords = this.props.keywords;
31 | let result = getSearchListData(city, page, category, keywords)
32 | this._processResult(result);
33 | }
34 |
35 | _processResult(result) {
36 | // 增加 page 计数
37 | const { page } = this.state;
38 | this.setState({
39 | page: page + 1
40 | })
41 | // 处理 fetch返回结果
42 | result.then(res => {
43 | return res.json();
44 | })
45 | .then(json => {
46 | this.setState({
47 | hasMore: json.hasMore,
48 | data: this.state.data.concat(json.data)
49 | });
50 | // 更新状态
51 | this.setState({
52 | isLoading: false
53 | });
54 | })
55 | .catch(ex => {
56 | /*global __DEV__*/
57 | if (__DEV__) {
58 | console.error('搜索页获取搜索列表数据出错:', ex.message);
59 | }
60 | // 更新状态
61 | this.setState({
62 | isLoading: false
63 | });
64 | });
65 | }
66 |
67 | componentDidMount() {
68 | this._getSearchListData();
69 | }
70 |
71 | // 处理加载更多的回调
72 | loadMoreHandler() {
73 | this._getSearchListData();
74 | }
75 |
76 | render() {
77 | return (
78 |
79 |
80 |
83 |
84 | );
85 | }
86 | }
87 |
88 | function mapStateToProps(state) {
89 | return {
90 | cityName: state.userInfo.cityName
91 | };
92 | }
93 |
94 | export default connect(mapStateToProps)(SearchList);
--------------------------------------------------------------------------------
/app/static/scss/font.scss:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: "iconfont";
3 | src: url('../fonts/iconfont.eot?t=1501690051389');
4 | /* IE9*/
5 | src: url('../fonts/iconfont.eot?t=1501690051389#iefix') format('embedded-opentype'), /* IE6-IE8 */
6 | url('../fonts/iconfont.woff?t=1501690051389') format('woff'), /* chrome, firefox */
7 | url('../fonts/iconfont.ttf?t=1501690051389') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
8 | url('../fonts/iconfont.svg?t=1501690051389#iconfont') format('svg');
9 | /* iOS 4.1- */
10 | }
11 |
12 | [class^="icon-"], [class*=" icon-"] {
13 | /* use !important to prevent issues with browser extensions that change fonts */
14 | font-family: 'iconfont' !important;
15 | speak: none;
16 | font-style: normal;
17 | font-weight: normal;
18 | font-variant: normal;
19 | text-transform: none;
20 | line-height: 1;
21 | /* Better Font Rendering =========== */
22 | -webkit-font-smoothing: antialiased;
23 | -moz-osx-font-smoothing: grayscale;
24 | }
25 |
26 | .icon-shuipingzuo:before {
27 | content: "\e600";
28 | }
29 |
30 | .icon-shuangyuzuo:before {
31 | content: "\e601";
32 | }
33 |
34 | .icon-mojiezuo:before {
35 | content: "\e602";
36 | }
37 |
38 | .icon-chunvzuo:before {
39 | content: "\e603";
40 | }
41 |
42 | .icon-shizizuo:before {
43 | content: "\e604";
44 | }
45 |
46 | .icon-juxiezuo:before {
47 | content: "\e605";
48 | }
49 |
50 | .icon-tianhezuo:before {
51 | content: "\e606";
52 | }
53 |
54 | .icon-baobei:before {
55 | content: "\e607";
56 | }
57 |
58 | .icon-dianpu:before {
59 | content: "\e608";
60 | }
61 |
62 | .icon-dazahui:before {
63 | content: "\e609";
64 | }
65 |
66 | .icon-sheyinglvxing:before {
67 | content: "\e60a";
68 | }
69 |
70 | .icon-gaoxiaoquwei:before {
71 | content: "\e60b";
72 | }
73 |
74 | .icon-mingxing:before {
75 | content: "\e60c";
76 | }
77 |
78 | .icon-chongwu:before {
79 | content: "\e60d";
80 | }
81 |
82 | .icon-DIY:before {
83 | content: "\e60e";
84 | }
85 |
86 | .icon-meishi:before {
87 | content: "\e60f";
88 | }
89 |
90 | .icon-qinggan:before {
91 | content: "\e610";
92 | }
93 |
94 | .icon-muying:before {
95 | content: "\e611";
96 | }
97 |
98 | .icon-jiaju:before {
99 | content: "\e612";
100 | }
101 |
102 | .icon-meifa:before {
103 | content: "\e613";
104 | }
105 |
106 | .icon-meirong:before {
107 | content: "\e614";
108 | }
109 |
110 | .icon-shuma:before {
111 | content: "\e615";
112 | }
113 |
114 | .icon-shishang:before {
115 | content: "\e616";
116 | }
117 |
118 | .icon-yundonghuwai:before {
119 | content: "\e617";
120 | }
121 |
122 | .icon-nanzhuang:before {
123 | content: "\e618";
124 | }
125 |
126 | .icon-peishi:before {
127 | content: "\e619";
128 | }
129 |
130 | .icon-xiezi:before {
131 | content: "\e61a";
132 | }
133 |
134 | .icon-baobao:before {
135 | content: "\e61b";
136 | }
137 |
138 | .icon-nvzhuang:before {
139 | content: "\e61c";
140 | }
141 |
142 | .icon-baobei1:before {
143 | content: "\e61d";
144 | }
145 |
146 | .icon-C-pad:before {
147 | content: "\e6bd";
148 | }
149 |
150 | .icon-share:before {
151 | content: "\e65a";
152 | }
153 |
154 | .icon-key:before {
155 | content: "\e8a3";
156 | }
157 |
158 | .icon-search:before {
159 | content: "\e8b8";
160 | }
161 |
162 | .icon-collect-fill:before {
163 | content: "\e8c2";
164 | }
165 |
166 | .icon-collect:before {
167 | content: "\e8c3";
168 | }
169 |
170 | .icon-user:before {
171 | content: "\e8c8";
172 | }
173 |
174 | .icon-image:before {
175 | content: "\e8d2";
176 | }
177 |
178 | .icon-arrow-left:before {
179 | content: "\e8ef";
180 | }
181 |
182 | .icon-arrow-right:before {
183 | content: "\e8f1";
184 | }
185 |
186 | .icon-arrow-down:before {
187 | content: "\e8f2";
188 | }
189 |
190 | .icon-location:before {
191 | content: "\e8ff";
192 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | /*global
2 | __DEV__
3 | __dirname
4 | process
5 | */
6 | const webpack = require('webpack');
7 | const path = require('path');
8 | const HtmlWebpackPlugin = require('html-webpack-plugin');
9 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
10 |
11 | module.exports = {
12 | devtool: 'cheap-module-eval-source-map',
13 | entry: [
14 | 'babel-polyfill',
15 | path.join(__dirname, 'app/index.jsx'),
16 | ],
17 | output: {
18 | path: path.join(__dirname, 'dev'),
19 | filename: 'bundle.js'
20 | },
21 | resolve: {
22 | extensions: ['.js', '.jsx', '.scss', '.css'],
23 | alias: {
24 | components: path.join(__dirname, 'app/components/'),
25 | containers: path.join(__dirname, 'app/containers/'),
26 | constants: path.join(__dirname, 'app/constants/'),
27 | actions: path.join(__dirname, 'app/actions/'),
28 | reducers: path.join(__dirname, 'app/reducers/'),
29 | util: path.join(__dirname, 'app/util/'),
30 | fetch: path.join(__dirname, 'app/fetch/'),
31 | config: path.join(__dirname, 'app/config/'),
32 | static: path.join(__dirname, 'app/static/')
33 | }
34 | },
35 | module: {
36 | rules: [
37 | {
38 | test: /\.(js|jsx)$/,
39 | exclude: /node_modules/,
40 | use: [
41 | 'react-hot-loader',
42 | 'babel-loader'
43 | ]
44 | },
45 | {
46 | test: /\.(jpg|jpeg|png|svg|gif|bmp)/i,
47 | use: [
48 | 'url-loader?limit=5000'
49 | ]
50 | },
51 | {
52 | test: /\.(png|woff|woff2|svg|ttf|eot)($|\?)/i,
53 | use: [
54 | 'url-loader?limit=5000'
55 | ]
56 | },
57 | {
58 | test: /\.(css|scss)$/,
59 | exclude: /node_modules/,
60 | use: ExtractTextPlugin.extract({
61 | fallback: 'style-loader',
62 | //resolve-url-loader may be chained before sass-loader if necessary
63 | use: [
64 | {
65 | loader: "css-loader",
66 | options: {
67 | sourceMap: true
68 | }
69 | },
70 | {
71 | loader: 'postcss-loader',
72 | options: {
73 | sourceMap: true
74 | }
75 | },
76 | {
77 | loader: "sass-loader",
78 | options: {
79 | sourceMap: true
80 | }
81 | }
82 | ]
83 | })
84 |
85 | }
86 | ]
87 | },
88 | plugins: [
89 | // Scope hosting
90 | new webpack.optimize.ModuleConcatenationPlugin(),
91 | new ExtractTextPlugin({
92 | filename: 'main.css',
93 | disable: true
94 | }),
95 | // html 模板插件
96 | new HtmlWebpackPlugin({
97 | template: __dirname + '/app/index.html'
98 | }),
99 | // 热加载插件
100 | new webpack.HotModuleReplacementPlugin(),
101 | // 可在业务 js 代码中使用 __DEV__ 判断是否是dev模式(dev模式下可以提示错误、测试报告等, production模式不提示)
102 | new webpack.DefinePlugin({
103 | __DEV__: JSON.stringify(JSON.parse((process.env.NODE_ENV == 'dev') || 'false'))
104 | })
105 | ],
106 | devServer: {
107 | proxy: {
108 | // 凡是 `/api` 开头的 http 请求,都会被代理到 localhost:7777 上,由 koa 提供 mock 数据。
109 | // koa 代码在 ./mock 目录中,启动命令为 npm run mock
110 | '/api': {
111 | target: 'http://localhost:7777',
112 | secure: false
113 | }
114 | },
115 | host: '0.0.0.0',
116 | port: '9999',
117 | disableHostCheck: true, // 为了手机可以访问
118 | contentBase: './dev', // 本地服务器所加载的页面所在的目录
119 | historyApiFallback: true, // 为了SPA应用服务
120 | inline: true, //实时刷新
121 | hot: true // 使用热加载插件 HotModuleReplacementPlugin
122 | }
123 | }
--------------------------------------------------------------------------------
/webpack.config.production.js:
--------------------------------------------------------------------------------
1 | /*global
2 | __DEV__
3 | __dirname
4 | process
5 | */
6 | const webpack = require('webpack');
7 | const path = require('path');
8 | const CleanWebpackPlugin = require('clean-webpack-plugin');
9 | const HtmlWebpackPlugin = require('html-webpack-plugin');
10 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
11 | const pkg = require('./package.json');
12 |
13 | module.exports = {
14 | devtool: 'cheap-module-source-map',
15 | entry: {
16 | app: path.join(__dirname, 'app/index.jsx'),
17 | // 将第三方依赖(node_modules)的库打包
18 | vendor: Object.keys(pkg.dependencies)
19 | },
20 | output: {
21 | path: path.join(__dirname, 'build'),
22 | publicPath: path.join(__dirname, 'build/'),
23 | filename: 'js/[name].[chunkhash:8].js'
24 | },
25 | resolve: {
26 | extensions: ['.js', '.jsx', '.scss', '.css'],
27 | alias: {
28 | components: path.join(__dirname, 'app/components/'),
29 | containers: path.join(__dirname, 'app/containers/'),
30 | constants: path.join(__dirname, 'app/constants/'),
31 | actions: path.join(__dirname, 'app/actions/'),
32 | reducers: path.join(__dirname, 'app/reducers/'),
33 | util: path.join(__dirname, 'app/util/'),
34 | fetch: path.join(__dirname, 'app/fetch/'),
35 | config: path.join(__dirname, 'app/config/'),
36 | static: path.join(__dirname, 'app/static/')
37 | }
38 | },
39 | module: {
40 | rules: [
41 | {
42 | test: /\.(js|jsx)$/,
43 | exclude: /node_modules/,
44 | use: [
45 | 'babel-loader',
46 | 'eslint-loader'
47 | ]
48 | },
49 | {
50 | test: /\.(jpg|jpeg|png|svg|gif|bmp)/i,
51 | use: [
52 | 'url-loader?limit=5000&name=img/[name].[sha512:hash:base64:8].[ext]',
53 | 'image-webpack-loader?{pngquant:{quality: "65-90", speed: 4}, mozjpeg: {quality: 65}}'
54 | ]
55 | },
56 | {
57 | test: /\.(woff|woff2|ttf|eot)($|\?)/i,
58 | use: [
59 | 'url-loader?limit=5000&name=fonts/[name].[sha512:hash:base64:8].[ext]'
60 | ]
61 | },
62 | {
63 | test: /\.(css|scss)$/,
64 | exclude: /node_modules/,
65 | use: ExtractTextPlugin.extract({
66 | fallback: 'style-loader',
67 | //resolve-url-loader may be chained before sass-loader if necessary
68 | use: [
69 | {
70 | loader: 'css-loader',
71 | options: {
72 | sourceMap: true
73 | }
74 | },
75 | {
76 | loader: 'postcss-loader',
77 | options: {
78 | sourceMap: true
79 | }
80 | },
81 | {
82 | loader: 'sass-loader',
83 | options: {
84 | sourceMap: true
85 | }
86 | }
87 | ]
88 | })
89 | }
90 | ]
91 | },
92 | plugins: [
93 | // Scope hosting
94 | new webpack.optimize.ModuleConcatenationPlugin(),
95 | // 删除build文件夹
96 | new CleanWebpackPlugin('./build'),
97 | // 加署名
98 | new webpack.BannerPlugin("Copyright by luoziwo.cn"),
99 | // html 模板插件
100 | new HtmlWebpackPlugin({
101 | template: __dirname + '/app/index.html',
102 | minify: {
103 | removeComments: true,
104 | collapseWhitespace: false
105 | }
106 | }),
107 | // 分离CSS和js
108 | new ExtractTextPlugin('css/[name].[chunkhash:8].css'),
109 | // 提供公共代码vendor
110 | new webpack.optimize.CommonsChunkPlugin({
111 | name: 'vendor',
112 | filename: 'js/[name].[chunkhash:8].js'
113 | }),
114 | /// 定义为生产环境,编译 React 时压缩到最小
115 | new webpack.DefinePlugin({
116 | 'process.env': {
117 | 'NODE_ENV': JSON.stringify(process.env.NODE_ENV)
118 | }
119 | }),
120 | // 可在业务 js 代码中使用 __DEV__ 判断是否是dev模式(dev模式下可以提示错误、测试报告等, production模式不提示)
121 | new webpack.DefinePlugin({
122 | __DEV__: JSON.stringify(JSON.parse((process.env.NODE_ENV == 'dev') || 'false'))
123 | })
124 | ]
125 | }
--------------------------------------------------------------------------------
/app/static/fonts/iconfont.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
160 |
--------------------------------------------------------------------------------