├── src
├── pages
│ ├── index.less
│ ├── common-components
│ │ ├── no-data
│ │ │ ├── index.jsx
│ │ │ └── index.less
│ │ ├── badge
│ │ │ ├── index.jsx
│ │ │ └── index.less
│ │ ├── lazy-loading
│ │ │ ├── index.jsx
│ │ │ └── index.less
│ │ ├── title-bar
│ │ │ ├── index.jsx
│ │ │ └── index.less
│ │ ├── gallery
│ │ │ ├── index.less
│ │ │ └── index.jsx
│ │ ├── address-row
│ │ │ ├── index.jsx
│ │ │ └── index.less
│ │ ├── skeleton
│ │ │ ├── recommend.jsx
│ │ │ ├── entry.jsx
│ │ │ ├── row.jsx
│ │ │ └── index.less
│ │ ├── auth-err
│ │ │ ├── index.jsx
│ │ │ └── index.less
│ │ ├── nav-bar
│ │ │ ├── index.less
│ │ │ └── index.jsx
│ │ ├── recommend-food-row
│ │ │ ├── index.jsx
│ │ │ └── index.less
│ │ ├── tab-bar
│ │ │ ├── index.less
│ │ │ └── index.jsx
│ │ ├── red-ticket-row
│ │ │ ├── index.jsx
│ │ │ └── index.less
│ │ └── order-list-row
│ │ │ ├── index.jsx
│ │ │ └── index.less
│ ├── compass
│ │ ├── index.less
│ │ └── index.jsx
│ ├── benefit
│ │ ├── index.less
│ │ └── index.jsx
│ ├── order
│ │ ├── index.less
│ │ └── index.jsx
│ ├── place-order
│ │ ├── index.less
│ │ └── index.jsx
│ ├── home
│ │ ├── skeleton-screen
│ │ │ └── index.jsx
│ │ ├── index.less
│ │ └── top-bar
│ │ │ ├── index.jsx
│ │ │ └── index.less
│ ├── address
│ │ ├── address-row
│ │ │ ├── index.jsx
│ │ │ └── index.less
│ │ ├── index.less
│ │ └── index.jsx
│ ├── shop-detail
│ │ ├── foods
│ │ │ └── menu.jsx
│ │ ├── skeleton-screen
│ │ │ ├── index.jsx
│ │ │ └── index.less
│ │ ├── shop
│ │ │ ├── index.less
│ │ │ └── index.jsx
│ │ └── cartcontrol
│ │ │ └── stepper.jsx
│ ├── search-shop
│ │ ├── list.jsx
│ │ ├── index.less
│ │ └── index.jsx
│ ├── address-nearby
│ │ └── index.less
│ ├── index.js
│ ├── address-edit
│ │ └── index.less
│ ├── restaurant
│ │ └── index.jsx
│ ├── order-detail
│ │ └── index.less
│ ├── login
│ │ └── index.less
│ └── profile
│ │ ├── index.less
│ │ └── index.jsx
├── assets
│ ├── img
│ │ └── thanks.gif
│ ├── svg
│ │ ├── compass.svg
│ │ ├── triangle_down_fill.svg
│ │ ├── back.svg
│ │ ├── unfold.svg
│ │ ├── people.svg
│ │ ├── right.svg
│ │ ├── close.svg
│ │ ├── filter.svg
│ │ ├── check.svg
│ │ ├── form.svg
│ │ ├── iphone.svg
│ │ ├── crown.svg
│ │ ├── location.svg
│ │ ├── search.svg
│ │ ├── edit.svg
│ │ ├── delete.svg
│ │ ├── round_add.svg
│ │ ├── success.svg
│ │ ├── refresh.svg
│ │ ├── avatar.svg
│ │ ├── elem.svg
│ │ ├── cry.svg
│ │ ├── cart.svg
│ │ ├── new.svg
│ │ ├── drumstick.svg
│ │ ├── shop.svg
│ │ └── gold.svg
│ └── css
│ │ ├── common.less
│ │ ├── theme.less
│ │ └── mixin.less
├── components
│ ├── loading
│ │ ├── loading.gif
│ │ ├── index.less
│ │ └── index.jsx
│ ├── icon-svg
│ │ ├── index.less
│ │ └── index.jsx
│ ├── rate
│ │ ├── index.less
│ │ └── index.jsx
│ ├── vertical-slide
│ │ ├── index.less
│ │ └── index.jsx
│ ├── async-loade.jsx
│ ├── modal
│ │ └── index.jsx
│ ├── scroll
│ │ └── index.less
│ ├── slide
│ │ └── index.less
│ └── toast
│ │ ├── index.less
│ │ ├── index.jsx
│ │ ├── notices.jsx
│ │ └── notification.jsx
├── utils
│ ├── config.js
│ ├── event-proxy.js
│ ├── utils.js
│ └── dom.js
├── stores
│ ├── global.js
│ ├── shopping-cart.js
│ ├── index.js
│ ├── order.js
│ ├── compass.js
│ ├── shop.js
│ ├── search-shop.js
│ └── home.js
├── index.js
├── index.html
├── api
│ ├── http.js
│ └── index.js
└── .eslintrc
├── screenshot
├── demo.gif
├── find.png
├── home.png
├── login.png
├── order.png
├── user.png
├── hongbao.png
├── search.png
├── my-address.png
├── shop-list.png
├── order-detail.png
├── shop-detail.png
├── shop-list-1.png
├── shop-list-2.png
├── search-address.png
└── update-address.png
├── .gitignore
├── .editorconfig
├── .postcssrc.js
├── .babelrc
├── LICENSE
├── static
└── css
│ └── reset.css
├── README.md
└── package.json
/src/pages/index.less:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/screenshot/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/demo.gif
--------------------------------------------------------------------------------
/screenshot/find.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/find.png
--------------------------------------------------------------------------------
/screenshot/home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/home.png
--------------------------------------------------------------------------------
/screenshot/login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/login.png
--------------------------------------------------------------------------------
/screenshot/order.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/order.png
--------------------------------------------------------------------------------
/screenshot/user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/user.png
--------------------------------------------------------------------------------
/screenshot/hongbao.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/hongbao.png
--------------------------------------------------------------------------------
/screenshot/search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/search.png
--------------------------------------------------------------------------------
/screenshot/my-address.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/my-address.png
--------------------------------------------------------------------------------
/screenshot/shop-list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/shop-list.png
--------------------------------------------------------------------------------
/src/assets/img/thanks.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/src/assets/img/thanks.gif
--------------------------------------------------------------------------------
/screenshot/order-detail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/order-detail.png
--------------------------------------------------------------------------------
/screenshot/shop-detail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/shop-detail.png
--------------------------------------------------------------------------------
/screenshot/shop-list-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/shop-list-1.png
--------------------------------------------------------------------------------
/screenshot/shop-list-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/shop-list-2.png
--------------------------------------------------------------------------------
/screenshot/search-address.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/search-address.png
--------------------------------------------------------------------------------
/screenshot/update-address.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/update-address.png
--------------------------------------------------------------------------------
/src/components/loading/loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/src/components/loading/loading.gif
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | .DS_Store
3 | node_modules/
4 | npm-debug.log
5 | dist/
6 | .idea
7 | .vscode
8 | tree.md
9 | *.idea
10 |
--------------------------------------------------------------------------------
/src/components/icon-svg/index.less:
--------------------------------------------------------------------------------
1 |
2 |
3 | .icon-svg {
4 | width: 1em;
5 | height: 1em;
6 | vertical-align: middle;
7 | fill: currentColor;
8 | overflow: hidden;
9 | }
10 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 |
2 | root = true
3 |
4 | [*]
5 | chartset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | end_of_line = lf
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 |
--------------------------------------------------------------------------------
/src/pages/common-components/no-data/index.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import React from 'react'
4 | import styles from './index.less'
5 |
6 | export default ({ text = '没有更多数据' }) => (
7 |
{text}
8 | )
9 |
--------------------------------------------------------------------------------
/src/pages/common-components/no-data/index.less:
--------------------------------------------------------------------------------
1 |
2 | @import '../../../assets/css/theme.less';
3 |
4 | .desc {
5 | text-align: center;
6 | font-size: @font-size-medium;
7 | font-weight: @font-weight-small;
8 | color: @color-base;
9 | padding: 20px 0;
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/rate/index.less:
--------------------------------------------------------------------------------
1 |
2 |
3 | .rate-wrapper {
4 | position: relative;
5 | width: 100%;
6 | height: 100%;
7 | .rate {
8 | position: absolute;
9 | top: 0;
10 | left: 0;
11 | bottom: 0;
12 | width: 0;
13 | overflow: hidden;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/pages/common-components/badge/index.jsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react'
3 | import cls from 'classnames'
4 | import styles from './index.less'
5 |
6 | export default ({ text, className, style }) => (
7 |
8 | )
9 |
--------------------------------------------------------------------------------
/src/pages/common-components/lazy-loading/index.jsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react'
3 | import science from 'assets/img/science.svg'
4 | import styles from './index.less'
5 |
6 | export default () => (
7 |
8 |
9 |

10 |
11 |
12 | )
13 |
--------------------------------------------------------------------------------
/src/pages/common-components/title-bar/index.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import React from 'react'
4 | import styles from './index.less'
5 |
6 | export default ({ title }) => (
7 |
8 |
9 |
{title}
10 |
11 |
12 | )
13 |
--------------------------------------------------------------------------------
/src/pages/compass/index.less:
--------------------------------------------------------------------------------
1 |
2 | @import '../../assets/css/theme.less';
3 | @import '../../assets/css/mixin.less';
4 |
5 | .compass {
6 | width: 100%;
7 | height: 100%;
8 | background-color: @fill-body;
9 | position: relative;
10 | .scroll {
11 | top: @bar-height;
12 | background-color: @fill-body;
13 | text-align: center;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/loading/index.less:
--------------------------------------------------------------------------------
1 |
2 | @import '../../assets/css/theme.less';
3 |
4 | .loading {
5 | width: 100%;
6 | text-align: center;
7 | display: flex;
8 | justify-content: center;
9 | align-items: center;
10 | .desc {
11 | line-height: 20px;
12 | font-size: @font-size-small;
13 | font-weight: @font-weight-small;
14 | color: @color-base;
15 | padding-left: 8px;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/.postcssrc.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = {
3 | plugins: {
4 | "autoprefixer": {},
5 | "postcss-px-to-viewport": {
6 | "viewportWidth": 750,
7 | "viewportHeight": 1334,
8 | "unitPrecision": 2,
9 | "viewportUnit": "vw",
10 | "selectorBlackList": [".ignore", ".hairlines"],
11 | "minPixelValue": 1,
12 | "mediaQuery": false
13 | },
14 | "postcss-viewport-units": {},
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "presets": [
4 | [
5 | "env", {
6 | "modules": false,
7 | "targets": {
8 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
9 | },
10 | "useBuiltIns": true
11 | }
12 | ],
13 | "react",
14 | "stage-0"
15 | ],
16 | "plugins": [
17 | "react-hot-loader/babel",
18 | "transform-decorators-legacy",
19 | "transform-runtime"
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/src/pages/benefit/index.less:
--------------------------------------------------------------------------------
1 |
2 |
3 | @import '../../assets/css/theme.less';
4 | @import '../../assets/css/mixin.less';
5 |
6 |
7 | .benefit {
8 | background-color: @fill-body-darken;
9 | position: fixed;
10 | .position-full;
11 | z-index: 1;
12 | .scroll {
13 | top: @bar-height;
14 | background-color: @fill-body-darken;
15 | box-sizing: border-box;
16 | padding: 0 20px;
17 | padding-top: 20px;
18 | }
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/src/pages/common-components/title-bar/index.less:
--------------------------------------------------------------------------------
1 |
2 | @import '../../../assets/css/mixin.less';
3 | @import '../../../assets/css/theme.less';
4 |
5 | .title {
6 | width: 100%;
7 | height: 72px;
8 | display: flex;
9 | .flex-center;
10 | .split {
11 | width: 40px;
12 | height: 2px;
13 | background-color: @color-base;
14 | }
15 | .text {
16 | font-size: @font-size-medium-x;
17 | color: @color-title;
18 | margin: 0 26px;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/pages/common-components/lazy-loading/index.less:
--------------------------------------------------------------------------------
1 |
2 | @import '../../../assets/css/theme.less';
3 | @import '../../../assets/css/mixin.less';
4 |
5 | .loading {
6 | position: fixed;
7 | .position-full;
8 | background-color: @fill-body-darken;
9 | .icon {
10 | width: 300px;
11 | height: 300px;
12 | position: absolute;
13 | left: 50%;
14 | top: 50%;
15 | transform: translate(-50%, -50%);
16 | img {
17 | width: 100%;
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/utils/config.js:
--------------------------------------------------------------------------------
1 |
2 | export default {
3 | shopImgSize: '?imageMogr/format/webp/thumbnail/!130x130r/gravity/Center/crop/130x130/',
4 | sexMap: ['女士', '先生'],
5 | addressTag: ['', '家', '学校', '公司'],
6 | orderByMap: [
7 | {
8 | id: 0,
9 | name: '综合排序',
10 | },
11 | {
12 | id: 6,
13 | name: '销量最高',
14 | },
15 | {
16 | id: 1,
17 | name: '起送价最低',
18 | },
19 | {
20 | id: 2,
21 | name: '起送价最低',
22 | },
23 | ],
24 | }
25 |
--------------------------------------------------------------------------------
/src/pages/order/index.less:
--------------------------------------------------------------------------------
1 |
2 |
3 | @import '../../assets/css/theme.less';
4 | @import '../../assets/css/mixin.less';
5 |
6 | .order {
7 | width: 100%;
8 | height: 100%;
9 | background-color: @fill-body-darken;
10 | position: relative;
11 | z-index: 1;
12 | .nav {
13 | position: absolute;
14 | top: 0;
15 | left: 0;
16 | z-index: 2;
17 | }
18 | .scroll {
19 | overflow: visible;
20 | top: @bar-height;
21 | z-index: 1;
22 | background-color: @fill-body-darken;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/stores/global.js:
--------------------------------------------------------------------------------
1 |
2 | const UPDATE = 'GLOBAL_UPDATE'
3 |
4 | const initState = {
5 | isLogin: false,
6 | userInfo: {},
7 | }
8 |
9 | export const globalState = (state = initState, action) => {
10 | switch (action.type) {
11 | case UPDATE:
12 | return {
13 | ...state,
14 | ...action.payload,
15 | }
16 | default:
17 | return state
18 | }
19 | }
20 |
21 | export const globalUpdate = (params) => {
22 | return {
23 | payload: params,
24 | type: UPDATE,
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/pages/common-components/gallery/index.less:
--------------------------------------------------------------------------------
1 |
2 | .gallery {
3 | position: fixed;
4 | z-index: 1;
5 | top: 0;
6 | left: 0;
7 | right: 0;
8 | bottom: 0;
9 | .slide {
10 | position: absolute;
11 | width: 600px;
12 | top: 50%;
13 | left: 50%;
14 | transform: translate3d(-50%, -50%, 0);
15 | z-index: 2;
16 | }
17 | .mask {
18 | position: fixed;
19 | top: 0;
20 | left: 0;
21 | right: 0;
22 | bottom: 0;
23 | z-index: 1;
24 | background-color: rgba(0,0,0,.5);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/pages/common-components/address-row/index.jsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react'
3 | import styles from './index.less'
4 |
5 | export default class AddressRow extends React.PureComponent {
6 | render() {
7 | const { data, handleClick } = this.props
8 | return (
9 |
10 |
{data.name}
11 |
{data.address}
12 |
13 |
14 | )
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/pages/common-components/badge/index.less:
--------------------------------------------------------------------------------
1 |
2 | @import '../../../assets/css/theme.less';
3 |
4 | .badge {
5 | background-color: @badge-color-open;
6 | overflow: hidden;
7 | &::after {
8 | position: absolute;
9 | top: 50%;
10 | left: 50%;
11 | content: attr(content) !important;
12 | text-align: center;
13 | color: #fff;
14 | font-size: @font-size-small;
15 | font-weight: @font-weight-small;
16 | transform: scale(.8) translate3D(-50%, -50%, 0);
17 | transform-origin: 0 0;
18 | white-space: nowrap;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/pages/place-order/index.less:
--------------------------------------------------------------------------------
1 |
2 |
3 | @import '../../assets/css/theme.less';
4 | @import '../../assets/css/mixin.less';
5 |
6 | .place-order {
7 | position: fixed;
8 | .position-full;
9 | z-index: 2;
10 | background-color: @fill-body;
11 | .content {
12 | text-align: center;
13 | img {
14 | width: 300px;
15 | height: 300px;
16 | content: none !important;
17 | }
18 | p {
19 | text-align: center;
20 | font-weight: @font-weight-small;
21 | color: @color-title;
22 | margin: 20px;
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/pages/common-components/skeleton/recommend.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import React from 'react'
4 | import styles from './index.less'
5 |
6 | export default class RecommendSk extends React.PureComponent {
7 | render() {
8 | return (
9 |
17 | )
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/pages/common-components/skeleton/entry.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import React from 'react'
4 | import styles from './index.less'
5 |
6 | export default class EntrySk extends React.PureComponent {
7 | render() {
8 | return (
9 |
10 | {
11 | Array.from({ length: 10 }, (v, i) => i).map(v => (
12 |
16 | ))
17 | }
18 |
19 | )
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/pages/common-components/skeleton/row.jsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react'
3 | import styles from './index.less'
4 |
5 | export default class RowSK extends React.PureComponent {
6 | render() {
7 | return (
8 |
17 | )
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/icon-svg/index.jsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react'
3 | import cls from 'classnames'
4 | import PropTypes from 'prop-types'
5 | import styles from './index.less'
6 |
7 | const SvgIcon = ({ name, className, onClick = () => {} }) => {
8 | const clsName = className ? cls(styles['icon-svg'], className) : styles['icon-svg'];
9 | return (
10 |
13 | );
14 | };
15 |
16 | SvgIcon.propTypes = {
17 | name: PropTypes.string,
18 | className: PropTypes.string,
19 | };
20 |
21 | export default SvgIcon
22 |
--------------------------------------------------------------------------------
/src/pages/home/skeleton-screen/index.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import React from 'react'
4 | import EntrySk from '../../common-components/skeleton/entry'
5 | import RowSk from '../../common-components/skeleton/row'
6 |
7 | export default class SkeletionScreen extends React.PureComponent {
8 | render() {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | )
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/vertical-slide/index.less:
--------------------------------------------------------------------------------
1 |
2 | .slide {
3 | min-height: 1px;
4 | position: absolute;
5 | top: 0;
6 | left: 0;
7 | right: 0;
8 | bottom: 0;
9 | overflow: hidden;
10 | .slide-group {
11 | position: relative;
12 | overflow: hidden;
13 | white-space: nowrap;
14 | .slide-item {
15 | box-sizing: border-box;
16 | overflow: hidden;
17 | a {
18 | display: block;
19 | height: 100%;
20 | overflow: hidden;
21 | text-decoration: none;
22 | }
23 | img {
24 | display: block;
25 | height: 100%;
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/assets/svg/compass.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/place-order/index.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import React from 'react'
4 | import NavBar from '../common-components/nav-bar'
5 | import thanks from '../../assets/img/thanks.gif'
6 | import styles from './index.less'
7 |
8 | export default class PlaceOrder extends React.Component {
9 | render() {
10 | return (
11 |
12 |
this.props.history.goBack()} />
16 |
17 |

18 |
老铁!给个star吧!
19 |
20 |
21 | )
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/assets/svg/triangle_down_fill.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/back.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/stores/shopping-cart.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | const UPDATE = 'SHOPPINGCART_UPDATE'
4 |
5 | // {
6 | // restaurant_id: '商店id',
7 | // food_id: '商品id',
8 | // name: '名字',
9 | // price: '价格',
10 | // quantity: '数量',
11 | // attrs: '属性',
12 | // new_specs: '规格'
13 | // }
14 |
15 | const initState = {
16 | cart: [],
17 | }
18 |
19 | export const shoppingCart = (state = initState, action) => {
20 | switch (action.type) {
21 | case UPDATE:
22 | return {
23 | ...state,
24 | ...action.payload,
25 | }
26 | default:
27 | return state
28 | }
29 | }
30 |
31 | export const shoppingCartUpdate = (params) => {
32 | return {
33 | payload: params,
34 | type: UPDATE,
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/pages/common-components/address-row/index.less:
--------------------------------------------------------------------------------
1 |
2 |
3 | @import '../../../assets/css/theme.less';
4 | @import '../../../assets/css/mixin.less';
5 |
6 | .row {
7 | .title {
8 | padding-right: 30px;
9 | box-sizing: border-box;
10 | font-size: @font-size-medium;
11 | color: @color-title;
12 | line-height: 32px;
13 | .no-wrap;
14 | padding-top: 20px;
15 | }
16 | .desc {
17 | padding-right: 30px;
18 | box-sizing: border-box;
19 | font-size: @font-size-small;
20 | font-weight: @font-weight-small;
21 | color: @color-base;
22 | line-height: 32px;
23 | .no-wrap;
24 | margin: 14px 0 20px 0;
25 | }
26 | .line {
27 | width: 100%;
28 | height: 1px;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/pages/common-components/auth-err/index.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import React from 'react'
4 | import { withRouter } from 'react-router-dom'
5 | import styles from './index.less'
6 |
7 | @withRouter
8 | export default class AuthErr extends React.PureComponent {
9 | render() {
10 | const goLogin = () => {
11 | this.props.history.push('/login')
12 | }
13 | return (
14 |
15 |
16 |

17 |
18 |
登录后查看更多信息
19 |
20 |
21 | )
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/async-loade.jsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react'
3 |
4 | export default (loadCompoent, loading) => {
5 | return class AsyncComponet extends React.Component {
6 | constructor(props) {
7 | super(props)
8 | this.state = {
9 | C: null,
10 | }
11 | this.unmount = false
12 | }
13 |
14 | async componentDidMount() {
15 | const { default: C } = await loadCompoent()
16 | if (this.unmount) return
17 | this.setState({ C }) // eslint-disable-line
18 | }
19 |
20 | componentWillUnmount() {
21 | this.unmount = true
22 | }
23 |
24 | render() {
25 | const { C } = this.state;
26 | return C ? : loading
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/assets/css/common.less:
--------------------------------------------------------------------------------
1 |
2 | @import './theme.less';
3 |
4 | :global {
5 | .hairline-h {
6 | position: relative;
7 | border: none;
8 | &:after {
9 | content: '' !important;
10 | position: absolute;
11 | left: 0;
12 | background: @hairline-color;
13 | width: 100%;
14 | height: 1px;
15 | transform: scaleY(0.5);
16 | transform-origin: 0 0;
17 | }
18 | }
19 |
20 | .hairline-v {
21 | position: relative;
22 | border: none;
23 | &:after {
24 | content: '' !important;
25 | position: absolute;
26 | left: 0;
27 | background: @hairline-color;
28 | height: 100%;
29 | width: 1px;
30 | transform: scaleX(0.5);
31 | transform-origin: 0 0;
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/stores/index.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | import {
4 | createStore,
5 | applyMiddleware,
6 | combineReducers,
7 | } from 'redux'
8 | import thunk from 'redux-thunk'
9 | import { home } from './home'
10 | import { globalState } from './global'
11 | import { order } from './order'
12 | import { shop } from './shop'
13 | import { compass } from './compass'
14 | import { restaurant } from './restaurant'
15 | import { searchShop } from './search-shop'
16 | import { shoppingCart } from './shopping-cart'
17 |
18 | const store = createStore(
19 | combineReducers({
20 | home,
21 | globalState,
22 | order,
23 | shop,
24 | compass,
25 | restaurant,
26 | searchShop,
27 | shoppingCart,
28 | }),
29 | applyMiddleware(thunk),
30 | )
31 |
32 | export default store
33 |
--------------------------------------------------------------------------------
/src/assets/svg/unfold.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/people.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/common-components/auth-err/index.less:
--------------------------------------------------------------------------------
1 |
2 | @import '../../../assets/css/theme.less';
3 |
4 | .err {
5 | width: 100%;
6 | text-align: center;
7 | .img {
8 | width: 600px;
9 | margin: 0 auto;
10 | img {
11 | width: 100%;
12 | }
13 | }
14 | .desc {
15 | margin: 20px 0;
16 | font-size: @font-size-medium-x;
17 | font-weight: @font-weight-small;
18 | color: @color-base;
19 | }
20 | .login {
21 | margin: 0 auto;
22 | display: block;
23 | background-color: #4cd96f;
24 | border-radius: 8px;
25 | outline: none;
26 | border: none;
27 | width: 600px;
28 | height: 84px;
29 | line-height: 84px;
30 | margin-top: 40px;
31 | font-size: @font-size-medium-x;
32 | font-weight: @font-weight-small;
33 | color:#fff;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/assets/svg/right.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/modal/index.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import React from 'react'
4 | import ReactDOM from 'react-dom'
5 | import PropTypes from 'prop-types'
6 | import QueueAnim from 'rc-queue-anim'
7 |
8 | export default class Modal extends React.Component {
9 | static propTypes = {
10 | visible: PropTypes.bool,
11 | }
12 |
13 | constructor(props) {
14 | super(props)
15 | this.el = document.createElement('div')
16 | }
17 |
18 | componentDidMount() {
19 | document.body.appendChild(this.el)
20 | }
21 |
22 | componentWillUnmount() {
23 | document.body.removeChild(this.el)
24 | }
25 | render() {
26 | const { visible, children } = this.props
27 | return ReactDOM.createPortal(
28 |
29 | {
30 | visible ? children : null
31 | }
32 | ,
33 | this.el,
34 | )
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/scroll/index.less:
--------------------------------------------------------------------------------
1 |
2 | @import '../../assets/css/theme.less';
3 | @import '../../assets/css/mixin.less';
4 |
5 | .list-wrapper {
6 | position: absolute;
7 | .position-full;
8 | overflow: hidden;
9 | background-color: @fill-body;
10 | .scroll-content {
11 | position: relative;
12 | z-index: 10;
13 | }
14 | .pullup-wrapper {
15 | width: 100%;
16 | padding-top: 20px;
17 | padding-bottom: 20px;
18 | .flex-center;
19 | }
20 | .after-trigger,.before-trigger,.refresh-txt {
21 | .flex-center;
22 | span {
23 | padding: 5px 0;
24 | font-size: @font-size-medium;
25 | font-weight: @font-weight-small;
26 | color: @color-base;
27 | }
28 | }
29 | .pulldown-wrapper {
30 | position: absolute;
31 | z-index: 1;
32 | width: 100%;
33 | left: 0;
34 | .flex-center;
35 | transition: all
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/pages/home/index.less:
--------------------------------------------------------------------------------
1 |
2 |
3 | @import '../../assets/css/theme.less';
4 | @import '../../assets/css/mixin.less';
5 |
6 | .root {
7 | width: 100%;
8 | height: 100%;
9 | position: relative;
10 | z-index: 1;
11 | .scroll {
12 | z-index: 1;
13 | overflow: visible;
14 | }
15 | .entry-wrapper {
16 | background-color: @fill-body;
17 | display: flex;
18 | flex-wrap: wrap;
19 | margin-bottom: 30px;
20 | .item {
21 | width: 20vw;
22 | margin-top: 22px;
23 | .flex-center;
24 | flex-direction: column;
25 | .img {
26 | width: 90px;
27 | height: 90px;
28 | img {
29 | width: 100%;
30 | }
31 | }
32 | .name {
33 | margin-top: 10px;
34 | font-size: @font-size-small;
35 | font-weight: @font-weight-small;
36 | color: @color-base;
37 | }
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/assets/svg/close.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/common-components/nav-bar/index.less:
--------------------------------------------------------------------------------
1 |
2 |
3 | @import '../../../assets/css/mixin.less';
4 | @import '../../../assets/css/theme.less';
5 |
6 | .nav {
7 | width: 100%;
8 | height: @bar-height;
9 | background-image: linear-gradient(-90deg, @primary-color, @primary-color-light);
10 | box-sizing: border-box;
11 | display: flex;
12 | justify-content: space-between;
13 | align-items: center;
14 | position: relative;
15 | .icon {
16 | flex: 0 0 @bar-height;
17 | width: @bar-height;
18 | height: @bar-height;
19 | display: flex;
20 | .flex-center;
21 | font-size: @font-size-large-x;
22 | color: #fff;
23 | }
24 | .title {
25 | position: absolute;
26 | left: 50%;
27 | top: 0;
28 | height: @bar-height;
29 | line-height: @bar-height;
30 | font-size: @font-size-medium-x;
31 | transform: translateX(-50%);
32 | color: #fff;
33 | text-align: center;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/assets/css/theme.less:
--------------------------------------------------------------------------------
1 |
2 | @hd: 2;
3 | @tab-theme: #f8f7f5;
4 | @tab-height: 100px;
5 |
6 | @bar-height: 100px;
7 | @item-height: 88px;
8 | @input-height: 96px;
9 |
10 | @shop-cart-height: 140px;
11 |
12 | // @white-space-height: 20px;
13 | // @white-space-color: #f4f4f4;
14 |
15 |
16 | @skeleton-color: #f3f3f3;
17 | @skeleton-color-darken: #e6e6e6;
18 | @hairline-color: #eee;
19 |
20 | @badge-color-open: rgb(84, 206, 117);
21 | @badge-color-first: rgb(112, 188, 70); // 首单color
22 |
23 | @border-color: #eee;
24 |
25 | @primary-color: #0085ff;
26 | @primary-color-light: #0af;
27 |
28 | @color-title: #333;
29 | @color-base: #666;
30 | @fill-body: #fff;
31 | @fill-body-darken: #f5f5f5;
32 |
33 | @font-weight-small: 300;
34 | @font-weight-medium: 400;
35 |
36 | @font-size-small-s: 10px * @hd;
37 | @font-size-small: 12px * @hd;
38 | @font-size-medium: 14px * @hd;
39 | @font-size-medium-x: 16px * @hd;
40 | @font-size-large: 18px * @hd;
41 | @font-size-large-x: 22px * @hd;
42 |
--------------------------------------------------------------------------------
/src/assets/svg/filter.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/address/address-row/index.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import React from 'react'
4 | import SvgIcon from 'components/icon-svg'
5 | import config from 'utils/config'
6 | import styles from './index.less'
7 |
8 | export default class AddressRow extends React.PureComponent {
9 | render() {
10 | const { data } = this.props
11 | const { sexMap } = config
12 | return (
13 |
14 |
15 |
16 |
{data.name}
17 | {sexMap[data.sex]}
18 | {data.phone}
19 |
20 |
{data.address + data.address_detail}
21 |
22 |
23 |
24 |
25 |
26 | )
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/assets/svg/check.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/form.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/iphone.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/crown.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 |
2 | /*eslint-disable*/
3 | import React from 'react'
4 | import ReactDOM from 'react-dom'
5 | import { Provider } from 'react-redux';
6 | import { BrowserRouter } from 'react-router-dom'
7 | import { AppContainer } from 'react-hot-loader'
8 | import store from './stores'
9 | import App from './pages'
10 | import './assets/css/common.less'
11 |
12 | // requires and returns all modules that match
13 | const requireAll = requireContext => requireContext.keys().map(requireContext)
14 | // import all svg
15 | const reqSvg = require.context('./assets/svg', true, /\.svg$/)
16 | requireAll(reqSvg)
17 |
18 | const render = Component => (
19 | ReactDOM.render((
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | ), document.getElementById('root'))
28 | )
29 |
30 | render(App)
31 |
32 | if (module.hot) {
33 | module.hot.accept('./pages', () => {
34 | render(App)
35 | })
36 | }
37 |
--------------------------------------------------------------------------------
/src/assets/svg/location.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/shop-detail/foods/menu.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import React from 'react'
4 | import cls from 'classnames'
5 | import { connect } from 'react-redux'
6 | import Scroll from 'components/scroll'
7 | import styles from './index.less'
8 |
9 | const mapStateToProps = ({ shop }) => ({
10 | menu: shop.menu,
11 | foodMenuIndex: shop.foodMenuIndex,
12 | })
13 | @connect(mapStateToProps)
14 | export default class Menu extends React.PureComponent {
15 | render() {
16 | const { menu, foodMenuIndex, menuClick } = this.props
17 | const menuCls = v => cls([styles.item, v === foodMenuIndex ? styles.active : null])
18 | return (
19 |
20 |
21 | {
22 | menu.map((v, i) => (
23 | menuClick(i)}>
24 | {v.name}
25 |
26 | ))
27 | }
28 |
29 |
30 | )
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/slide/index.less:
--------------------------------------------------------------------------------
1 |
2 | .slide {
3 | min-height: 1px;
4 | position: relative;
5 | overflow: hidden;
6 | .slide-group {
7 | white-space: nowrap;
8 | .slide-item {
9 | float: left;
10 | box-sizing: border-box;
11 | overflow: hidden;
12 | text-align: center;
13 | a {
14 | display: block;
15 | width: 100%;
16 | overflow: hidden;
17 | text-decoration: none;
18 | }
19 | img {
20 | display: block;
21 | width: 100%;
22 | }
23 | }
24 | }
25 | .dots {
26 | position: absolute;
27 | right: 0;
28 | left: 0;
29 | bottom: 12px;
30 | transform: translateZ(1px);
31 | text-align: center;
32 | font-size: 0;
33 | .dot {
34 | display: inline-block;
35 | margin: 0 4px;
36 | width: 12px;
37 | height: 12px;
38 | border-radius: 50%;
39 | background-color: rgba(255, 255, 255, .5);
40 | &.active {
41 | width: 20px;
42 | border-radius: 5px;
43 | background-color: #f95108;
44 | }
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | eleme
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/components/loading/index.jsx:
--------------------------------------------------------------------------------
1 |
2 | /* eslint-disable */
3 | import React from 'react'
4 | import Proptypes from 'prop-types'
5 | import loading from './loading.gif'
6 | import styles from './index.less'
7 |
8 | export default class Loading extends React.Component {
9 | static proptypes = {
10 | title: Proptypes.string,
11 | style: Proptypes.object,
12 | }
13 |
14 | static defaultProps = {
15 | title: '',
16 | style: {},
17 | }
18 |
19 | constructor(props) {
20 | super(props)
21 | this.ratio = window.devicePixelRatio
22 | this.state = {
23 | width: 14 * this.ratio,
24 | height: 14 * this.ratio,
25 | }
26 | }
27 |
28 | render() {
29 | const { width, height } = this.state
30 | const { title, style } = this.props
31 | return (
32 |
33 |

38 |
正在加载...
39 |
40 | )
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/pages/address/address-row/index.less:
--------------------------------------------------------------------------------
1 |
2 |
3 | @import '../../../assets/css/theme.less';
4 | @import '../../../assets/css/mixin.less';
5 |
6 | .row {
7 | width: 100%;
8 | box-sizing: border-box;
9 | padding: 30px;
10 | background-color: @fill-body;
11 | border-bottom: 1px solid @border-color;
12 | display: flex;
13 | .desc {
14 | flex: 1;
15 | width: 0;
16 | .info {
17 | display: flex;
18 | align-items: center;
19 | height: 50px;
20 | line-height: 50px;
21 | .name {
22 | font-size: @font-size-medium-x;
23 | color: @color-title;
24 | }
25 | .sex {
26 | margin: 0 10px;
27 | }
28 | .sex,.phone {
29 | font-size: @font-size-medium;
30 | }
31 | }
32 | .address {
33 | font-size: @font-size-small;
34 | font-weight: @font-weight-small;
35 | line-height: 34px;
36 | }
37 | }
38 | .edit {
39 | flex: 0 0 100px;
40 | width: 100px;
41 | display: flex;
42 | justify-content: flex-end;
43 | align-items: center;
44 | font-size: @font-size-large-x;
45 | color: #999;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 gaojingran960611
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/assets/svg/search.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/stores/order.js:
--------------------------------------------------------------------------------
1 |
2 | import Toast from 'components/toast'
3 | import { getOrderList } from '../api'
4 |
5 | const UPDATE = 'ORDER_UPDATE'
6 |
7 | const initState = {
8 | init: false,
9 | orderList: [],
10 | }
11 |
12 | export const order = (state = initState, action) => {
13 | switch (action.type) {
14 | case UPDATE:
15 | return {
16 | ...state,
17 | ...action.payload,
18 | }
19 | default:
20 | return state
21 | }
22 | }
23 |
24 | export const orderUpdate = (params) => {
25 | return {
26 | payload: params,
27 | type: UPDATE,
28 | }
29 | }
30 |
31 | export const fetchOrderList = (isRefresh, callback) => {
32 | return async (dispatch, getState) => {
33 | const { orderList } = getState().order
34 | try {
35 | const { data } = await getOrderList({
36 | limit: 8,
37 | offset: isRefresh ? 0 : orderList.length,
38 | })
39 | dispatch(orderUpdate({
40 | init: true,
41 | orderList: isRefresh ? data : [...orderList, ...data],
42 | }))
43 | callback && callback() // eslint-disable-line
44 | } catch ({ err }) {
45 | Toast.info(err, 3, false)
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/api/http.js:
--------------------------------------------------------------------------------
1 |
2 | import axios from 'axios'
3 |
4 | const successCode = 0
5 | const instance = axios.create({
6 | baseURL: '/api',
7 | withCredentials: true, // 跨域类型时是否在请求中协带cookie
8 | })
9 |
10 | export default class HttpUtil {
11 | static get(url, params = {}) {
12 | return new Promise((resolve, reject) => {
13 | instance.get(url, { params }).then(({ data }) => {
14 | if (data.code === successCode) {
15 | const { result } = data
16 | resolve({ data: result })
17 | } else {
18 | reject({ err: data.errmsg, name: data.name || '' })
19 | }
20 | }).catch((err) => {
21 | reject({ err: JSON.stringify(err) })
22 | })
23 | })
24 | }
25 |
26 | static post(url, params = {}) {
27 | return new Promise((resolve, reject) => {
28 | instance.post(url, { data: params }).then(({ data }) => {
29 | if (data.code === successCode) {
30 | const { result } = data
31 | resolve({ data: result })
32 | } else {
33 | reject({ err: data.errmsg, name: data.name || '' })
34 | }
35 | }).catch((err) => {
36 | reject({ err: JSON.stringify(err) })
37 | })
38 | })
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/assets/svg/edit.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/shop-detail/skeleton-screen/index.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import React from 'react'
4 | import Row from '../../common-components/skeleton/row'
5 | import styles from './index.less'
6 |
7 | export default class Skeleton extends React.PureComponent {
8 | render() {
9 | return (
10 |
11 |
17 |
18 |
19 | {
20 | Array.from({ length: 9 }, (v, i) => i + 1).map(v => (
21 |
22 |
23 |
24 | ))
25 | }
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | )
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/assets/css/mixin.less:
--------------------------------------------------------------------------------
1 |
2 | .no-wrap {
3 | overflow: hidden;
4 | text-overflow: ellipsis;
5 | white-space: nowrap;
6 | word-break: break-all;
7 | }
8 |
9 | .extend-click {
10 | position: relative;
11 | &:before {
12 | content: '' !important;
13 | position: absolute;
14 | top: -10px;
15 | left: -10px;
16 | right: -10px;
17 | bottom: -10px;
18 | }
19 | }
20 |
21 | .clearfix {
22 | display: inline-block;
23 | &:after {
24 | display: block;
25 | content: '' !important;
26 | height: 0;
27 | line-height: 0;
28 | clear: both;
29 | visibility: hidden;
30 | }
31 | }
32 |
33 | .position-full {
34 | top: 0;
35 | left: 0;
36 | right: 0;
37 | bottom: 0;
38 | }
39 |
40 | .flex-center {
41 | display: flex;
42 | align-items: center;
43 | justify-content: center;
44 | }
45 |
46 | .placeholder-color(@color) {
47 | input::-webkit-input-placeholder, textarea::-webkit-input-placeholder {
48 | color: @color;
49 | }
50 | input:-moz-placeholder, textarea:-moz-placeholder {
51 | color: @color;
52 | }
53 | input::-moz-placeholder, textarea::-moz-placeholder {
54 | color: @color;
55 | }
56 | input:-ms-input-placeholder, textarea:-ms-input-placeholder {
57 | color: @color;
58 | }
59 | }
60 |
61 |
--------------------------------------------------------------------------------
/src/pages/address/index.less:
--------------------------------------------------------------------------------
1 | @import '../../assets/css/theme.less';
2 | @import '../../assets/css/mixin.less';
3 |
4 | @btn-height: 100px;
5 |
6 | .address {
7 | width: 100%;
8 | height: 100%;
9 | background-color: @fill-body-darken;
10 | position: relative;
11 | z-index: 1;
12 | .list {
13 | position: fixed;
14 | top: @bar-height;
15 | left: 0;
16 | right: 0;
17 | bottom: @btn-height;
18 | .scroll {
19 | background-color: @fill-body-darken;
20 | }
21 | }
22 | .add {
23 | position: fixed;
24 | z-index: 1;
25 | bottom: 0;
26 | left: 0;
27 | width: 100%;
28 | height: @btn-height;
29 | line-height: @btn-height;
30 | background-color: @fill-body;
31 | box-sizing: border-box;
32 | padding: 0;
33 | border: none;
34 | border-top: 1px solid @border-color;
35 | font-size: 0;
36 | color: @primary-color-light;
37 | .icon {
38 | display: inline-block;
39 | vertical-align: middle;
40 | font-size: @font-size-large;
41 | margin-right: 8px;
42 | margin-top: -4px;
43 | }
44 | span {
45 | display: inline-block;
46 | vertical-align: middle;
47 | font-size: @font-size-medium-x;
48 | font-weight: @font-weight-small;
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/pages/common-components/gallery/index.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import React from 'react'
4 | import PropTypes from 'prop-types'
5 | import Modal from 'components/modal'
6 | import Slide from 'components/slide'
7 | import styles from './index.less'
8 |
9 | export default class Gallery extends React.Component {
10 | static propTypes = {
11 | visible: PropTypes.bool,
12 | handleCancel: PropTypes.func,
13 | photos: PropTypes.array,
14 | }
15 |
16 | render() {
17 | const { visible, photos, handleCancel = () => {} } = this.props
18 | return (
19 |
20 |
21 |
22 | {
23 | photos.length ? (
24 |
1}
26 | showDot={false}
27 | autoPlay={false}>
28 | {
29 | photos.map((v, i) => (
30 |
31 | ))
32 | }
33 |
34 | ) : null
35 | }
36 |
37 |
38 |
39 |
40 | )
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/assets/svg/delete.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/common-components/recommend-food-row/index.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import React from 'react'
4 | import SvgIcon from 'components/icon-svg'
5 | import { getImageUrl } from 'utils/utils'
6 | import styles from './index.less'
7 |
8 | export default class RecommedFoodRow extends React.PureComponent {
9 | render() {
10 | const { data, rowClick } = this.props
11 | const { food, restaurant } = data
12 | const foodUrl = getImageUrl(food.image_path)
13 | return (
14 |
15 |
16 |

17 |
{food.reason}
18 |
19 |
20 |
{food.name}
21 |
{`月售${food.month_sales} 好评率${food.satisfy_rate}%`}
22 |
23 | ¥
24 | {food.price}
25 |
26 |
27 |
28 |
{restaurant.name}
29 |
30 |
31 |
32 | )
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/utils/event-proxy.js:
--------------------------------------------------------------------------------
1 |
2 | // http://taobaofed.org/blog/2016/11/17/react-components-communication/
3 | /*eslint-disable*/
4 | const eventProxy = {
5 | onObj: {},
6 | oneObj: {},
7 | on: function(key, fn) {
8 | if(this.onObj[key] === undefined) {
9 | this.onObj[key] = []
10 | }
11 | this.onObj[key].push(fn)
12 | },
13 | one: function(key, fn) {
14 | if(this.oneObj[key] === undefined) {
15 | this.oneObj[key] = []
16 | }
17 | this.oneObj[key].push(fn)
18 | },
19 | off: function(key) {
20 | this.onObj[key] = []
21 | this.oneObj[key] = []
22 | },
23 | trigger: function() {
24 | let key, args
25 | if(arguments.length == 0) {
26 | return false
27 | }
28 | key = arguments[0]
29 | args = [].concat(Array.prototype.slice.call(arguments, 1))
30 |
31 | if(this.onObj[key] !== undefined && this.onObj[key].length > 0) {
32 | for(let i in this.onObj[key]) {
33 | this.onObj[key][i].apply(null, args)
34 | }
35 | }
36 | if(this.oneObj[key] !== undefined && this.oneObj[key].length > 0) {
37 | for(let i in this.oneObj[key]) {
38 | this.oneObj[key][i].apply(null, args)
39 | this.oneObj[key][i] = undefined
40 | }
41 | this.oneObj[key] = []
42 | }
43 | }
44 | }
45 |
46 | export default eventProxy
47 |
--------------------------------------------------------------------------------
/src/assets/svg/round_add.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/.eslintrc:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "parser": "babel-eslint",
4 | "extends": "airbnb",
5 | "globals": {
6 | "document": true,
7 | "AMap": true,
8 | "window": true
9 | },
10 | "rules": {
11 | "semi": [0],
12 | "camelcase": [0],
13 | "no-plusplus": [0],
14 | "consistent-return": [0],
15 | "arrow-body-style": [0],
16 | "react/prop-types": [0],
17 | "no-unneeded-ternary": [0],
18 | "react/no-find-dom-node": [0],
19 | "react/jsx-filename-extension": [0],
20 | "array-callback-return": [0],
21 | "no-mixed-operators": [0],
22 | "no-nested-ternary": [0],
23 | "jsx-a11y/alt-text": [0],
24 | "jsx-a11y/anchor-is-valid": [0],
25 | "react/forbid-prop-types": [0],
26 | "react/require-default-props": [0],
27 | "class-methods-use-this": [0],
28 | "prefer-promise-reject-errors": [0],
29 | "jsx-a11y/click-events-have-key-events": [0],
30 | "jsx-a11y/no-noninteractive-element-interactions": [0],
31 | "react/no-array-index-key": [0],
32 | "import/no-extraneous-dependencies": [0],
33 | "import/no-unresolved": [0],
34 | "import/extensions": [0],
35 | "react/jsx-closing-bracket-location": [0],
36 | "react/prefer-stateless-function": [0],
37 | "jsx-a11y/no-static-element-interactions": [0],
38 | "react/jsx-boolean-value": [0],
39 | "no-console": [0],
40 | "no-return-assign": [0]
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/pages/common-components/tab-bar/index.less:
--------------------------------------------------------------------------------
1 |
2 | @import '../../../assets/css/mixin.less';
3 | @import '../../../assets/css/theme.less';
4 |
5 | .root {
6 | position: fixed;
7 | .position-full;
8 | overflow: hidden;
9 | z-index: 1;
10 | background-color: @fill-body;
11 | padding-bottom: @tab-height;
12 | padding-bottom: calc(@tab-height + constant(safe-area-inset-bottom));
13 | padding-bottom: calc(@tab-height + env(safe-area-inset-bottom));
14 | }
15 |
16 | .tab-wrapper {
17 | position: fixed;
18 | z-index: 1;
19 | left: 0;
20 | right: 0;
21 | height: @tab-height;
22 | padding-bottom: constant(safe-area-inset-bottom);
23 | padding-bottom: env(safe-area-inset-bottom);
24 | background-color: @tab-theme;
25 | box-shadow: 0 -0.5px 2px rgba(0,0,0,.1);
26 | .flex-center;
27 | .item {
28 | flex: 1;
29 | flex-direction: column;
30 | .flex-center;
31 | .icon {
32 | width: 44px;
33 | height: 44px;
34 | fill: @color-base;
35 | &.scale {
36 | transform: scale(1.67);
37 | transform-origin: center;
38 | }
39 | }
40 | .text {
41 | font-weight: @font-weight-small;
42 | font-size: @font-size-small-s;
43 | color: @color-base;
44 | margin-top: 8px;
45 | }
46 | &.active {
47 | .icon {
48 | fill: @primary-color;
49 | }
50 | .text {
51 | color: @primary-color;
52 | }
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/assets/svg/success.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/utils/utils.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | /*eslint-disable*/
4 | /***
5 | * array = [
6 | * { a: 1 }
7 | * { a: 1 }
8 | * { b: 1 }
9 | * ]
10 | */
11 | export const unique = (array, key) => {
12 | const filter = array.reduce((acc, val) => {
13 | acc[val[key]] = val;
14 | return acc
15 | }, {})
16 | let result = []
17 | for (let key in filter) {
18 | result.push(filter[key])
19 | }
20 | return result
21 | }
22 |
23 | export const getGuid = (len = 8, radix = 2) => {
24 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
25 | const r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
26 | return v.toString(16);
27 | });
28 | }
29 |
30 | export const formatPhone = (phone) => {
31 | return phone.substr(0, 3) + '****' + phone.substr(7, 11)
32 | }
33 |
34 | const baseUrl = '//fuss10.elemecdn.com'
35 | export const getImageUrl = (hash) => {
36 | if (!hash) {
37 | return null
38 | }
39 | const suffixArray = ['png', 'bmp', 'jpg', 'gif', 'jpeg', 'svg']
40 | const suffix = suffixArray.find(v => hash.indexOf(v) !== -1)
41 | if (!suffix) {
42 | return null
43 | } else {
44 | return `${baseUrl}/${hash.substring(0,1)}/${hash.substring(1,3)}/${hash.substring(3)}.${suffix}`
45 | }
46 | }
47 |
48 | export const debounce = function(fn, interval = 600) {
49 | let timeout = null
50 | return function() {
51 | clearTimeout(timeout)
52 | timeout = setTimeout(() => {
53 | fn.apply(this, arguments)
54 | }, interval)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/assets/svg/refresh.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/pages/common-components/red-ticket-row/index.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import React from 'react'
4 | import SvgIcon from 'components/icon-svg'
5 | import styles from './index.less'
6 |
7 | export default class RedTicketRow extends React.PureComponent {
8 | render() {
9 | const { data, rowClick } = this.props
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | ¥
17 | {data.amount}
18 |
19 |
{data.description_map.sum_condition}
20 |
21 |
22 |
{data.name}
23 |
{data.description_map.validity_periods}
24 |
{data.description_map.phone}
25 |
26 |
27 |
{data.description_map.validity_delta}
28 |
29 |
30 |
31 |
32 |
33 | {data.limit_map.restaurant_flavor_ids}
34 |
35 |
36 |
37 | )
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/pages/common-components/nav-bar/index.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import React from 'react'
4 | import Proptypes from 'prop-types'
5 | import cls from 'classnames'
6 | import SvgIcon from 'components/icon-svg'
7 | import styles from './index.less'
8 |
9 | export default class NavBar extends React.PureComponent {
10 | /* eslint-disable */
11 | static proptypes = {
12 | iconLeft: Proptypes.string,
13 | iconRight: Proptypes.string,
14 | leftClick: Proptypes.func,
15 | rightClick: Proptypes.func,
16 | title: Proptypes.string,
17 | className: Proptypes.string,
18 | }
19 | /* eslint-enable */
20 |
21 | static defaultProps = {
22 | iconLeft: '',
23 | iconRight: '',
24 | leftClick: () => {},
25 | rightClick: () => {},
26 | title: '',
27 | }
28 |
29 | render() {
30 | const {
31 | iconLeft,
32 | iconRight,
33 | leftClick,
34 | rightClick,
35 | title,
36 | className,
37 | } = this.props
38 | return (
39 |
40 | {
41 | iconLeft ? (
42 |
43 |
44 |
45 | ) : null
46 | }
47 |
{title}
48 | {
49 | iconRight ? (
50 |
51 |
52 |
53 | ) : null
54 | }
55 |
56 | )
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/assets/svg/avatar.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/pages/home/top-bar/index.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import React from 'react'
4 | import { connect } from 'react-redux'
5 | import { bindActionCreators } from 'redux'
6 | import { withRouter } from 'react-router-dom'
7 | import cls from 'classnames'
8 | import SvgIcon from 'components/icon-svg'
9 | import { homeUpdate } from '../../../stores/home'
10 | import styles from './index.less'
11 |
12 | const mapStateToProps = ({ home }) => ({
13 | topBarShrink: home.topBarShrink,
14 | locationInfo: home.locationInfo,
15 | })
16 | const mapActionsToProps = dispatch => bindActionCreators({ homeUpdate }, dispatch)
17 |
18 | @connect(mapStateToProps, mapActionsToProps)
19 | @withRouter
20 | export default class TopBar extends React.PureComponent {
21 | render() {
22 | const { topBarShrink, history } = this.props
23 | const { address } = this.props.locationInfo
24 | const clsname = cls({
25 | [styles.header]: true,
26 | [styles.shrink]: topBarShrink,
27 | })
28 | return (
29 |
30 |
history.push('/search-address')}>
31 |
32 |
{address ? address : '正在识别地址...'}
33 |
34 |
35 |
history.push('/search-shop')}>
36 |
37 |
搜索饿了么商家、商品名称
38 |
39 |
40 | )
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/toast/index.less:
--------------------------------------------------------------------------------
1 |
2 |
3 | @maskIndex: 1000;
4 | @noticeIndex: 1100;
5 | @toast-bg-color: rgba(0, 0, 0, .7);
6 | @font-size: 28px;
7 | @line-height: 34px;
8 |
9 | .mask {
10 | position: fixed;
11 | z-index: @maskIndex;
12 | top: 0;
13 | left: 0;
14 | right: 0;
15 | bottom: 0;
16 | }
17 |
18 | .notification-container {
19 | .notification-box {
20 | .notice-container {
21 | position: absolute;
22 | z-index: @noticeIndex;
23 | top: 30%;
24 | left: 50%;
25 | padding: 20px 30px;
26 | max-width: 500px;
27 | white-space: pre-wrap;
28 | word-break: break-all;
29 | background-color: @toast-bg-color;
30 | color: #fff;
31 | font-size: @font-size;
32 | line-height: @line-height;
33 | text-align: center;
34 | border-radius: 10px;
35 |
36 | transition: all .3s;
37 | transform: translate3D(-50%, -50%, 0) scale(1, 1);
38 |
39 | animation: slideInFromBottom .3s linear;
40 | &.leave {
41 | transform: translate3D(-50%, -100%, 0);
42 | opacity: 0;
43 | }
44 |
45 | .notice {
46 | .icon {
47 | width: 60px;
48 | height: 60px;
49 | fill: #fff;
50 | margin-bottom: 12px;
51 | &.loading {
52 | animation: rotation 1s linear infinite;
53 | }
54 | }
55 | }
56 | }
57 | }
58 | }
59 |
60 | // 从底部滑入
61 | @keyframes slideInFromBottom {
62 | 0% {
63 | transform: translate(-50%, 0%);
64 | opacity: 0;
65 | }
66 | 100% {
67 | transform: translate(-50%, -50%);
68 | opacity: 1;
69 | }
70 | }
71 |
72 | @keyframes rotation {
73 | to {
74 | transform: rotate(360deg);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/stores/compass.js:
--------------------------------------------------------------------------------
1 |
2 | import Toast from 'components/toast'
3 | import { getRecommendation, getGeolocation } from '../api'
4 | import { homeUpdate } from './home'
5 |
6 | const UPDATE = 'COMPASS_UPDATE'
7 |
8 | const initState = {
9 | init: false,
10 | foodList: [],
11 | rank_id: undefined,
12 | }
13 |
14 | export const compass = (state = initState, action) => {
15 | switch (action.type) {
16 | case UPDATE:
17 | return {
18 | ...state,
19 | ...action.payload,
20 | }
21 | default:
22 | return state
23 | }
24 | }
25 |
26 | export const compassUpdate = (params) => {
27 | return {
28 | payload: params,
29 | type: UPDATE,
30 | }
31 | }
32 |
33 | export const fetchFoodList = () => {
34 | return async (dispatch, getState) => {
35 | const { foodList, rank_id } = getState().compass
36 | let { locationInfo } = getState().home
37 | if (!locationInfo.latitude && !locationInfo.longitude) {
38 | try {
39 | const { data } = await getGeolocation()
40 | if (data) {
41 | locationInfo = data
42 | dispatch(homeUpdate({
43 | locationInfo: data,
44 | }))
45 | }
46 | } catch ({ err }) {
47 | return Toast.info(err)
48 | }
49 | }
50 | try {
51 | const { data } = await getRecommendation({
52 | rank_id,
53 | limit: 20,
54 | offset: foodList.length,
55 | latitude: locationInfo.latitude,
56 | longitude: locationInfo.longitude,
57 | })
58 | dispatch(compassUpdate({
59 | init: true,
60 | rank_id: data.rank_id,
61 | foodList: [...foodList, ...data.items],
62 | }))
63 | } catch ({ err }) {
64 | return Toast.info(err)
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/pages/common-components/tab-bar/index.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import React from 'react'
4 | import cls from 'classnames'
5 | import SvgIcon from 'components/icon-svg'
6 | import styles from './index.less'
7 |
8 | export default (Component) => {
9 | return class TabBar extends React.Component {
10 | render() {
11 | const { pathname } = this.props.location
12 | const itemCls = name => cls({
13 | [styles.item]: true,
14 | [styles.active]: pathname === name,
15 | })
16 | const handleClick = (path) => {
17 | if (path === pathname) return
18 | this.props.history.push(path)
19 | }
20 | return (
21 |
22 |
23 |
24 |
handleClick('/home')}>
25 |
26 |
微淘
27 |
28 |
handleClick('/compass')}>
29 |
30 |
发现
31 |
32 |
handleClick('/order')}>
33 |
34 |
订单
35 |
36 |
handleClick('/profile')}>
37 |
38 |
我的
39 |
40 |
41 |
42 | )
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/pages/search-shop/list.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import React from 'react'
4 | import { connect } from 'react-redux'
5 | import { withRouter } from 'react-router-dom'
6 | import Loading from 'components/loading'
7 | import Scroll from 'components/scroll'
8 | import NoData from '../common-components/no-data'
9 | import ShopListRow from '../common-components/shop-list-row'
10 | import styles from './index.less'
11 |
12 | @connect(({ searchShop, home }) => ({
13 | shopLists: searchShop.shopLists,
14 | loading: searchShop.loading,
15 | locationInfo: home.locationInfo,
16 | }))
17 | @withRouter
18 | export default class ShopList extends React.PureComponent {
19 | refreshScroll = () => {
20 | this.scroll && this.scroll.refresh() // eslint-disable-line
21 | }
22 |
23 | handleRowClick = (val) => {
24 | const { history, locationInfo } = this.props
25 | const { id } = val
26 | const { latitude, longitude } = locationInfo
27 | history.push({
28 | pathname: '/shop-detail',
29 | search: `?restaurant_id=${id}&latitude=${latitude}&longitude=${longitude}`,
30 | })
31 | }
32 |
33 | render() {
34 | const { shopLists, loading } = this.props
35 | return (
36 |
37 | {
38 | loading ? : shopLists.length ? (
39 | this.scroll = c}>
40 | {
41 | shopLists.map((shop, i) => (
42 | this.handleRowClick(shop)}
44 | key={`${shop.id}--${i}--${new Date().getTime()}`}
45 | data={shop}
46 | refresh={this.refreshScroll} />
47 | ))
48 | }
49 |
50 | ) :
51 | }
52 |
53 | )
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/assets/svg/elem.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/pages/address-nearby/index.less:
--------------------------------------------------------------------------------
1 |
2 | @import '../../assets/css/theme.less';
3 | @import '../../assets/css/mixin.less';
4 |
5 | .address {
6 | position: fixed;
7 | .position-full;
8 | background-color: @fill-body;
9 | .list {
10 | position: fixed;
11 | top: @bar-height;
12 | left: 0;
13 | right: 0;
14 | bottom: 0;
15 | overflow: hidden;
16 | .search {
17 | padding: 20px 30px 4px 30px;
18 | display: flex;
19 | align-items: center;
20 | .input {
21 | flex: 1;
22 | box-sizing: border-box;
23 | border-radius: 4px;
24 | overflow: hidden;
25 | background-color: #f5f5f5;
26 | border: 1px solid @border-color;
27 | height: 58px;
28 | line-height: 58px;
29 | box-sizing: border-box;
30 | padding: 0 20px 0 60px;
31 | position: relative;
32 | .icon {
33 | position: absolute;
34 | left: 14px;
35 | top: 50%;
36 | transform: translateY(-50%);
37 | font-size: @font-size-medium;
38 | color: @color-base;
39 | }
40 | input {
41 | display: block;
42 | background-color: transparent;
43 | font-size: @font-size-small;
44 | font-weight: @font-weight-small;
45 | color: @color-base;
46 | outline: none;
47 | width: 100%;
48 | line-height: 58px;
49 | }
50 | }
51 | .btn {
52 | flex: 0 0 120px;
53 | width: 120px;
54 | height: 58px;
55 | margin-left: 20px;
56 | background-color: @primary-color-light;
57 | color: #fff;
58 | font-size: @font-size-medium;
59 | line-height: 58px;
60 | border-radius: 4px;
61 | outline: none;
62 | border: none;
63 | }
64 | }
65 | .container {
66 | box-sizing: border-box;
67 | padding-left: 30px;
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/utils/dom.js:
--------------------------------------------------------------------------------
1 |
2 | /*eslint-disable*/
3 | export const hasClass = (el, className) => {
4 | if (el.classList) {
5 | return el.classList.contains(className)
6 | }
7 | const nameArray = el.className.split(' ')
8 | return nameArray.indexOf(className) > -1
9 | }
10 |
11 | export const addClass = (el, className) => {
12 | if (el.classList) {
13 | el.classList.add(className)
14 | } else {
15 | const newClass = el.className.split(' ').concat([className]).join(' ')
16 | el.className = newClass
17 | }
18 | }
19 |
20 | export const removeClass = (el, className) => {
21 | if (el.classList) {
22 | el.classList.remove(className)
23 | } else {
24 | if (hasClass(el, className)) {
25 | const newClass = el.className.split(' ').filter(name => name !== className).join(' ')
26 | el.className = newClass
27 | }
28 | }
29 | }
30 |
31 | // css3 hark前缀
32 | const elementStyle = document.createElement('div').style
33 | const vendor = (() => {
34 | const transformNames = {
35 | Webkit: 'webkitTransform',
36 | Moz: 'MozTransform',
37 | O: 'OTransform',
38 | ms: 'msTransform',
39 | standard: 'transform',
40 | }
41 | for (let key in transformNames) {
42 | if (elementStyle[transformNames[key]] !== undefined) {
43 | return key
44 | }
45 | }
46 | return false
47 | })()
48 |
49 | export const prefixStyle = (style) => {
50 | if (!vendor) return false
51 | if (vendor === 'standard') {
52 | return style
53 | }
54 | return vendor + style.charAt(0).toUpperCase() + style.substr(1)
55 | }
56 |
57 | export const watchTransitionEvent = () => {
58 | const transitions = {
59 | transition: 'transitionend',
60 | OTransition: 'oTransitionEnd',
61 | MozTransition: 'transitionend',
62 | WebkitTransition: 'webkitTransitionEnd',
63 | };
64 | for (let key in transitions) {
65 | if (elementStyle[key] !== undefined) {
66 | return transitions[key];
67 | }
68 | }
69 | return false;
70 | };
71 |
--------------------------------------------------------------------------------
/src/pages/common-components/order-list-row/index.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import React from 'react'
4 | import numeral from 'numeral'
5 | import SvgIcon from 'components/icon-svg'
6 | import styles from './index.less'
7 |
8 | export default class OrderRow extends React.PureComponent {
9 | render() {
10 | const { data, handleClick } = this.props
11 |
12 | return (
13 |
14 |
15 |
16 |

17 |
18 |
19 |
20 |
21 |
22 |
{data.restaurant_name}
23 |
24 |
25 |
26 |
27 |
{data.status_bar.title}
28 |
29 |
30 |
{data.formatted_created_at}
31 |
32 |
33 |
34 |
35 | {data.basket.group[0][0].name}
36 | {
37 | data.basket.group[0].length ? `等${data.basket.group[0].length}商品` : ''
38 | }
39 |
40 |
¥{numeral(data.total_amount).format('0.00')}
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | )
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/pages/shop-detail/skeleton-screen/index.less:
--------------------------------------------------------------------------------
1 |
2 |
3 | @import '../../../assets/css/theme.less';
4 | @import '../../../assets/css/mixin.less';
5 |
6 | @header-height: 424px; // 商家信息
7 |
8 | .skeleton {
9 | position: fixed;
10 | .position-full;
11 | background-color: @fill-body;
12 | overflow: hidden;
13 | .header {
14 | width: 100%;
15 | height: @header-height;
16 | background-color: @skeleton-color;
17 | display: flex;
18 | flex-direction: column;
19 |
20 | .avatar {
21 | width: 130px;
22 | height: 130px;
23 | margin: 0 auto;
24 | margin-top: 80px;
25 | margin-bottom: 30px;
26 | border-radius: 4px;
27 | background-color: @skeleton-color-darken;
28 | }
29 | .desc {
30 | width: 80%;
31 | height: 20px;
32 | background-color: @skeleton-color-darken;
33 | margin: 10px auto;
34 | &:nth-child(3) {
35 | width: 70%;
36 | }
37 | &:nth-child(4) {
38 | width: 80%;
39 | }
40 | }
41 | }
42 |
43 | .body {
44 | width: 100%;
45 | height: calc(100vh - @header-height);
46 | display: flex;
47 | .left {
48 | flex: 0 0 160px;
49 | width: 160px;
50 | height: 100%;
51 | margin-right: 20px;
52 | background-color: @fill-body-darken;
53 |
54 | .item {
55 | padding: 30px 15px;
56 | border-top: 1px solid @border-color;
57 | &:first-child {
58 | border-top: none;
59 | }
60 | .text {
61 | font-size: @font-size-small;
62 | font-weight: @font-weight-small;
63 | color: @color-base;
64 | min-height: 20px;
65 | width: 80px;
66 | display: block;
67 | background-color: @skeleton-color-darken;
68 | }
69 | }
70 | }
71 | .right {
72 | flex: 1;
73 | }
74 | }
75 |
76 | .footer {
77 | position: absolute;
78 | z-index: 2;
79 | height: @shop-cart-height;
80 | background-color: @skeleton-color-darken;
81 | bottom: 0;
82 | left: 0;
83 | right: 0;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/pages/home/top-bar/index.less:
--------------------------------------------------------------------------------
1 |
2 | @import '../../../assets/css/theme.less';
3 | @import '../../../assets/css/mixin.less';
4 |
5 | @search-height: 72px;
6 |
7 | .header {
8 | position: fixed;
9 | z-index: 2;
10 | top: 0;
11 | left: 0;
12 | box-sizing: border-box;
13 | width: 100%;
14 | padding: 0 28px;
15 | background-image: linear-gradient(-90deg, @primary-color, @primary-color-light);
16 | transition: all .3s ease;
17 | .location {
18 | width: 100%;
19 | height: @bar-height;
20 | font-size: 0;
21 | transition: all .3s ease;
22 | .icon {
23 | display: inline-block;
24 | vertical-align: middle;
25 | width: 42px;
26 | height: 100%;
27 | fill: #fff;
28 | margin-right: 6px;
29 | }
30 | .address {
31 | display: inline-block;
32 | vertical-align: middle;
33 | font-size: @font-size-medium-x;
34 | margin-top: 2px;
35 | color: #fff;
36 | max-width: 330px;
37 | margin-right: 16px;
38 | .no-wrap;
39 | }
40 | .down {
41 | display: inline-block;
42 | vertical-align: middle;
43 | width: 16px;
44 | height: 100%;
45 | fill: #fff;
46 | }
47 | }
48 | .search {
49 | width: 100%;
50 | height: @search-height;
51 | line-height: @search-height;
52 | margin-bottom: (@bar-height - @search-height) / 2;
53 | background-color: @fill-body;
54 | font-size: 0;
55 | text-align: center;
56 | transition: all .3s ease;
57 | .icon {
58 | display: inline-block;
59 | vertical-align: middle;
60 | width: 32px;
61 | height: 100%;
62 | fill: @color-base;
63 | margin-right: 6px;
64 | }
65 | .desc {
66 | display: inline-block;
67 | vertical-align: middle;
68 | font-size: @font-size-medium;
69 | color: @color-base;
70 | font-weight: @font-weight-small;
71 | }
72 | }
73 |
74 | &.shrink {
75 | .location {
76 | height: 0;
77 | opacity: 0;
78 | }
79 | .search {
80 | margin-top: (@bar-height - @search-height) / 2;
81 | border-radius: @bar-height;
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/components/toast/index.jsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react'
3 | import PropTypes from 'prop-types'
4 | import cls from 'classnames'
5 | import Notification from './notification'
6 | import SvgIcon from '../icon-svg'
7 | import styles from './index.less'
8 |
9 | let messageInstance
10 |
11 | const getMessageInstance = (props = {}, callback) => {
12 | if (messageInstance) {
13 | messageInstance.destroy()
14 | messageInstance = null
15 | }
16 | Notification.newInstance(props, (notification) => {
17 | callback && callback(notification) // eslint-disable-line
18 | })
19 | }
20 |
21 | const notice = (content, duration, mask = true, onClose) => {
22 | getMessageInstance({}, (notification) => {
23 | messageInstance = notification
24 | notification.notice({
25 | duration,
26 | mask,
27 | content,
28 | onClose: () => {
29 | if (onClose) onClose()
30 | notification.destroy()
31 | messageInstance = null
32 | },
33 | })
34 | })
35 | }
36 |
37 | const WithIcon = ({ name, content, isLoading = false }) => (
38 |
44 | )
45 |
46 | WithIcon.propTypes = {
47 | isLoading: PropTypes.bool,
48 | name: PropTypes.string,
49 | content: PropTypes.string,
50 | };
51 |
52 | export default {
53 | info: (content, duration, mask = true, onClose) => (notice(content, duration, mask, onClose)),
54 | success: (content, duration, mask = true, onClose) => (notice(, duration, mask, onClose)),
55 | fail: (content, duration, mask = true, onClose) => (notice(, duration, mask, onClose)),
56 | loading: (content, duration, mask = true, onClose) => (notice(, duration, mask, onClose)),
57 | hide: () => {
58 | if (messageInstance) {
59 | messageInstance.destroy()
60 | messageInstance = null
61 | }
62 | },
63 | }
64 |
--------------------------------------------------------------------------------
/src/components/toast/notices.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import React from 'react'
4 | import cls from 'classnames'
5 | import PropTypes from 'prop-types'
6 | import styles from './index.less'
7 |
8 | const noticeProps = {
9 | duration: PropTypes.number,
10 | content: PropTypes.oneOfType([
11 | PropTypes.string,
12 | PropTypes.element,
13 | ]),
14 | onClose: PropTypes.func,
15 | }
16 |
17 | const defaultProps = {
18 | duration: 2,
19 | content: '',
20 | onClose: () => {},
21 | }
22 |
23 | // 动画时间
24 | const animationDuration = 300
25 | const prefixCls = 'notice'
26 |
27 | /**
28 | * Notice 初始化的时候 生成一个定时器
29 | * 根据输入的时间 加载一个动画 然后执行输入的回调
30 | * Notice的显示和隐藏收到父组件Notification的绝对控制
31 | */
32 | export default class Notice extends React.Component {
33 | static defaultProps = defaultProps
34 | static propTypes = noticeProps
35 |
36 | constructor(props) {
37 | super(props)
38 | // shouldClose 用于判断何时改添加上离场动画
39 | this.state = {
40 | shouldClose: false,
41 | }
42 | }
43 |
44 | componentDidMount() {
45 | if (this.props.duration) {
46 | this.closeTimer = setTimeout(() => {
47 | this.onClose()
48 | }, (this.props.duration * 1000) - animationDuration)
49 | }
50 | }
51 |
52 | componentWillUnmount() {
53 | this.clearCloseTimer()
54 | }
55 |
56 | onClose = () => {
57 | // 清除定时器 并且 开启离场动画 并且等待动画结束后执行 onClose回调
58 | this.clearCloseTimer()
59 | this.setState({ shouldClose: true })
60 | this.timer = setTimeout(() => {
61 | if (this.props.onClose) {
62 | this.props.onClose()
63 | }
64 | clearTimeout(this.timer)
65 | }, animationDuration)
66 | }
67 |
68 | clearCloseTimer = () => {
69 | if (this.closeTimer) {
70 | clearTimeout(this.closeTimer)
71 | this.closeTimer = null
72 | }
73 | }
74 |
75 | render() {
76 | const { shouldClose } = this.state
77 | const { content } = this.props
78 |
79 | return (
80 |
86 | )
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/static/css/reset.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Eric Meyer's Reset CSS v2.0 (http://meyerweb.com/eric/tools/css/reset/)
3 | * http://cssreset.com
4 | */
5 |
6 | html,
7 | body,
8 | div,
9 | span,
10 | applet,
11 | object,
12 | iframe,
13 | h1,
14 | h2,
15 | h3,
16 | h4,
17 | h5,
18 | h6,
19 | p,
20 | blockquote,
21 | pre,
22 | a,
23 | abbr,
24 | acronym,
25 | address,
26 | big,
27 | cite,
28 | code,
29 | del,
30 | dfn,
31 | em,
32 | img,
33 | ins,
34 | kbd,
35 | q,
36 | s,
37 | samp,
38 | small,
39 | strike,
40 | strong,
41 | sub,
42 | sup,
43 | tt,
44 | var,
45 | b,
46 | u,
47 | i,
48 | center,
49 | dl,
50 | dt,
51 | dd,
52 | ol,
53 | ul,
54 | li,
55 | fieldset,
56 | form,
57 | label,
58 | legend,
59 | table,
60 | caption,
61 | tbody,
62 | tfoot,
63 | thead,
64 | tr,
65 | th,
66 | td,
67 | article,
68 | aside,
69 | canvas,
70 | details,
71 | embed,
72 | figure,
73 | figcaption,
74 | footer,
75 | header,
76 | menu,
77 | nav,
78 | output,
79 | ruby,
80 | section,
81 | summary,
82 | time,
83 | mark,
84 | audio,
85 | video,
86 | input {
87 | margin: 0;
88 | padding: 0;
89 | border: 0;
90 | font-size: 100%;
91 | font-weight: normal;
92 | vertical-align: baseline;
93 | }
94 |
95 |
96 | /* HTML5 display-role reset for older browsers */
97 |
98 | article,
99 | aside,
100 | details,
101 | figcaption,
102 | figure,
103 | footer,
104 | header,
105 | menu,
106 | nav,
107 | section {
108 | display: block;
109 | }
110 |
111 | body {
112 | line-height: 1;
113 | }
114 |
115 | blockquote,
116 | q {
117 | quotes: none;
118 | }
119 |
120 | blockquote:before,
121 | blockquote:after,
122 | q:before,
123 | q:after {
124 | content: none;
125 | }
126 |
127 | table {
128 | border-collapse: collapse;
129 | border-spacing: 0;
130 | }
131 |
132 |
133 | /* custom */
134 |
135 | a {
136 | color: #818181;
137 | text-decoration: none;
138 | -webkit-backface-visibility: hidden;
139 | }
140 |
141 | html,
142 | body {
143 | width: 100%;
144 | color: #818181;
145 | background-color: #f4f4f4;
146 | font-weight: 200;
147 | font-family: 'PingFang SC', 'STHeitiSC-Light', 'Arial, Helvetica, sans-serif';
148 | }
149 |
--------------------------------------------------------------------------------
/src/pages/shop-detail/shop/index.less:
--------------------------------------------------------------------------------
1 |
2 |
3 | @import '../../../assets/css/theme.less';
4 | @import '../../../assets/css/mixin.less';
5 |
6 | .shop-info {
7 | width: 100%;
8 | height: 100%;
9 | box-sizing: border-box;
10 | padding-bottom: @shop-cart-height;
11 | background-color: @fill-body-darken;
12 | position: relative;
13 | z-index: 1;
14 | .scroll {
15 | background-color: @fill-body-darken;
16 | bottom: @shop-cart-height;
17 | }
18 |
19 | .card {
20 | background-color: @fill-body;
21 | margin-bottom: 20px;
22 | padding: 30px;
23 | .title {
24 | font-weight: bold;
25 | color: #000;
26 | font-size: @font-size-medium;
27 | margin-bottom: 20px;
28 | }
29 | .desc {
30 | font-size: @font-size-small;
31 | font-weight: @font-weight-small;
32 | color: @color-base;
33 | line-height: 40px;
34 | }
35 |
36 | .img {
37 | display: inline-block;
38 | vertical-align: middle;
39 | margin-right: 10px;
40 | margin-top: 10px;
41 | width: 140px;
42 | height: 140px;
43 | img {
44 | width: 100%;
45 | height: 100%;
46 | }
47 | }
48 |
49 | .activities {
50 | display: flex;
51 | align-items: center;
52 | margin-bottom: 10px;
53 | &:last-child {
54 | margin-bottom: 0;
55 | }
56 | .icon {
57 | flex: 0 0 60px;
58 | width: 60px;
59 | height: 30px;
60 | color: #fff;
61 | position: relative;
62 | }
63 | .tips {
64 | flex: 1;
65 | margin-left: 10px;
66 | line-height: 30px;
67 | font-size: @font-size-small;
68 | font-weight: @font-weight-small;
69 | color: @color-base;
70 | }
71 | }
72 |
73 | .item {
74 | display: flex;
75 | border-bottom: 1px solid @border-color;
76 | padding: 30px 0;
77 | justify-content: space-between;
78 | .label {
79 | flex: 0 0 110px;
80 | width: 110px;
81 | font-size: @font-size-small;
82 | color: #000;
83 | }
84 | .value {
85 | flex: 1;
86 | font-size: @font-size-small;
87 | font-weight: @font-weight-small;
88 | color: @color-base;
89 | text-align: right;
90 | }
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/pages/search-shop/index.less:
--------------------------------------------------------------------------------
1 |
2 |
3 | @import '../../assets/css/theme.less';
4 | @import '../../assets/css/mixin.less';
5 |
6 | .search {
7 | position: fixed;
8 | .position-full;
9 | background-color: @fill-body;
10 |
11 | .search-bar {
12 | height: 88px;
13 | box-sizing: border-box;
14 | padding: 20px 30px 4px 30px;
15 | display: flex;
16 | align-items: center;
17 | .input {
18 | flex: 1;
19 | box-sizing: border-box;
20 | border-radius: 4px;
21 | overflow: hidden;
22 | background-color: #f5f5f5;
23 | border: 1px solid @border-color;
24 | height: 58px;
25 | line-height: 58px;
26 | box-sizing: border-box;
27 | padding: 0 20px 0 60px;
28 | position: relative;
29 | .icon {
30 | position: absolute;
31 | left: 14px;
32 | top: 50%;
33 | transform: translateY(-50%);
34 | font-size: @font-size-medium;
35 | color: @color-base;
36 | }
37 | input {
38 | display: block;
39 | background-color: transparent;
40 | font-size: @font-size-small;
41 | font-weight: @font-weight-small;
42 | color: @color-base;
43 | outline: none;
44 | width: 100%;
45 | line-height: 58px;
46 | }
47 | }
48 | .btn {
49 | flex: 0 0 120px;
50 | width: 120px;
51 | height: 58px;
52 | margin-left: 20px;
53 | background-color: @primary-color-light;
54 | color: #fff;
55 | font-size: @font-size-medium;
56 | line-height: 58px;
57 | border-radius: 4px;
58 | outline: none;
59 | border: none;
60 | }
61 | }
62 |
63 | .hot {
64 | padding: 30px;
65 | box-sizing: border-box;
66 | width: 100%;
67 | .title {
68 | font-size: @font-size-medium;
69 | color: @color-title;
70 | }
71 | .badge {
72 | display: inline-block;
73 | padding: 15px 20px;
74 | background-color: @fill-body-darken;
75 | font-size: @font-size-medium;
76 | font-weight: @font-weight-small;
77 | line-height: 40px;
78 | color: @color-base;
79 | border-radius: 4px;
80 | margin-top: 20px;
81 | margin-right: 20px;
82 | }
83 | }
84 | }
85 |
86 | .shops {
87 | position: fixed;
88 | top: @bar-height + 88px;
89 | left: 0;
90 | right: 0;
91 | bottom: 0;
92 | background-color: @fill-body;
93 | }
94 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## React版高仿饿了么webApp
2 | ## 说明
3 | > 此项目部分数据接口需要手机登陆后才能访问,我是在mac上开发的,并且只在chrome和safari以及自己的iphone上简单浏览测试了下,如果有什么问题欢迎提出😋,启动后webpack的warning是因为使用的postcss-viewport-units适配vw,wh,vmin,vmax后会在css的content的打上viewport-units-buggyfill, 所以当我用!important修改content时postcss-viewport-units会给出警告,如果看着不爽可以在node_modules/postcss-viewport-units/index.js中注释掉warning就行了
4 |
5 | ## 技术栈
6 | >react, react-router4, redux, redux-thunk, betterScroll, webpack4
7 |
8 | ##项目运行
9 | >首先clone数据接口,请确保3333端口没被占用或自行更改app/app.js中的端口号,🙈因为重点是前端(其实是我不会后端😂)所以只是用express和axios转发了请求,代码比较粗糙,也没有使用babel,所以请确保使用高版本node😋
10 |
11 | ```
12 | $ git clone https://github.com/gaojingran/eleme-api.git
13 | $ npm install
14 | $ npm start
15 | ```
16 | >前端项目
17 |
18 | ```
19 | $ git clone https://github.com/gaojingran/react-eleme.git
20 | $ npm install
21 | $ npm start
22 | ```
23 |
24 | ## 效果演示
25 | [查看demo请戳这里](http://elm.superoreo.cn)(请用chrome手机模式预览))
26 |
27 |
28 |
29 | ## 项目截图
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/assets/svg/cry.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/svg/cart.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/rate/index.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import React from 'react'
4 | import PropTypes from 'prop-types'
5 | import cls from 'classnames';
6 | import { prefixStyle } from 'utils/dom'
7 | import styles from './index.less'
8 |
9 | /**
10 | * color 颜色
11 | * value 数值
12 | * length 星星颗数
13 | * size 星星大小
14 | * animate 动画时间
15 | * readonly 是否只读
16 | */
17 | const rateProps = {
18 | className: PropTypes.string,
19 | color: PropTypes.string,
20 | value: PropTypes.number,
21 | length: PropTypes.number,
22 | animate: PropTypes.number,
23 | size: PropTypes.string,
24 | // readonly: PropTypes.bool,
25 | }
26 |
27 | const defaultValue = {
28 | className: '',
29 | color: '#fed100',
30 | value: 0,
31 | length: 5,
32 | size: '1em',
33 | animate: 0,
34 | // readonly: true,
35 | }
36 |
37 | const transition = prefixStyle('transition')
38 |
39 | export default class Rate extends React.Component {
40 | static defaultProps = defaultValue
41 | static propTypes = rateProps
42 |
43 | constructor(props) {
44 | super(props)
45 | this.state = {
46 | stars: new Array(props.length - 0).fill('★'),
47 | hollow: new Array(props.length - 0).fill('☆'),
48 | styleObject: {},
49 | }
50 | this.styleFont = {
51 | color: props.color,
52 | fontSize: props.size,
53 | }
54 | }
55 |
56 | componentDidMount() {
57 | if (!this.props.animate) {
58 | this.setStyle()
59 | }
60 | this.timer = setTimeout(() => this.setStyle(), 60)
61 | }
62 |
63 | componentWillUnmount() {
64 | if (this.timer) {
65 | clearTimeout(this.timer)
66 | this.timer = null
67 | }
68 | }
69 |
70 | setStyle = () => {
71 | this.setState(() => {
72 | return {
73 | styleObject: {
74 | width: `${this.props.value}em`,
75 | [transition]: `width ${this.props.animate}s`,
76 | },
77 | }
78 | })
79 | }
80 |
81 | render() {
82 | const { stars, hollow, styleObject } = this.state
83 | const { className } = this.props
84 | return (
85 |
86 | {
87 | hollow.map((v, i) => (
88 |
{v}
89 | ))
90 | }
91 |
92 | {
93 | stars.map((v, i) => (
94 | {v}
95 | ))
96 | }
97 |
98 |
99 | )
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/pages/benefit/index.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import React from 'react'
4 | import Scroll from 'components/scroll'
5 | import { connect } from 'react-redux'
6 | import Toast from 'components/toast'
7 | import Loading from 'components/loading'
8 | import NavBar from '../common-components/nav-bar'
9 | import AuthError from '../common-components/auth-err'
10 | import NoData from '../common-components/no-data'
11 | import RedTicketRow from '../common-components/red-ticket-row'
12 | import { getHongbaos } from '../../api'
13 | import styles from './index.less'
14 |
15 | @connect(({ globalState }) => ({
16 | isLogin: globalState.isLogin,
17 | }))
18 | export default class Benefit extends React.Component {
19 | constructor(props) {
20 | super(props)
21 | this.state = {
22 | list: [],
23 | init: true,
24 | hasMore: true,
25 | }
26 | props.isLogin && this.getList(true) // eslint-disable-line
27 | }
28 |
29 | componentWillReceiveProps(nextProps) {
30 | if (nextProps.isLogin && nextProps.isLogin !== this.props.isLogin) {
31 | this.getList(true)
32 | }
33 | }
34 |
35 | getList = async (isRefresh = false) => {
36 | const { list } = this.state
37 | let nextPayload = {}
38 | try {
39 | const { data } = await getHongbaos({
40 | limit: 20,
41 | offset: isRefresh ? 0 : list.length,
42 | })
43 | nextPayload = {
44 | list: [...list, ...data],
45 | hasMore: data.length === 20,
46 | }
47 | } catch ({ err }) {
48 | Toast.info(err)
49 | }
50 | this.setState({
51 | ...nextPayload,
52 | init: false,
53 | })
54 | }
55 |
56 | render() {
57 | const { isLogin, history } = this.props
58 | const { list, init, hasMore } = this.state
59 | const scrollProps = {
60 | className: styles.scroll,
61 | dataSource: list,
62 | pullDownRefresh: { stop: 40 },
63 | pullUpLoad: hasMore,
64 | pullingDown: () => this.getList(true),
65 | pullingUp: () => this.getList(),
66 | }
67 |
68 | return !isLogin ? : (
69 |
70 | this.props.history.goBack()} />
74 | {
75 | init ? : list.length ? (
76 |
77 | {
78 | list.map(v => (
79 | history.push('/home')} />
80 | ))
81 | }
82 |
83 | ) :
84 | }
85 |
86 | )
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/pages/address/index.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import React from 'react'
4 | import { connect } from 'react-redux'
5 | import Toast from 'components/toast'
6 | import SvgIcon from 'components/icon-svg'
7 | import Scroll from 'components/scroll'
8 | import Loading from 'components/loading'
9 | import NavBar from '../common-components/nav-bar'
10 | import NoData from '../common-components/no-data'
11 | import AuthError from '../common-components/auth-err'
12 | import AddressRow from './address-row'
13 | import { getAddress } from '../../api'
14 | import styles from './index.less'
15 |
16 | @connect(({ globalState }) => ({
17 | isLogin: globalState.isLogin,
18 | }))
19 | export default class Address extends React.Component {
20 | constructor(props) {
21 | super(props)
22 | this.state = {
23 | list: [],
24 | loading: false,
25 | }
26 | }
27 |
28 | componentDidMount() {
29 | this.props.isLogin && this.getAddress() // eslint-disable-line
30 | }
31 |
32 | componentWillReceiveProps(nextProps) {
33 | if (nextProps.isLogin && !this.state.loading) {
34 | this.getAddress()
35 | }
36 | }
37 |
38 | getAddress = async () => {
39 | try {
40 | this.setState({ loading: true })
41 | const { data } = await getAddress()
42 | this.setState({
43 | list: data,
44 | loading: false,
45 | })
46 | } catch ({ err }) {
47 | this.setState({ loading: false })
48 | Toast.info(err)
49 | }
50 | }
51 |
52 | goEdit = (val = false) => {
53 | this.props.history.push({
54 | pathname: '/address-edit',
55 | state: val,
56 | })
57 | }
58 |
59 | render() {
60 | const { isLogin } = this.props
61 | const { list, loading } = this.state
62 |
63 | return !isLogin ? : (
64 |
65 |
this.props.history.goBack()} />
69 | {
70 | loading ? : list.length ? (
71 |
72 |
73 | {
74 | list.map(v => (
75 | this.goEdit(v)} />
76 | ))
77 | }
78 |
79 |
80 | ) :
81 | }
82 |
88 |
89 | )
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/stores/shop.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | import Toast from 'components/toast'
4 | import { getShopInfo, getShopRatings, getShopFood, getRatingTags, getRatingScores } from '../api'
5 |
6 | const UPDATE = 'SHOP_UPDATE'
7 |
8 | const initState = {
9 | loading: true,
10 | info: {},
11 | menu: [],
12 | tags: [],
13 | tagIndex: '',
14 | foodMenuIndex: 0,
15 | ratings: [],
16 | restaurant_id: null,
17 | }
18 |
19 | export const shop = (state = initState, action) => {
20 | switch (action.type) {
21 | case UPDATE:
22 | return {
23 | ...state,
24 | ...action.payload,
25 | }
26 | default:
27 | return state
28 | }
29 | }
30 |
31 | export const shopUpdate = (params) => {
32 | return {
33 | payload: params,
34 | type: UPDATE,
35 | }
36 | }
37 |
38 | export const shopDestroy = () => {
39 | return {
40 | payload: initState,
41 | type: UPDATE,
42 | }
43 | }
44 |
45 | export const shopInit = (params) => {
46 | return async (dispatch) => {
47 | const { restaurant_id } = params
48 | try {
49 | const [info, menu, ratings, tags, scores] = await Promise.all([
50 | getShopInfo({
51 | ...params,
52 | terminal: 'h5',
53 | extras: ['activities', 'albums', 'license', 'identification', 'qualification'],
54 | }),
55 | getShopFood({ restaurant_id }),
56 | getShopRatings({
57 | restaurant_id,
58 | has_content: true,
59 | offset: 0,
60 | limit: 8,
61 | }),
62 | getRatingTags({ restaurant_id }),
63 | getRatingScores({ restaurant_id }),
64 | ])
65 | dispatch(shopUpdate({
66 | restaurant_id,
67 | loading: false,
68 | info: info.data,
69 | menu: menu.data,
70 | ratings: ratings.data,
71 | tags: tags.data,
72 | tagIndex: tags.data.length ? tags.data[0].name : '',
73 | scores: scores.data,
74 | }))
75 | } catch ({ err }) {
76 | Toast.info(err, 3, false)
77 | }
78 | }
79 | }
80 |
81 | export const changeRatingTag = (params) => {
82 | return async (dispatch, getState) => {
83 | const { restaurant_id } = getState().shop
84 | Toast.loading('加载中...', 0)
85 | try {
86 | const { data } = await getShopRatings({
87 | restaurant_id,
88 | has_content: true,
89 | tag_name: params,
90 | offset: 0,
91 | limit: 8,
92 | })
93 | dispatch(shopUpdate({
94 | ratings: data,
95 | tagIndex: params,
96 | }))
97 | setTimeout(() => Toast.hide(), 400)
98 | } catch ({ err }) {
99 | Toast.info(err, 3, false)
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/pages/common-components/skeleton/index.less:
--------------------------------------------------------------------------------
1 |
2 |
3 | @import '../../../assets/css/mixin.less';
4 | @import '../../../assets/css/theme.less';
5 |
6 | .entry {
7 | display: flex;
8 | width: 100%;
9 | flex-wrap: wrap;
10 | .item {
11 | width: 20vw;
12 | margin-top: 22px;
13 | .flex-center;
14 | flex-direction: column;
15 | .circle {
16 | width: 90px;
17 | height: 90px;
18 | border-radius: 100%;
19 | background-color: @skeleton-color;
20 | }
21 | .desc {
22 | width: 50px;
23 | height: 20px;
24 | margin-top: 10px;
25 | background-color: @skeleton-color;
26 | }
27 | }
28 | }
29 |
30 |
31 | .row {
32 | display: flex;
33 | padding: 30px 20px;
34 | .left {
35 | flex: 0 0 130px;
36 | width: 130px;
37 | height: 130px;
38 | background-color: @skeleton-color;
39 | margin-right: 20px;
40 | }
41 | .right {
42 | flex: 1;
43 | .desc {
44 | height: 20px;
45 | background-image: linear-gradient(
46 | to right,
47 | @skeleton-color 8%,
48 | #eee 18%,
49 | @skeleton-color 33%
50 | );
51 | margin: 10px 0;
52 | animation: progress 2s linear infinite;
53 | }
54 | .desc:nth-child(1) {
55 | width: 90%;
56 | }
57 | .desc:nth-child(2) {
58 | width: 90%;
59 | }
60 | .desc:nth-child(3) {
61 | width: 80%
62 | }
63 | .desc:nth-child(4) {
64 | width: 70%
65 | }
66 |
67 | }
68 | }
69 |
70 | .recommend {
71 | display: inline-block;
72 | border: 1px solid @border-color;
73 | margin: 1vw;
74 | width: 46vw;
75 | background-color: @fill-body;
76 | transform: translate3d(0,0,0);
77 | .pic {
78 | width: 100%;
79 | height: 46vw;
80 | background-color: @skeleton-color;
81 | }
82 | .desc {
83 | width: 100%;
84 | box-sizing: border-box;
85 | padding: 8px 16px 8px 20px;
86 | .placeholder {
87 | width: 100%;
88 | height: 20px;
89 | background-image: linear-gradient(
90 | to right,
91 | @skeleton-color 8%,
92 | #eee 18%,
93 | @skeleton-color 33%
94 | );
95 | margin: 10px 0;
96 | animation: progress 3s linear infinite;
97 | }
98 | .placeholder:nth-child(2) {
99 | width: 90%;
100 | }
101 | .placeholder:nth-child(3) {
102 | width: 80%;
103 | }
104 | }
105 | }
106 |
107 | @keyframes progress{
108 | from { background-position: -400px 0 }
109 | to { background-position: 400px 0 }
110 | }
111 |
--------------------------------------------------------------------------------
/src/stores/search-shop.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | import Toast from 'components/toast'
4 | import { getHotKeywords, getGeolocation, getShopListByKw } from '../api'
5 | import { homeUpdate } from './home'
6 |
7 | const UPDATE = 'SEARCHSHOP_UPDATE'
8 |
9 | const initState = {
10 | loading: false,
11 | keywords: '',
12 | shopLists: [],
13 | hotKeys: [],
14 | rank_id: undefined,
15 | }
16 |
17 | export const searchShop = (state = initState, action) => {
18 | switch (action.type) {
19 | case UPDATE:
20 | return {
21 | ...state,
22 | ...action.payload,
23 | }
24 | default:
25 | return state
26 | }
27 | }
28 |
29 | export const searchShopUpdate = (params) => {
30 | return {
31 | payload: params,
32 | type: UPDATE,
33 | }
34 | }
35 |
36 | export const searchShopDestroy = () => {
37 | return {
38 | payload: initState,
39 | type: UPDATE,
40 | }
41 | }
42 |
43 | export const getHotKeys = () => {
44 | return async (dispatch, getState) => {
45 | let { locationInfo } = getState().home
46 | if (!locationInfo.latitude && !locationInfo.longitude) {
47 | try {
48 | const { data } = await getGeolocation()
49 | if (data) {
50 | locationInfo = data
51 | dispatch(homeUpdate({
52 | locationInfo: data,
53 | }))
54 | }
55 | } catch ({ err }) {
56 | return Toast.info(err)
57 | }
58 | }
59 | try {
60 | const { data } = await getHotKeywords({
61 | latitude: locationInfo.latitude,
62 | longitude: locationInfo.longitude,
63 | })
64 | dispatch(searchShopUpdate({
65 | hotKeys: data,
66 | }))
67 | } catch ({ err }) {
68 | Toast.info(err)
69 | }
70 | }
71 | }
72 |
73 | export const getShopList = () => {
74 | return async (dispatch, getState) => {
75 | const { locationInfo } = getState().home
76 | const { keywords } = getState().searchShop
77 | dispatch(searchShopUpdate({
78 | loading: true,
79 | shopLists: [],
80 | }))
81 | let nextPayload = {}
82 | try {
83 | const { data } = await getShopListByKw({
84 | keyword: keywords,
85 | latitude: locationInfo.latitude,
86 | longitude: locationInfo.longitude,
87 | offset: 0,
88 | limit: 15,
89 | search_item_type: 0,
90 | is_rewrite: 1,
91 | extras: ['activities'],
92 | terminal: 'h5',
93 | })
94 | nextPayload = {
95 | shopLists: data,
96 | }
97 | } catch ({ err }) {
98 | Toast.info(err)
99 | }
100 | dispatch(searchShopUpdate({
101 | ...nextPayload,
102 | loading: false,
103 | }))
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-eleme",
3 | "version": "1.0.0",
4 | "description": "饿了么webapp",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "cross-env NODE_ENV=development webpack-dev-server --config build/webpack.config.js",
8 | "clear": "rimraf dist",
9 | "precommit": "eslint --ext .js --ext .jsx src/",
10 | "build": "npm run clear && webpack --config build/webpack.config.js"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/gaojingran/react-eleme.git"
15 | },
16 | "keywords": [
17 | "react",
18 | "redux",
19 | "betterscroll",
20 | "webpack"
21 | ],
22 | "author": "gaojingran",
23 | "license": "MIT",
24 | "bugs": {
25 | "url": "https://github.com/gaojingran/react-eleme/issues"
26 | },
27 | "homepage": "https://github.com/gaojingran/react-eleme#readme",
28 | "devDependencies": {
29 | "autoprefixer": "^8.2.0",
30 | "babel-core": "^6.26.0",
31 | "babel-eslint": "^8.2.3",
32 | "babel-loader": "^7.1.4",
33 | "babel-plugin-transform-decorators-legacy": "^1.3.4",
34 | "babel-plugin-transform-runtime": "^6.23.0",
35 | "babel-preset-env": "^1.6.1",
36 | "babel-preset-react": "^6.24.1",
37 | "babel-preset-stage-0": "^6.24.1",
38 | "babel-runtime": "^6.26.0",
39 | "copy-webpack-plugin": "^4.5.1",
40 | "cross-env": "^5.1.4",
41 | "css-loader": "^0.28.11",
42 | "eslint": "^4.19.1",
43 | "eslint-config-airbnb": "^16.1.0",
44 | "eslint-loader": "^2.0.0",
45 | "eslint-plugin-import": "^2.11.0",
46 | "eslint-plugin-jsx-a11y": "^6.0.3",
47 | "eslint-plugin-react": "^7.7.0",
48 | "file-loader": "^1.1.11",
49 | "happypack": "^5.0.0",
50 | "html-webpack-plugin": "^3.2.0",
51 | "husky": "^0.14.3",
52 | "less": "^3.0.1",
53 | "less-loader": "^4.1.0",
54 | "mini-css-extract-plugin": "^0.4.0",
55 | "postcss-loader": "^2.1.3",
56 | "postcss-px-to-viewport": "0.0.3",
57 | "postcss-viewport-units": "^0.1.4",
58 | "react-hot-loader": "^4.0.1",
59 | "rimraf": "^2.6.2",
60 | "style-loader": "^0.20.3",
61 | "svg-sprite-loader": "^3.7.3",
62 | "webpack": "^4.5.0",
63 | "webpack-cli": "^2.0.14",
64 | "webpack-dev-server": "^3.1.3",
65 | "webpack-merge": "^4.1.2"
66 | },
67 | "dependencies": {
68 | "axios": "^0.18.0",
69 | "better-scroll": "^1.10.2",
70 | "classnames": "^2.2.5",
71 | "lodash.omit": "^4.5.0",
72 | "numeral": "^2.0.6",
73 | "prop-types": "^15.6.1",
74 | "query-string": "^6.0.0",
75 | "rc-queue-anim": "^1.5.0",
76 | "react": "^16.3.1",
77 | "react-dom": "^16.3.1",
78 | "react-redux": "^5.0.7",
79 | "react-router-dom": "^4.2.2",
80 | "react-transition-group": "^2.3.1",
81 | "redux": "^3.7.2",
82 | "redux-thunk": "^2.2.0"
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/pages/common-components/recommend-food-row/index.less:
--------------------------------------------------------------------------------
1 |
2 | @import '../../../assets/css/mixin.less';
3 | @import '../../../assets/css/theme.less';
4 |
5 | .row {
6 | display: inline-block;
7 | border: 1px solid @border-color;
8 | margin: 1vw;
9 | width: 46vw;
10 | background-color: @fill-body;
11 | transform: translate3d(0,0,0);
12 | .pic {
13 | width: 100%;
14 | height: 46vw;
15 | position: relative;
16 | background-color: @fill-body-darken;
17 | img {
18 | width: 100%;
19 | height: 46vw;
20 | content: none !important;
21 | }
22 | .tip {
23 | position: absolute;
24 | left: 0;
25 | bottom: 0;
26 | width: 100%;
27 | height: 40px;
28 | background-color: rgba(0,0,0,.7);
29 | span {
30 | display: inline-block;
31 | line-height: 40px;
32 | transform: scale(.8);
33 | transform-origin: center;
34 | font-size: @font-size-small;
35 | font-weight: @font-weight-small;
36 | color: #fff;
37 | }
38 | }
39 | }
40 |
41 | .desc {
42 | width: 100%;
43 | box-sizing: border-box;
44 | padding: 8px 16px 8px 20px;
45 | background-color: @fill-body-darken;
46 | .title {
47 | .no-wrap;
48 | font-size: @font-size-medium;
49 | color: @color-title;
50 | text-align: left;
51 | line-height: 40px;
52 | margin: 4px 0;
53 | }
54 | .sell {
55 | .no-wrap;
56 | font-size: @font-size-small;
57 | font-weight: @font-weight-small;
58 | color: @color-base;
59 | text-align: left;
60 | line-height: 30px;
61 | transform: scale(.8);
62 | transform-origin: 0 0;
63 | }
64 | .amount {
65 | font-size: 0;
66 | text-align: left;
67 | margin: 4px 0 10px 0;
68 | span {
69 | display: inline-block;
70 | vertical-align: middle;
71 | font-size: @font-size-medium;
72 | color: #ff6000;
73 | }
74 | .unit {
75 | font-size: @font-size-small;
76 | font-weight: @font-weight-small;
77 | margin-right: 4px;
78 | }
79 | }
80 |
81 | .shop {
82 | font-size: 0;
83 | text-align: left;
84 | height: 40px;
85 | line-height: 40px;
86 | border-top: 1px dashed @border-color;
87 | padding-top: 4px;
88 | .icon, .name{
89 | display: inline-block;
90 | vertical-align: middle;
91 | font-size: @font-size-small;
92 | font-weight: @font-weight-small;
93 | color: @color-base;
94 | }
95 | .icon {
96 | font-size: @font-size-large;
97 | }
98 | .name {
99 | margin-left: 10px;
100 | width: 260px;
101 | .no-wrap;
102 | }
103 | }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/pages/index.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | import React from 'react'
4 | import { connect } from 'react-redux'
5 | import { bindActionCreators } from 'redux'
6 | import { Switch, Route, Redirect } from 'react-router-dom'
7 | import asyncLoad from 'components/async-loade'
8 | import Loading from './common-components/lazy-loading'
9 | import { getUserInfo } from '../api'
10 | import { globalUpdate } from '../stores/global'
11 |
12 | @connect(() => ({}), dispatch => bindActionCreators({
13 | globalUpdate,
14 | }, dispatch))
15 | class AuthComponent extends React.Component {
16 | async componentDidMount() {
17 | try {
18 | const { data } = await getUserInfo()
19 | this.props.globalUpdate({
20 | isLogin: true,
21 | userInfo: data,
22 | })
23 | } catch (err) {
24 | this.props.globalUpdate({
25 | globalUpdate: false,
26 | userInfo: {},
27 | })
28 | }
29 | }
30 |
31 | render() {
32 | return null
33 | }
34 | }
35 |
36 | const home = asyncLoad(() => import('./home'), )
37 | const order = asyncLoad(() => import('./order'), )
38 | const orderDetail = asyncLoad(() => import('./order-detail'), )
39 | const compass = asyncLoad(() => import('./compass'), )
40 | const profile = asyncLoad(() => import('./profile'), )
41 | const login = asyncLoad(() => import('./login'), )
42 | const restaurant = asyncLoad(() => import('./restaurant'), )
43 | const shopDetail = asyncLoad(() => import('./shop-detail'), )
44 | const address = asyncLoad(() => import('./address'), )
45 | const addressEdit = asyncLoad(() => import('./address-edit'), )
46 | const searchAddress = asyncLoad(() => import('./address-nearby'), )
47 | const searchShop = asyncLoad(() => import('./search-shop'), )
48 | const benefit = asyncLoad(() => import('./benefit'), )
49 |
50 | export default () => (
51 |
52 |
53 |
54 | } />
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | )
71 |
72 |
--------------------------------------------------------------------------------
/src/pages/address-edit/index.less:
--------------------------------------------------------------------------------
1 |
2 | @import '../../assets/css/theme.less';
3 | @import '../../assets/css/mixin.less';
4 |
5 | .badge {
6 | display: inline-block;
7 | width: 126px;
8 | height: 54px;
9 | line-height: 54px;
10 | background-color: @fill-body;
11 | border: 1px solid @border-color;
12 | text-align: center;
13 | font-size: @font-size-medium;
14 | font-weight: @font-weight-small;
15 | color: @color-title;
16 | margin-right: 10px;
17 | border-radius: 6px;
18 | &.active {
19 | border-color: @primary-color-light;
20 | color: @primary-color-light;
21 | background-color: #eef7ff;
22 | }
23 | }
24 |
25 | .address {
26 | width: 100%;
27 | height: 100%;
28 | background-color: @fill-body-darken;
29 | position: relative;
30 | z-index: 1;
31 |
32 | .form {
33 | width: 100%;
34 | padding-left: 30px;
35 | background-color: @fill-body;
36 | box-sizing: border-box;
37 | .item {
38 | display: flex;
39 | position: relative;
40 | align-items: center;
41 | .line {
42 | width: 100%;
43 | position: absolute;
44 | left: 0;
45 | right: 0;
46 | bottom: 0;
47 | height: 1px;
48 | }
49 | .label {
50 | align-self: flex-start;
51 | flex: 0 0 140px;
52 | width: 140px;
53 | line-height: 38px;
54 | padding: 28px 0;
55 | font-size: @font-size-medium;
56 | color: @color-title;
57 | }
58 | .control {
59 | flex: 1;
60 | width: 0;
61 | position: relative;
62 | .line {
63 | position: static;
64 | height: 1px;
65 | }
66 | .placeholder-color(#c4c4c4);
67 | .input, .textarea {
68 | margin: 28px 0;
69 | width: 100%;
70 | font-size: @font-size-medium;
71 | font-weight: @font-weight-small;
72 | color: @color-base;
73 | line-height: 38px;
74 | outline: none;
75 | }
76 | .textarea {
77 | padding: 0;
78 | margin: 28px 0;
79 | resize: none;
80 | border: none;
81 | }
82 | .tag-wrapper {
83 | padding: 28px 0;
84 | }
85 | }
86 |
87 | .icon {
88 | margin: 0 20px;
89 | flex: 0 0 40px;
90 | width: 40px;
91 | font-size: @font-size-large-x;
92 | color: #999;
93 | }
94 | }
95 | }
96 |
97 | .btn {
98 | margin: 0 auto;
99 | display: block;
100 | background-color: #4cd96f;
101 | border-radius: 8px;
102 | outline: none;
103 | border: none;
104 | width: 90%;
105 | height: 84px;
106 | line-height: 84px;
107 | margin-top: 30px;
108 | font-size: @font-size-medium-x;
109 | font-weight: @font-weight-small;
110 | color:#fff;
111 | }
112 | }
113 |
114 |
--------------------------------------------------------------------------------
/src/pages/restaurant/index.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import React from 'react'
4 | import { connect } from 'react-redux'
5 | import { bindActionCreators } from 'redux'
6 | import Loading from 'components/loading'
7 | import Scroll from 'components/scroll'
8 | import { restaurantDestroy, restaurantInit, fetchShopList } from '../../stores/restaurant'
9 | import ShopListRow from '../common-components/shop-list-row'
10 | import NavBar from '../common-components/nav-bar'
11 | import NoData from '../common-components/no-data'
12 | import SiftFactors from './sift-factors'
13 | import FilterBar from './filter'
14 | import styles from './index.less'
15 |
16 | const mapStateToProp = ({ restaurant, home }) => ({
17 | loading: restaurant.loading,
18 | shopList: restaurant.shopList,
19 | locationInfo: home.locationInfo,
20 | })
21 | const mapDispatchToProps = dispatch => bindActionCreators({
22 | restaurantInit,
23 | restaurantDestroy,
24 | fetchShopList,
25 | }, dispatch)
26 |
27 | @connect(mapStateToProp, mapDispatchToProps)
28 | export default class Shop extends React.Component {
29 | componentDidMount() {
30 | const { location } = this.props
31 | this.props.restaurantInit(location.state)
32 | }
33 |
34 | componentWillUnmount() {
35 | this.props.restaurantDestroy()
36 | }
37 |
38 | refreshScroll = () => {
39 | this.scroll && this.scroll.refresh() // eslint-disable-line
40 | }
41 |
42 | handleRowClick = (val) => {
43 | const { history, locationInfo } = this.props
44 | const { id } = val
45 | const { latitude, longitude } = locationInfo
46 | history.push({
47 | pathname: '/shop-detail',
48 | search: `?restaurant_id=${id}&latitude=${latitude}&longitude=${longitude}`,
49 | })
50 | }
51 |
52 | render() {
53 | const { location, loading, shopList } = this.props
54 |
55 | const renderList = () => {
56 | if (loading && !shopList.length) {
57 | return
58 | }
59 | return (
60 | this.props.fetchShopList({}, true)}
64 | ref={c => this.scroll = c}>
65 | {
66 | shopList.length ? shopList.map((v, i) => (
67 | this.handleRowClick(v)} />
72 | )) :
73 | }
74 |
75 | )
76 | }
77 |
78 | return (
79 |
80 |
this.props.history.goBack()} />
84 |
85 |
86 |
87 | {renderList()}
88 |
89 |
90 | )
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/stores/home.js:
--------------------------------------------------------------------------------
1 |
2 | import omit from 'lodash.omit'
3 | import Toast from 'components/toast'
4 | import { getGeolocation, getEntry, getBanner, getShopList } from '../api'
5 |
6 | const UPDATE = 'HOME_UPDATE'
7 |
8 | const initState = {
9 | init: false,
10 | topBarShrink: false,
11 | locationInfo: {},
12 | banner: [],
13 | entry: [],
14 | shoplist: [],
15 | rank_id: undefined,
16 | }
17 |
18 | export const home = (state = initState, action) => {
19 | switch (action.type) {
20 | case UPDATE:
21 | return {
22 | ...state,
23 | ...action.payload,
24 | }
25 | default:
26 | return state
27 | }
28 | }
29 |
30 | export const homeUpdate = (params) => {
31 | return {
32 | payload: params,
33 | type: UPDATE,
34 | }
35 | }
36 |
37 | export const homeInit = () => {
38 | return async (dispatch, getState) => {
39 | const { init } = getState().home
40 | let { locationInfo } = getState().home
41 | if (init) return
42 | try {
43 | // 定理位置
44 | if (!locationInfo.latitude && !locationInfo.longitude) {
45 | const geoInfo = await getGeolocation()
46 | dispatch(homeUpdate({ locationInfo: geoInfo.data }))
47 | locationInfo = geoInfo.data // eslint-disable-line
48 | }
49 | const location = { ...omit(locationInfo, ['address']) }
50 | // 获取banner entry
51 | const [banner, entry, list] = await Promise.all([
52 | getBanner(location),
53 | getEntry(location),
54 | getShopList({
55 | ...location,
56 | terminal: 'h5',
57 | offset: 0,
58 | limit: 8,
59 | extra_filters: 'home',
60 | extras: ['activities', 'tags'],
61 | rank_id: '',
62 | }),
63 | ])
64 | dispatch(homeUpdate({
65 | banner: banner.data,
66 | entry: entry.data,
67 | shoplist: list.data.items,
68 | rank_id: list.data.meta.rank_id,
69 | init: true,
70 | }))
71 | } catch ({ err }) {
72 | Toast.info(err, 3, false)
73 | }
74 | }
75 | }
76 |
77 | export const homeList = (callback) => {
78 | return async (dispatch, getState) => {
79 | const { rank_id, locationInfo, shoplist } = getState().home // eslint-disable-line
80 | const location = { ...omit(locationInfo, ['address']) }
81 | try {
82 | const list = await getShopList({
83 | ...location,
84 | rank_id: rank_id, // eslint-disable-line
85 | terminal: 'h5',
86 | offset: shoplist.length,
87 | limit: 8,
88 | extra_filters: 'home',
89 | extras: ['activities', 'tags'],
90 | })
91 | dispatch(homeUpdate({
92 | shoplist: [...shoplist, ...list.data.items],
93 | rank_id: list.data.meta.rank_id,
94 | }))
95 | callback && callback() // eslint-disable-line
96 | } catch ({ err }) {
97 | Toast.info(err, 3, false)
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/pages/order/index.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import React from 'react'
4 | import { connect } from 'react-redux'
5 | import { bindActionCreators } from 'redux'
6 | import { fetchOrderList } from 'stores/order'
7 | import Scroll from 'components/scroll'
8 | import NavBar from '../common-components/nav-bar'
9 | import withTabBar from '../common-components/tab-bar'
10 | import OrderRow from '../common-components/order-list-row'
11 | import AuthErr from '../common-components/auth-err'
12 | import RowSk from '../common-components/skeleton/row'
13 | import styles from './index.less'
14 |
15 | const mapStateToProps = ({ globalState, order }) => ({
16 | isLogin: globalState.isLogin,
17 | init: order.init,
18 | orderList: order.orderList,
19 | })
20 |
21 | const mapDispatchToProps = dispatch => bindActionCreators({
22 | fetchOrderList,
23 | }, dispatch)
24 |
25 | @connect(mapStateToProps, mapDispatchToProps)
26 | @withTabBar
27 | export default class Order extends React.PureComponent {
28 | componentDidMount() {
29 | const { isLogin, init } = this.props
30 | if (isLogin && !init) {
31 | this.props.fetchOrderList(true, false)
32 | }
33 | }
34 |
35 | componentWillReceiveProps(nextProps) {
36 | if (nextProps.isLogin && !nextProps.init) {
37 | this.props.fetchOrderList(true, false)
38 | }
39 | }
40 |
41 | handlePullingDown = () => {
42 | this.props.fetchOrderList(true, () => {
43 | this.scroll && this.scroll.forceUpdate() // eslint-disable-line
44 | })
45 | }
46 |
47 | handlePullingUp = () => {
48 | this.props.fetchOrderList(false, () => {
49 | this.scroll && this.scroll.forceUpdate() // eslint-disable-line
50 | })
51 | }
52 |
53 | rowClick = (id) => {
54 | this.props.history.push({
55 | pathname: '/order-detail',
56 | state: { id },
57 | })
58 | }
59 |
60 | render() {
61 | const {
62 | orderList,
63 | isLogin,
64 | init,
65 | } = this.props
66 | const scrollProps = {
67 | className: styles.scroll,
68 | dataSource: orderList,
69 | pullDownRefresh: { stop: 40 },
70 | pullUpLoad: true,
71 | pullingDown: this.handlePullingDown,
72 | pullingUp: this.handlePullingUp,
73 | }
74 |
75 | return (
76 |
77 |
this.props.history.goBack()} />
82 | {
83 | isLogin ? (
84 | this.scroll = c}>
85 | {
86 | init ? orderList.map(v => (
87 | this.rowClick(v.unique_id)} />
88 | )) : Array.from({ length: 10 }, (v, i) => i).map(v => (
89 |
90 | ))
91 | }
92 |
93 | ) :
94 | }
95 |
96 | )
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/components/toast/notification.jsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react'
3 | import ReactDOM from 'react-dom'
4 | import Notice from './notices'
5 | import styles from './index.less'
6 |
7 | const prefixCls = 'notification'
8 | let noticeNumber = 0
9 | const getUuid = () => `notification-${+new Date()}-${noticeNumber++}`
10 |
11 | export default class Notification extends React.Component {
12 | constructor(props) {
13 | super(props)
14 | /**
15 | * notices 存放notices数组
16 | * hasMask 是否显示遮罩
17 | */
18 | this.state = {
19 | notices: [],
20 | hasMask: true,
21 | }
22 | }
23 |
24 | // 创建noticeDom
25 | getNoticeDOM = () => {
26 | const { notices } = this.state
27 | return notices.map((notice) => {
28 | return
29 | })
30 | }
31 |
32 | // 创建mask遮罩
33 | getMaskDOM = () => {
34 | const { notices, hasMask } = this.state
35 | // notices 不为空 && 始终只显示一个 mask
36 | if (notices.length && hasMask) {
37 | return
38 | }
39 | }
40 |
41 | // 添加 notice 方法
42 | add = (notice) => {
43 | const key = notice.key || getUuid()
44 | const mask = notice.mask || false
45 | // 排除重复因素后再添加
46 | this.setState((preState) => {
47 | const { notices } = preState
48 | if (!notices.find(v => v.key === key)) {
49 | return {
50 | notices: [...notices, { ...notice, key }],
51 | hasMask: mask,
52 | }
53 | }
54 | })
55 | }
56 |
57 | // 移除notice
58 | remove = (key) => {
59 | this.setState((preState) => {
60 | return {
61 | notices: preState.notices.filter(v => v.key === key),
62 | }
63 | })
64 | }
65 |
66 | render() {
67 | const noticesDOM = this.getNoticeDOM()
68 | const maskDOM = this.getMaskDOM()
69 | return (
70 |
71 | {maskDOM}
72 |
73 | {noticesDOM}
74 |
75 |
76 | )
77 | }
78 | }
79 |
80 | /**
81 | * Notification静态类方法 用于创建 Notification组件 以及返回他的方法和组件本身
82 | * properties 需要传递给 Notification的props
83 | * callback 用于接收 Notification各种方法的回调
84 | */
85 | Notification.newInstance = (properties, callback) => {
86 | const { ...props } = properties || {}
87 | const div = document.createElement('div')
88 | document.body.appendChild(div)
89 |
90 | let called = false
91 | function ref(notification) {
92 | if (called) return
93 | called = true
94 | callback({
95 | notice(noticeProps) {
96 | notification.add(noticeProps)
97 | },
98 | removeNotice(key) {
99 | notification.remove(key)
100 | },
101 | destroy() {
102 | ReactDOM.unmountComponentAtNode(div)
103 | div && div.parentNode.removeChild(div) // eslint-disable-line
104 | },
105 | })
106 | }
107 | ReactDOM.render(, div)
108 | }
109 |
--------------------------------------------------------------------------------
/src/pages/shop-detail/cartcontrol/stepper.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import React from 'react'
4 | import QueueAnim from 'rc-queue-anim'
5 | import cls from 'classnames'
6 | import { connect } from 'react-redux'
7 | import { bindActionCreators } from 'redux'
8 | import { shoppingCartUpdate } from 'stores/shopping-cart'
9 | import eventProxy from 'utils/event-proxy'
10 | import styles from './index.less'
11 |
12 | @connect(({ shoppingCart }) => ({
13 | cart: shoppingCart.cart,
14 | }), dispatch => bindActionCreators({
15 | shoppingCartUpdate,
16 | }, dispatch))
17 | export default class Stepper extends React.PureComponent {
18 | increment = ({ target }) => {
19 | const { food, cart, dropBall = true } = this.props
20 | const specs = food.specfoods ? food.specfoods[0] : null
21 | const isHas = cart.find(v => v.virtual_food_id === food.virtual_food_id)
22 | if (!isHas && specs) {
23 | this.props.shoppingCartUpdate({
24 | cart: [
25 | ...cart,
26 | {
27 | attrs: [],
28 | quantity: 1,
29 | restaurant_id: food.restaurant_id,
30 | price: specs.price,
31 | new_specs: specs.specs,
32 | name: specs.name,
33 | food_id: specs.food_id,
34 | virtual_food_id: food.virtual_food_id,
35 | },
36 | ],
37 | })
38 | } else {
39 | const result = cart.map((v) => {
40 | if (v.virtual_food_id === food.virtual_food_id) {
41 | return { ...v, quantity: v.quantity + 1 }
42 | }
43 | return v
44 | })
45 | this.props.shoppingCartUpdate({ cart: result })
46 | }
47 |
48 | dropBall && eventProxy.trigger('cartBall', target) // eslint-disable-line
49 | }
50 |
51 | decrement = () => {
52 | const { food, cart } = this.props
53 | const result = cart.map((v) => {
54 | if (v.virtual_food_id === food.virtual_food_id) {
55 | return { ...v, quantity: v.quantity - 1 }
56 | }
57 | return v
58 | }).filter(v => v.quantity > 0)
59 | this.props.shoppingCartUpdate({ cart: result })
60 | }
61 |
62 | render() {
63 | const { food, cart } = this.props
64 | const cartFood = cart.find(v => v.virtual_food_id === food.virtual_food_id)
65 | const count = cartFood ? cartFood.quantity : 0
66 |
67 | return (
68 |
69 |
72 | {
73 | count > 0 ? (
74 |
80 | ) : null
81 | }
82 |
83 | {
84 | count > 0 ? (
85 |
{count}
86 | ) : null
87 | }
88 |
93 |
94 | )
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/pages/compass/index.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import React from 'react'
4 | import { connect } from 'react-redux'
5 | import { bindActionCreators } from 'redux'
6 | import Scroll from 'components/scroll'
7 | import NavBar from '../common-components/nav-bar'
8 | import withTabBar from '../common-components/tab-bar'
9 | import AuthErr from '../common-components/auth-err'
10 | import RecommedFoodRow from '../common-components/recommend-food-row'
11 | import NoData from '../common-components/no-data'
12 | import RecommedSk from '../common-components/skeleton/recommend'
13 | import { fetchFoodList } from '../../stores/compass'
14 | import styles from './index.less'
15 |
16 | const mapStateToProps = ({ globalState, compass, home }) => ({
17 | isLogin: globalState.isLogin,
18 | init: compass.init,
19 | foodList: compass.foodList,
20 | locationInfo: home.locationInfo,
21 | })
22 |
23 | const mapDispatchToProps = dispatch => bindActionCreators({
24 | fetchFoodList,
25 | }, dispatch)
26 |
27 | @connect(mapStateToProps, mapDispatchToProps)
28 | @withTabBar
29 | export default class Compass extends React.Component {
30 | componentDidMount() {
31 | const { isLogin, init } = this.props
32 | if (isLogin && !init) {
33 | this.props.fetchFoodList()
34 | }
35 | }
36 |
37 | componentWillReceiveProps(nextProps) {
38 | if (nextProps.isLogin && !nextProps.init) {
39 | this.props.fetchFoodList()
40 | }
41 | }
42 |
43 | handleRowClick = (val) => {
44 | const { history, locationInfo } = this.props
45 | const { id } = val
46 | const { latitude, longitude } = locationInfo
47 | history.push({
48 | pathname: '/shop-detail',
49 | search: `?restaurant_id=${id}&latitude=${latitude}&longitude=${longitude}`,
50 | })
51 | }
52 |
53 | render() {
54 | const {
55 | isLogin,
56 | foodList,
57 | init,
58 | history,
59 | } = this.props
60 |
61 | const scrollProps = {
62 | className: styles.scroll,
63 | dataSource: foodList,
64 | pullUpLoad: true,
65 | pullingUp: this.props.fetchFoodList,
66 | }
67 |
68 | return (
69 |
70 |
history.goBack()} />
74 | {
75 | isLogin && init ? (
76 |
77 | {
78 | foodList.length ? foodList.map((v, i) => (
79 | this.handleRowClick(v.restaurant)} />
83 | )) :
84 | }
85 |
86 | ) : isLogin && !init ? (
87 |
88 | {
89 | Array.from({ length: 20 }, (v, i) => i).map(v => (
90 |
91 | ))
92 | }
93 |
94 | ) :
95 | }
96 |
97 | )
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/pages/shop-detail/shop/index.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import React from 'react'
4 | import { connect } from 'react-redux'
5 | import { getImageUrl } from 'utils/utils'
6 | import Scroll from 'components/scroll'
7 | import Badge from '../../common-components/badge'
8 | import styles from './index.less'
9 |
10 | @connect(({ shop }) => ({
11 | info: shop.info,
12 | }))
13 | export default class ShopInfo extends React.PureComponent {
14 | render() {
15 | const { show, info } = this.props
16 | const flavors = info.flavors.length ? info.flavors.map(v => v.name).join(',') : '--'
17 | const opening_hours = info.opening_hours.length ? info.opening_hours.join(',') : '--'
18 |
19 | return !show ? null : (
20 |
21 |
22 |
23 |
配送信息
24 |
{`由蜂鸟快送提供配送,约${info.order_lead_time}分钟送达,距离${info.distance}m`}
25 |
{info.piecewise_agent_fee ? info.piecewise_agent_fee.description : ''}
26 |
27 |
28 |
29 |
活动与服务
30 | {
31 | info.activities ? info.activities.map(v => (
32 |
33 |
37 | {v.tips}
38 |
39 | )) :
暂无活动
40 | }
41 |
42 |
43 |
44 |
商家实景
45 |
46 | {
47 | info.albums ? info.albums.map((img, i) => (
48 |
49 |
})
50 |
51 | )) :
暂无实景
52 | }
53 |
54 |
55 |
56 |
57 |
商家信息
58 |
{info.description}
59 |
60 |
品类
61 |
{flavors}
62 |
63 |
64 |
商家电话
65 |
{info.phone}
66 |
67 |
68 |
地址
69 |
{info.address}
70 |
71 |
72 |
营业时间
73 |
{opening_hours}
74 |
75 |
76 |
77 |
78 | )
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/pages/common-components/order-list-row/index.less:
--------------------------------------------------------------------------------
1 |
2 | @import '../../../assets/css/mixin.less';
3 | @import '../../../assets/css/theme.less';
4 |
5 | .order-row {
6 | background-color: @fill-body;
7 | margin-bottom: 20px;
8 | box-sizing: border-box;
9 | padding: 28px 0 0 30px;
10 |
11 | .line {
12 | width: 100%;
13 | height: 1px;
14 | margin-top: 20px;
15 | }
16 |
17 | .order-body {
18 | display: flex;
19 | .shop-img {
20 | flex: 0 0 64px;
21 | width: 64px;
22 | margin-right: 24px;
23 | img {
24 | width: 100%;
25 | }
26 | }
27 | .info-wrapper {
28 | flex: 1;
29 | .shop-info {
30 | display: flex;
31 | height: 48px;
32 | box-sizing: border-box;
33 | padding-right: 30px;
34 | align-items: center;
35 | .shop {
36 | flex: 1;
37 | font-size: 0;
38 | .name {
39 | display: inline-block;
40 | vertical-align: middle;
41 | max-width: 300px;
42 | .no-wrap;
43 | font-size: @font-size-medium-x;
44 | color: @color-title;
45 | }
46 | .icon {
47 | display: inline-block;
48 | vertical-align: middle;
49 | color: @color-base;
50 | font-size: @font-size-small-s;
51 | }
52 | }
53 | .status {
54 | flex: 0 0 260px;
55 | width: 260px;
56 | .no-wrap;
57 | text-align: right;
58 | font-size: @font-size-small;
59 | font-weight: @font-weight-small;
60 | color: @color-title;
61 | }
62 | }
63 | .time {
64 | margin: 8px 0 0;
65 | font-size: @font-size-small;
66 | font-weight: @font-weight-small;
67 | color: @color-base;
68 | transform: scale(.8);
69 | transform-origin: 0;
70 | }
71 |
72 | .order-detail {
73 | display: flex;
74 | height: 80px;
75 | line-height: 80px;
76 | padding-right: 30px;
77 | box-sizing: border-box;
78 | justify-content: center;
79 | align-items: space-between;
80 | .desc {
81 | flex: 1;
82 | width: 0;
83 | .no-wrap;
84 | font-size: @font-size-small;
85 | font-weight: @font-weight-small;
86 | color: @color-base;
87 | margin-right: 40px;
88 | }
89 | .price {
90 | font-size: @font-size-small;
91 | color: #000;
92 | }
93 | }
94 | }
95 | }
96 |
97 | .order-footer {
98 | display: flex;
99 | height: 100px;
100 | padding-right: 30px;
101 | box-sizing: border-box;
102 | align-items: center;
103 | justify-content: flex-end;
104 | .more {
105 | padding: 0;
106 | display: block;
107 | background-color: #fff;
108 | border-radius: 4px;
109 | outline: none;
110 | border: none;
111 | width: 146px;
112 | height: 60px;
113 | line-height: 60px;
114 | font-size: @font-size-small;
115 | font-weight: @font-weight-small;
116 | border: 1px solid @primary-color-light;
117 | color: @primary-color;
118 | }
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/pages/search-shop/index.jsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react'
3 | import { connect } from 'react-redux'
4 | import { bindActionCreators } from 'redux'
5 | import { searchShopDestroy, getHotKeys, searchShopUpdate, getShopList } from 'stores/search-shop'
6 | import SvgIcon from 'components/icon-svg'
7 | import NavBar from '../common-components/nav-bar'
8 | import ShopList from './list'
9 | import styles from './index.less'
10 |
11 | const mapStateToProps = ({ searchShop, home }) => ({
12 | keywords: searchShop.keywords,
13 | hotKeys: searchShop.hotKeys,
14 | locationInfo: home.locationInfo,
15 | })
16 | const mapDispatchToProps = dispatch => bindActionCreators({
17 | searchShopDestroy,
18 | getHotKeys,
19 | searchShopUpdate,
20 | getShopList,
21 | }, dispatch)
22 |
23 | @connect(mapStateToProps, mapDispatchToProps)
24 | export default class SearchShop extends React.Component {
25 | componentDidMount() {
26 | this.props.getHotKeys()
27 | }
28 |
29 | componentWillUnmount() {
30 | this.props.searchShopDestroy()
31 | if (this.timer) {
32 | clearTimeout(this.timer)
33 | this.timer = null
34 | }
35 | }
36 |
37 | searhChange = ({ target }) => {
38 | const { locationInfo } = this.props
39 | this.props.searchShopUpdate({
40 | keywords: target.value,
41 | })
42 | if (!locationInfo.latitude && !locationInfo.longitude) {
43 | return
44 | }
45 | if (this.timer) clearTimeout(this.timer)
46 | this.timer = setTimeout(this.props.getShopList, 600)
47 | }
48 |
49 | badgeClick = (val) => {
50 | const { locationInfo } = this.props
51 | this.props.searchShopUpdate({
52 | keywords: val,
53 | })
54 | if (!locationInfo.latitude && !locationInfo.longitude) {
55 | return
56 | }
57 | this.props.getShopList()
58 | }
59 |
60 | searchClick = () => {
61 | const { locationInfo } = this.props
62 | if (!locationInfo.latitude && !locationInfo.longitude) {
63 | return
64 | }
65 | this.props.getShopList()
66 | }
67 |
68 | render() {
69 | const { keywords, hotKeys } = this.props
70 |
71 | return (
72 |
73 |
this.props.history.goBack()} />
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | {
87 | !keywords ? (
88 |
89 |
热门搜索
90 | {
91 | hotKeys.map((v, i) => (
92 |
this.badgeClick(v.word)}>
96 | {v.word}
97 |
98 | ))
99 | }
100 |
101 | ) :
102 | }
103 |
104 |
105 | )
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/pages/order-detail/index.less:
--------------------------------------------------------------------------------
1 |
2 |
3 | @import '../../assets/css/theme.less';
4 | @import '../../assets/css/mixin.less';
5 |
6 | .detail {
7 | position: fixed;
8 | z-index: 1;
9 | .position-full;
10 | background-color: @fill-body-darken;
11 | .scroll {
12 | top: @bar-height;
13 | background-color: @fill-body-darken;
14 | margin: 30px;
15 | overflow: visible;
16 | z-index: -1;
17 | .content {
18 | box-sizing: border-box;
19 | padding: 0 20px;
20 | box-shadow: 0 3px 5px rgba(0,0,0,.05);
21 | background-color: @fill-body;
22 | .item {
23 | display: flex;
24 | min-height: 88px;
25 | align-items: center;
26 | box-shadow: 0 1px 1px -1px @border-color;
27 | .img {
28 | flex: 0 0 40px;
29 | height: 40px;
30 | border-radius: 40px;
31 | background-color: @fill-body-darken;
32 | margin-right: 20px;
33 | img {
34 | width: 100%;
35 | border-radius: 100%;
36 | }
37 | }
38 | .text {
39 | flex: 1;
40 | width: 0;
41 | .no-wrap;
42 | font-size: @font-size-medium;
43 | font-weight: @font-weight-small;
44 | line-height: 40px;
45 | color: @color-title;
46 | }
47 | .num,.price {
48 | text-align: right;
49 | font-size: @font-size-small;
50 | font-weight: @font-weight-small;
51 | line-height: 40px;
52 | color: @color-title;
53 | &.red {
54 | color: #ff5339;
55 | }
56 | }
57 | .num {
58 | flex: 0 0 80px;
59 | width: 60px;
60 | }
61 | .price {
62 | flex: 0 0 130px;
63 | width: 100px;
64 | }
65 | }
66 | .total {
67 | text-align: right;
68 | font-size: @font-size-large;
69 | color: @color-base;
70 | line-height: 100px;
71 | }
72 | }
73 | .info {
74 | margin-top: 30px;
75 | box-shadow: 0 3px 5px rgba(0,0,0,.05);
76 | background-color: @fill-body;
77 | .title {
78 | box-sizing: border-box;
79 | padding: 0 30px;
80 | box-shadow: 0 1px 1px -1px @border-color;
81 | line-height: 88px;
82 | font-size: @font-size-medium;
83 | font-weight: @font-weight-small;
84 | color: @color-title;
85 | }
86 | .desc {
87 | box-sizing: border-box;
88 | padding-left: 30px;
89 | .item {
90 | display: flex;
91 | min-height: 68px;
92 | padding: 10px 0;
93 | align-items: center;
94 | box-shadow: 0 1px 1px -1px @border-color;
95 | .label {
96 | flex: 0 0 130px;
97 | width: 130px;
98 | margin-right: 20px;
99 | font-size: @font-size-small;
100 | font-weight: @font-weight-small;
101 | line-height: 40px;
102 | color: @color-title;
103 | }
104 | .text {
105 | flex: 1;
106 | width: 0;
107 | font-size: @font-size-small;
108 | font-weight: @font-weight-small;
109 | line-height: 40px;
110 | color: @color-base;
111 | .no-wrap;
112 | }
113 | }
114 | }
115 | }
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/api/index.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | import HttpUtils from './http'
4 |
5 | const position = new AMap.Geolocation({
6 | enableHighAccuracy: true,
7 | maximumAge: 0,
8 | convert: true,
9 | })
10 |
11 | export const getGeolocation = () => {
12 | return new Promise((resolve, reject) => {
13 | position.getCurrentPosition((status, result) => {
14 | if (status === 'complete') {
15 | resolve({
16 | data: {
17 | latitude: result.position.lat,
18 | longitude: result.position.lng,
19 | address: result.formattedAddress,
20 | },
21 | })
22 | } else {
23 | reject({
24 | err: result.message,
25 | })
26 | }
27 | })
28 | })
29 | }
30 | export const getEntry = (params) => { return HttpUtils.get('/elm/entry', params) }
31 | export const getBanner = (params) => { return HttpUtils.get('/elm/banner', params) }
32 | export const getShopList = (params) => { return HttpUtils.get('/elm/restaurants', params) }
33 | // 通过关键字搜索商家
34 | export const getShopListByKw = (params) => { return HttpUtils.get('/elm/restaurants_search', params) }
35 |
36 | export const getOrderList = (params) => { return HttpUtils.get('/elm/orders', params) }
37 | export const getOrderSnapshot = (params) => { return HttpUtils.get('/elm/order-snapshot', params) }
38 | export const getOrderDesc = (params) => { return HttpUtils.get('/elm/order-desc', params) }
39 |
40 | // 商店
41 | export const getShopInfo = (params) => { return HttpUtils.get('/elm/restaurant_byid', params) }
42 | export const getShopRatings = (params) => { return HttpUtils.get('/elm/restaurant_ratings', params) }
43 | export const getShopFood = (params) => { return HttpUtils.get('/elm/restaurant_menu', params) }
44 | export const getRatingTags = (params) => { return HttpUtils.get('/elm/rating_tags', params) }
45 | export const getRatingScores = (params) => { return HttpUtils.get('/elm/rating_scores', params) }
46 |
47 | export const getTotalCategory = (params) => { return HttpUtils.get('/elm/total_category', params) }
48 | export const getFoodSiftFactors = (params) => { return HttpUtils.get('/elm/food_sift_factors', params) }
49 | export const getFilterAttr = (params) => { return HttpUtils.get('/elm/filter_attributes', params) }
50 |
51 | // 热门关键词
52 | export const getHotKeywords = (params) => { return HttpUtils.get('/elm/hot_keywords', params) }
53 | // 推荐食物
54 | export const getRecommendation = (params) => { return HttpUtils.get('/elm/recommendation', params) }
55 |
56 | // 登陆 用户信息
57 | export const mobileSendCode = (params) => { return HttpUtils.post('/elm/mobile_send_code', params) }
58 | export const mobileCaptchas = (params) => { return HttpUtils.post('/elm/captchas', params) }
59 | export const loginByMobile = (params) => { return HttpUtils.post('/elm/login_by_mobile', params) }
60 | export const getUserInfo = (params) => { return HttpUtils.get('/elm/users', params) }
61 |
62 | export const getAddress = (params) => { return HttpUtils.get('/elm/address', params) }
63 | export const delAddress = (params) => { return HttpUtils.get('/elm/del_address', params) }
64 | export const upAddress = (params) => { return HttpUtils.post('/elm/update_address', params) }
65 | export const addAddress = (params) => { return HttpUtils.post('/elm/add_address', params) }
66 |
67 | export const getHongbaos = (params) => { return HttpUtils.get('/elm/hongbaos', params) }
68 |
69 | // 根据经纬度 关键词 获取地址
70 | export const getNearby = (params) => { return HttpUtils.get('/elm/search_nearby', params) }
71 |
--------------------------------------------------------------------------------
/src/assets/svg/new.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/drumstick.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/common-components/red-ticket-row/index.less:
--------------------------------------------------------------------------------
1 |
2 |
3 | @import '../../../assets/css/mixin.less';
4 | @import '../../../assets/css/theme.less';
5 |
6 | .row {
7 | width: 100%;
8 | background-color: @fill-body;
9 | margin-bottom: 20px;
10 | border-radius: 4px;
11 | overflow: hidden;
12 | box-shadow: 0px 6px 5px -5px rgba(0,0,0,.1);
13 | .body {
14 | width: 100%;
15 | height: 168px;
16 | background-color: @fill-body;
17 | display: flex;
18 | position: relative;
19 | &::after, &::before{
20 | position: absolute;
21 | content: '' !important;
22 | display: block;
23 | border: 12px solid @fill-body-darken;
24 | border-radius: 100%;
25 | z-index: 2;
26 | }
27 | &::after {
28 | left: -12px;
29 | bottom: -12px;
30 | }
31 | &::before {
32 | right: -12px;
33 | bottom: -12px;
34 | }
35 | .icon {
36 | position: absolute;
37 | font-size: @font-size-large-x;
38 | top: 0;
39 | left: 0;
40 | color: @badge-color-open;
41 | }
42 | .left {
43 | flex: 0 0 190px;
44 | width: 190px;
45 | display: flex;
46 | flex-direction: column;
47 | justify-content: center;
48 | .amount {
49 | text-align: center;
50 | color: #ff0025;
51 | font-size: @font-size-large-x;
52 | span {
53 | font-weight: 700;
54 | }
55 | .unit {
56 | font-size: @font-size-small;
57 | margin-right: 10px;
58 | }
59 | }
60 | .desc {
61 | font-size: @font-size-small;
62 | font-weight: @font-weight-small;
63 | color: @color-base;
64 | text-align: center;
65 | transform: scale(.9);
66 | transform-origin: center center;
67 | margin-top: 10px;
68 | }
69 | }
70 | .center {
71 | flex: 1;
72 | width: 0;
73 | display: flex;
74 | flex-direction: column;
75 | justify-content: center;
76 | margin: 0 10px;
77 | .title {
78 | font-size: @font-size-medium;
79 | color: @color-title;
80 | margin-bottom: 10px;
81 | }
82 | .desc {
83 | font-size: @font-size-small;
84 | font-weight: @font-weight-small;
85 | color: @color-base;
86 | transform: scale(.9);
87 | margin-bottom: 6px;
88 | transform-origin: 0;
89 | }
90 | }
91 | .right {
92 | flex: 0 0 150px;
93 | width: 150px;
94 | display: flex;
95 | flex-direction: column;
96 | justify-content: center;
97 | .title {
98 | text-align: center;
99 | font-size: @font-size-small;
100 | color: #ff0025;
101 | margin-bottom: 10px;
102 | }
103 | .btn {
104 | outline: none;
105 | margin: 0 auto;
106 | border: none;
107 | text-align: center;
108 | width: 110px;
109 | line-height: 46px;
110 | color: #fff;
111 | background-color: #ff0025;
112 | border-radius: 46px;
113 | }
114 | }
115 | }
116 | .bottom {
117 | width: 100%;
118 | height: 74px;
119 | background-color: #fcfcfc;
120 | box-sizing: border-box;
121 | border-top: 1px dotted @border-color;
122 | padding: 0 30px;
123 | .desc {
124 | width: 100%;
125 | height: 100%;
126 | position: relative;
127 | span {
128 | position: absolute;
129 | left: 0;
130 | right: -120px;
131 | top: 50%;
132 | display: block;
133 | font-size: @font-size-small;
134 | font-weight: @font-weight-small;
135 | transform: scale(.8) translateY(-50%);
136 | transform-origin: 0 0;
137 | color: @color-base;
138 | .no-wrap;
139 | }
140 | }
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/src/pages/login/index.less:
--------------------------------------------------------------------------------
1 |
2 |
3 | @import '../../assets/css/theme.less';
4 | @import '../../assets/css/mixin.less';
5 |
6 | .login {
7 | position: fixed;
8 | .position-full;
9 | background-color: @fill-body;
10 | .logo {
11 | font-size: 260px;
12 | text-align: center;
13 | }
14 | .form {
15 | width: 600px;
16 | margin: 0 auto;
17 | .item {
18 | width: 100%;
19 | height: @input-height;
20 | border: 1px solid #ddd;
21 | border-radius: 8px;
22 | margin-bottom: 40px;
23 | overflow: hidden;
24 | position: relative;
25 | .placeholder-color(#ababab);
26 | .code-btn {
27 | height: @input-height;
28 | line-height: @input-height;
29 | font-size: @font-size-medium;
30 | font-weight: @font-weight-small;
31 | color: #cfcfcf;
32 | background-color: transparent;
33 | outline: none;
34 | position: absolute;
35 | border: none;
36 | right: 0;
37 | top: 0;
38 | }
39 | input {
40 | width: 100%;
41 | height: 100%;
42 | padding-left: 20px;
43 | font-size: @font-size-medium;
44 | font-weight: @font-weight-small;
45 | outline: none;
46 | color: #333;
47 | }
48 | }
49 | }
50 | .desc {
51 | font-size: @font-size-small;
52 | color: #ababab;
53 | width: 600px;
54 | margin: 0 auto;
55 | line-height: 1.6em;
56 | span {
57 | color: @primary-color-light;
58 | }
59 | }
60 | .login-btn {
61 | margin: 0 auto;
62 | display: block;
63 | background-color: #4cd96f;
64 | border-radius: 8px;
65 | outline: none;
66 | border: none;
67 | width: 600px;
68 | height: 84px;
69 | line-height: 84px;
70 | margin-top: 40px;
71 | font-size: @font-size-medium-x;
72 | font-weight: @font-weight-small;
73 | color:#fff;
74 | }
75 | }
76 |
77 |
78 | .captcha-modal {
79 | position: fixed;
80 | .position-full;
81 | z-index: 1;
82 | .mask {
83 | position: absolute;
84 | .position-full;
85 | z-index: 1;
86 | background-color: rgba(0,0,0,0.5);
87 | }
88 | .body {
89 | position: absolute;
90 | top: 50%;
91 | left: 50%;
92 | transform: translate3d(-50%, -50%, 0);
93 | z-index: 2;
94 | background-color: @fill-body;
95 | border-radius: 6px;
96 | padding: 30px 20px;
97 | width: 500px;
98 | .title {
99 | font-size: @font-size-medium-x;
100 | color: @color-title;
101 | line-height: 40px;
102 | text-align: center;
103 | margin-bottom: 30px;
104 | }
105 | .item {
106 | width: 100%;
107 | height: @input-height - 20;
108 | border: 1px solid #ddd;
109 | border-radius: 8px;
110 | overflow: hidden;
111 | margin-bottom: 30px;
112 | position: relative;
113 | .placeholder-color(#ababab);
114 | input {
115 | width: 100%;
116 | height: 100%;
117 | padding-left: 20px;
118 | box-sizing: border-box;
119 | font-size: @font-size-medium;
120 | font-weight: @font-weight-small;
121 | outline: none;
122 | color: #333;
123 | }
124 | .img {
125 | position: absolute;
126 | content: none !important;
127 | right: 6px;
128 | top: 10px;
129 | width: 160px;
130 | height: @input-height - 40;
131 | }
132 | }
133 | }
134 | .footer {
135 | border-top: 1px solid @border-color;
136 | font-size: 0;
137 | .reset, .submit {
138 | display: inline-block;
139 | width: 50%;
140 | height: 80px;
141 | line-height: 80px;
142 | text-align: center;
143 | background-color: @fill-body-darken;
144 | font-size: @font-size-medium;
145 | font-weight: @font-weight-small;
146 | color: @color-title;
147 | }
148 | .submit {
149 | color: #fff;
150 | background-color: #4cd96f;
151 | }
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/src/assets/svg/shop.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/profile/index.less:
--------------------------------------------------------------------------------
1 |
2 |
3 | @import '../../assets/css/theme.less';
4 | @import '../../assets/css/mixin.less';
5 |
6 | .root {
7 | width: 100%;
8 | height: 100%;
9 | background-color: @fill-body-darken;
10 |
11 | .profile-info {
12 | width: 100%;
13 | height: 200px;
14 | box-sizing: border-box;
15 | padding: 10px 50px 30px;
16 | background-image: linear-gradient(-90deg, @primary-color, @primary-color-light);
17 | display: flex;
18 | position: relative;
19 | .flex-center;
20 | .icon-right {
21 | position: absolute;
22 | right: 20px;
23 | top: 50%;
24 | transform: translateY(-50%);
25 | font-size: @font-size-medium;
26 | color: #fff;
27 | }
28 | .avatar {
29 | flex: 0 0 120px;
30 | width: 120px;
31 | height: 120px;
32 | border-radius: 100%;
33 | background-color: @fill-body;
34 | overflow: hidden;
35 | position: relative;
36 | color: #dcdcdc;
37 | font-size: 120px;
38 | .icon {
39 | position: absolute;
40 | left: 50%;
41 | transform: translateX(-50%);
42 | bottom: -8px;
43 | }
44 | .img {
45 | width: 120px;
46 | height: 120px;
47 | content: none !important;
48 | }
49 | }
50 | .desc {
51 | flex: 1;
52 | margin-left: 36px;
53 | .info {
54 | font-size: @font-size-large-x;
55 | color: #fff;
56 | font-weight: @font-weight-medium;
57 | margin-bottom: 16px;
58 | }
59 | .text {
60 | font-size: 0;
61 | .icon, span {
62 | display: inline-block;
63 | vertical-align: bottom;
64 | color: #fff;
65 | font-size: @font-size-small;
66 | font-weight: @font-weight-small;
67 | }
68 | .icon {
69 | font-size: @font-size-medium;
70 | }
71 | }
72 | }
73 | }
74 |
75 | .column {
76 | width: 100%;
77 | height: 160px;
78 | background-color: @fill-body;
79 | border-bottom: 1px solid @border-color;
80 | margin-bottom: 20px;
81 | display: flex;
82 | .item {
83 | flex: 1;
84 | border-right: 1px solid @border-color;
85 | &:last-child {
86 | border-right: none;
87 | }
88 | display: flex;
89 | .flex-center;
90 | flex-direction: column;
91 | .icon {
92 | display: flex;
93 | .flex-center;
94 | width: 68px;
95 | height: 68px;
96 | background-color: rgba(0, 0, 0, .1);
97 | font-size: @font-size-large-x;
98 | border-radius: 100%;
99 | margin-bottom: 10px;
100 | }
101 | .count {
102 | font-size: @font-size-large-x;
103 | margin-bottom: 10px;
104 | &.blue {
105 | color: rgb(0, 152, 251);
106 | }
107 | &.red {
108 | color: rgb(255, 95, 62);
109 | }
110 | &.green {
111 | color: rgb(106, 194, 11);
112 | }
113 | .unit {
114 | margin-left: 6px;
115 | font-size: @font-size-medium;
116 | }
117 | }
118 | .desc {
119 | font-size: @font-size-small;
120 | color: @color-base;
121 | }
122 | }
123 | }
124 |
125 | .list {
126 | background-color: @fill-body;
127 | border-top: 1px solid @border-color;
128 | &:last-child {
129 | border-bottom: 1px solid @border-color;
130 | }
131 | .item {
132 | background-color: @fill-body;
133 | display: flex;
134 | .flex-center;
135 | height: @item-height;
136 | position: relative;
137 | .icon {
138 | flex: 0 0 @item-height;
139 | height: @item-height;
140 | display: flex;
141 | .flex-center;
142 | font-size: @font-size-large-x;
143 | }
144 | .desc {
145 | flex: 1;
146 | height: @item-height;
147 | line-height: @item-height;
148 | font-size: @font-size-medium;
149 | }
150 | .icon-right {
151 | position: absolute;
152 | right: 20px;
153 | top: 50%;
154 | transform: translateY(-50%);
155 | font-size: @font-size-medium;
156 | color: @color-base;
157 | }
158 | }
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/src/assets/svg/gold.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/profile/index.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import React from 'react'
4 | import { connect } from 'react-redux'
5 | import cls from 'classnames'
6 | import SvgIcon from 'components/icon-svg'
7 | import { formatPhone, getImageUrl } from 'utils/utils'
8 | import NavBar from '../common-components/nav-bar'
9 | import withTabBar from '../common-components/tab-bar'
10 | import styles from './index.less'
11 |
12 | @connect(({ globalState }) => ({
13 | isLogin: globalState.isLogin,
14 | userInfo: globalState.userInfo,
15 | }))
16 | @withTabBar
17 | export default class Profile extends React.Component {
18 | render() {
19 | const { history, userInfo, isLogin } = this.props
20 | const {
21 | username,
22 | mobile,
23 | avatar,
24 | balance,
25 | brand_member_new,
26 | gift_amount,
27 | } = userInfo
28 | const avatarUrl = getImageUrl(avatar)
29 | const changePage = path => history.push(path)
30 |
31 | const getItemContent = (icon, style, count, unit) => {
32 | if (isLogin) {
33 | return (
34 |
35 | {count}
36 | {unit}
37 |
38 | )
39 | }
40 | return (
41 |
42 |
43 |
44 | )
45 | }
46 |
47 | const goDetail = () => {
48 | console.log('123123')
49 | }
50 |
51 | return (
52 |
53 |
this.props.history.goBack()} />
57 |
58 | changePage('/login') : goDetail}>
59 |
60 | {
61 | avatarUrl ? (
62 |

63 | ) :
64 | }
65 |
66 |
67 |
68 | {
69 | !isLogin ? '登陆/注册' : username
70 | }
71 |
72 |
73 |
74 |
75 | {
76 | !isLogin ? '登陆后享受更多特权' : formatPhone(mobile)
77 | }
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | {
87 | getItemContent('#purse', styles.blue, balance, '元')
88 | }
89 |
钱包
90 |
91 |
changePage('/benefit')}>
92 | {
93 | getItemContent('#red-packet', styles.red, gift_amount, '个')
94 | }
95 |
红包
96 |
97 |
98 | {
99 | getItemContent('#gold', styles.green, brand_member_new, '个')
100 | }
101 |
金币
102 |
103 |
104 |
105 |
106 |
changePage('/order')}>
107 |
108 |
109 |
110 |
我的订单
111 |
112 |
113 |
114 |
115 |
116 |
changePage('/address')}>
117 |
118 |
119 |
120 |
我的地址
121 |
122 |
123 |
124 |
125 |
126 | )
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/components/vertical-slide/index.jsx:
--------------------------------------------------------------------------------
1 |
2 | /*eslint-disable*/
3 | import React from 'react'
4 | import BScroll from 'better-scroll'
5 | import PropTypes from 'prop-types'
6 | import { addClass } from 'utils/dom'
7 | import styles from './index.less'
8 |
9 | const slideProps = {
10 | loop: PropTypes.bool,
11 | autoPlay: PropTypes.bool,
12 | interval: PropTypes.number,
13 | click: PropTypes.bool,
14 | }
15 |
16 | const defaultSlideProps = {
17 | loop: true,
18 | autoPlay: true,
19 | interval: 900,
20 | click: true,
21 | }
22 |
23 | export default class VerticalSlide extends React.Component {
24 | static defaultProps = defaultSlideProps
25 | static propTypes = slideProps
26 |
27 | constructor(props) {
28 | super(props)
29 | this.state = {
30 | currentPageIndex: 0
31 | }
32 | this.children = []
33 | this.timer = null
34 | this.resizeTimer = null
35 | }
36 |
37 | componentDidMount() {
38 | this.update()
39 | window.addEventListener('resize', () => {
40 | if (!this.slide || !this.slide.enabled) return
41 | this.resizeTimer && clearTimeout(this.resizeTimer)
42 | this.resizeTimer = setTimeout(() => {
43 | if (this.slide.isInTransition) {
44 | this.onScrollEnd()
45 | } else {
46 | this.props.autoPlay && this.play()
47 | }
48 | this.refresh()
49 | }, 60)
50 | }, false)
51 | }
52 |
53 | componentWillUnmount() {
54 | this.slide.disable()
55 | this.slide.destroy()
56 | if (this.timer) {
57 | clearTimeout(this.timer)
58 | this.timer = null
59 | }
60 | if (this.resizeTimer) {
61 | clearTimeout(this.resizeTimer)
62 | this.resizeTimer = null
63 | }
64 | }
65 |
66 | update = () => {
67 | if (this.slide) {
68 | this.slide.destroy()
69 | }
70 | this.init()
71 | }
72 |
73 | refresh = () => {
74 | this.setSlideHeight(true)
75 | this.slide.refresh()
76 | }
77 |
78 | next = () => {
79 | this.slide.next()
80 | }
81 |
82 | init = () => {
83 | if (!this.wrapper) return
84 | this.timer && clearTimeout(this.timer)
85 | this.setState({ currentPageIndex: 0 })
86 | // 设置容器高度
87 | this.setSlideHeight()
88 | // 初始化slide
89 | this.initSldie()
90 | }
91 |
92 | setSlideHeight = (isResize = false) => {
93 | this.children = this.slideGroup.childNodes
94 | let height = 0
95 | let slideHeight = this.wrapper.clientHeight
96 | this.children.forEach(child => {
97 | if (child.nodeType === 1) {
98 | // 添加默认样式
99 | addClass(child, styles['slide-item'])
100 | child.style.height = `${slideHeight}px`
101 | height += slideHeight
102 | }
103 | })
104 | if (this.props.loop && !isResize) {
105 | height += 2 * slideHeight
106 | }
107 | this.slideGroup.style.height = `${height}px`
108 | }
109 |
110 | initSldie = () => {
111 | const { loop, autoPlay, click } = this.props
112 | this.slide = new BScroll(this.wrapper, {
113 | click,
114 | scrollX: false,
115 | scrollY: true,
116 | momentum: false,
117 | snap: {
118 | loop,
119 | threshold: 0.3,
120 | speed: 400,
121 | },
122 | bounce: false,
123 | })
124 |
125 | this.slide.on('scrollEnd', this.onScrollEnd)
126 |
127 | this.slide.on('touchEnd', () => {
128 | this.props.autoPlay && this.play()
129 | })
130 |
131 | this.slide.on('beforeScrollStart', () => {
132 | this.props.autoPlay && this.timer && clearTimeout(this.timer)
133 | })
134 |
135 | if (this.props.autoPlay) {
136 | this.play()
137 | }
138 | }
139 |
140 | onScrollEnd = () => {
141 | let pageIndex = this.slide.getCurrentPage().pageY
142 | this.setState({ currentPageIndex: pageIndex })
143 | this.props.autoPlay && this.play()
144 | }
145 |
146 | play = () => {
147 | this.timer && clearTimeout(this.timer)
148 | this.timer = setTimeout(() => {
149 | this.slide.next()
150 | }, this.props.interval)
151 | }
152 |
153 | render() {
154 | const { children } = this.props
155 | return (
156 | this.wrapper =c}>
157 |
this.slideGroup = c}>
158 | {children}
159 |
160 |
161 | )
162 | }
163 | }
164 |
--------------------------------------------------------------------------------