├── .gitignore
├── README.md
├── craco.config.js
├── jsconfig.json
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
└── src
├── App.js
├── assets
├── css
│ ├── common.less
│ ├── index.less
│ └── reset.less
├── data
│ ├── filter_data.json
│ ├── footer.json
│ └── search_titles.json
├── img
│ └── cover_01.jpeg
├── svg
│ ├── icon-arrow-left.jsx
│ ├── icon-arrow-right.jsx
│ ├── icon-close.jsx
│ ├── icon-global.jsx
│ ├── icon-more-arrow.jsx
│ ├── icon-profile-avatar.jsx
│ ├── icon-profile-menu.jsx
│ ├── icon-search-bar.jsx
│ ├── icon-triangle-bottom.jsx
│ ├── icon-triangle-top.jsx
│ ├── icon_logo.jsx
│ └── utils
│ │ └── index.js
└── theme
│ └── index.js
├── base-ui
├── Indicator
│ ├── index.jsx
│ └── style.js
├── picture-browser
│ ├── index.jsx
│ └── style.js
└── scroll-view
│ ├── index.jsx
│ └── style.js
├── components
├── app-footer
│ ├── index.jsx
│ └── style.js
├── app-header
│ ├── c-cpns
│ │ ├── header-center
│ │ │ ├── c-cpns
│ │ │ │ ├── search-sections
│ │ │ │ │ ├── index.jsx
│ │ │ │ │ └── style.js
│ │ │ │ └── search-tabs
│ │ │ │ │ ├── index.jsx
│ │ │ │ │ └── style.js
│ │ │ ├── index.jsx
│ │ │ └── style.js
│ │ ├── header-left
│ │ │ ├── index.jsx
│ │ │ └── style.js
│ │ └── header-right
│ │ │ ├── index.jsx
│ │ │ └── style.js
│ ├── index.jsx
│ └── style.js
├── longfor-item
│ ├── index.jsx
│ └── style.js
├── room-item
│ ├── index.jsx
│ └── style.js
├── section-footer
│ ├── index.jsx
│ └── style.js
├── section-header
│ ├── index.jsx
│ └── style.js
├── section-rooms
│ ├── index.jsx
│ └── style.js
└── section-tabs
│ ├── index.jsx
│ └── style.js
├── hooks
├── index.js
├── useScrollPosition.js
└── useScrollTop.js
├── index.js
├── router
└── index.js
├── services
├── index.js
├── modules
│ ├── entire.js
│ └── home.js
└── request
│ ├── config.js
│ └── index.js
├── store
├── features
│ ├── detail.js
│ ├── entire.js
│ ├── entire
│ │ ├── actionCreators.js
│ │ ├── constants.js
│ │ ├── index.js
│ │ └── reducer.js
│ ├── home.js
│ ├── home
│ │ ├── actionCreators.js
│ │ ├── constants.js
│ │ ├── index.js
│ │ └── reducer.js
│ └── main.js
└── index.js
└── views
├── detail
├── c-cpns
│ └── detail-pictures
│ │ ├── index.jsx
│ │ └── style.js
├── index.jsx
└── style.js
├── entire
├── c-cpns
│ ├── entire-filter
│ │ ├── index.jsx
│ │ └── style.js
│ ├── entire-pagination
│ │ ├── index.jsx
│ │ └── style.js
│ └── entire-rooms
│ │ ├── index.jsx
│ │ └── style.js
├── index.jsx
└── style.js
└── home
├── c-cpns
├── home-banner
│ ├── index.jsx
│ └── style.js
├── home-longfor
│ ├── index.jsx
│ └── style.js
├── home-section-v1
│ ├── index.jsx
│ └── style.js
├── home-section-v2
│ ├── index.jsx
│ └── style.js
└── home-section-v3
│ ├── index.jsx
│ └── style.js
├── index.jsx
└── style.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `npm start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
13 |
14 | The page will reload when you make changes.\
15 | You may also see any lint errors in the console.
16 |
17 | ### `npm test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `npm run build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `npm run eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!**
35 |
36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
39 |
40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
48 | ### Code Splitting
49 |
50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
51 |
52 | ### Analyzing the Bundle Size
53 |
54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
55 |
56 | ### Making a Progressive Web App
57 |
58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
59 |
60 | ### Advanced Configuration
61 |
62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
63 |
64 | ### Deployment
65 |
66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
67 |
68 | ### `npm run build` fails to minify
69 |
70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
71 |
--------------------------------------------------------------------------------
/craco.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path")
2 | const CracoLessPlugin = require('craco-less');
3 |
4 | const resolve = dir => path.resolve(__dirname, dir)
5 |
6 | module.exports = {
7 | plugins: [
8 | {
9 | plugin: CracoLessPlugin,
10 | options: {
11 | lessLoaderOptions: {
12 | lessOptions: {
13 | modifyVars: { '@primary-color': '#1DA57A' },
14 | javascriptEnabled: true,
15 | },
16 | },
17 | },
18 | },
19 | ],
20 | webpack: {
21 | alias: {
22 | "@": resolve("src"),
23 | "components": resolve("src/components"),
24 | // '@mui/styled-engine': '@mui/styled-engine-sc'
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "esnext",
5 | "baseUrl": "./",
6 | "moduleResolution": "node",
7 | "paths": {
8 | "@/*": [
9 | "src/*"
10 | ]
11 | },
12 | "jsx": "preserve",
13 | "lib": [
14 | "esnext",
15 | "dom",
16 | "dom.iterable",
17 | "scripthost"
18 | ]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hy_airbnb_temp",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@mui/material": "^5.10.5",
7 | "@mui/styled-engine-sc": "^5.10.1",
8 | "@reduxjs/toolkit": "^1.8.5",
9 | "@testing-library/jest-dom": "^5.16.5",
10 | "@testing-library/react": "^13.4.0",
11 | "@testing-library/user-event": "^13.5.0",
12 | "antd": "^4.23.1",
13 | "axios": "^0.27.2",
14 | "classnames": "^2.3.2",
15 | "normalize.css": "^8.0.1",
16 | "react": "^18.2.0",
17 | "react-dom": "^18.2.0",
18 | "react-redux": "^8.0.2",
19 | "react-router-dom": "^6.3.0",
20 | "react-scripts": "5.0.1",
21 | "react-transition-group": "^4.4.5",
22 | "underscore": "^1.13.4",
23 | "web-vitals": "^2.1.4"
24 | },
25 | "scripts": {
26 | "start": "craco start",
27 | "build": "craco build",
28 | "test": "craco test",
29 | "eject": "react-scripts eject"
30 | },
31 | "eslintConfig": {
32 | "extends": [
33 | "react-app",
34 | "react-app/jest"
35 | ]
36 | },
37 | "browserslist": {
38 | "production": [
39 | ">0.2%",
40 | "not dead",
41 | "not op_mini all"
42 | ],
43 | "development": [
44 | "last 1 chrome version",
45 | "last 1 firefox version",
46 | "last 1 safari version"
47 | ]
48 | },
49 | "devDependencies": {
50 | "@craco/craco": "^7.0.0-alpha.7",
51 | "craco-less": "^2.1.0-alpha.0",
52 | "styled-components": "^5.3.5"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coderwhy/hy-react-airbnb/5043d75c3a869508ef4466da933546c73eda8132/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | Airbnb爱彼迎(课堂) - 全球民宿_公寓_短租_住宿_预订平台
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coderwhy/hy-react-airbnb/5043d75c3a869508ef4466da933546c73eda8132/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coderwhy/hy-react-airbnb/5043d75c3a869508ef4466da933546c73eda8132/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { useRoutes } from "react-router-dom"
3 | import AppFooter from './components/app-footer'
4 | import AppHeader from './components/app-header'
5 | import { useScrollTop } from './hooks'
6 | import routes from './router'
7 |
8 | const App = memo((props) => {
9 | useScrollTop() // 回到顶部
10 |
11 | return (
12 |
13 |
14 |
{useRoutes(routes)}
15 |
16 |
17 | )
18 | })
19 |
20 | export default App
21 |
--------------------------------------------------------------------------------
/src/assets/css/common.less:
--------------------------------------------------------------------------------
1 | body {
2 | font-size: 14px;
3 | font-family: Circular, "PingFang-SC", "Hiragino Sans GB", "微软雅黑", "Microsoft YaHei", "Heiti SC" ;
4 | -webkit-font-smoothing: antialiased;
5 | }
--------------------------------------------------------------------------------
/src/assets/css/index.less:
--------------------------------------------------------------------------------
1 | @import "./reset.less";
2 | @import "./common.less";
3 |
--------------------------------------------------------------------------------
/src/assets/css/reset.less:
--------------------------------------------------------------------------------
1 | @mainColor: #484848;
2 |
3 | blockquote, body, button, dd, dl, dt, fieldset, form, h1, h2, h3, h4, h5, h6, hr, input, legend, li, ol, p, pre, td, textarea, th, ul {
4 | // color: @mainColor;
5 | padding: 0;
6 | margin: 0;
7 | }
8 |
9 | a {
10 | color: @mainColor;
11 | text-decoration: none;
12 | }
13 |
14 | img {
15 | vertical-align: top;
16 | }
17 |
--------------------------------------------------------------------------------
/src/assets/data/filter_data.json:
--------------------------------------------------------------------------------
1 | [
2 | "人数",
3 | "可免费取消",
4 | "房源类型",
5 | "价格",
6 | "位置区域",
7 | "闪定",
8 | "卧室/床数",
9 | "促销/优惠",
10 | "更多筛选条件"
11 | ]
--------------------------------------------------------------------------------
/src/assets/data/footer.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "爱彼迎",
4 | "list": ["工作机会", "爱彼迎新闻", "政策", "无障碍设施"]
5 | },
6 | {
7 | "name": "发现",
8 | "list": ["信任与安全", "旅行基金", "商务差旅", "爱彼迎杂志", "Airbnb.org"]
9 | },
10 | {
11 | "name": "出租",
12 | "list": ["为什么要出租", "待客之道", "房东义务", "开展体验", "资源中心"]
13 | },
14 | {
15 | "name": "客服支持",
16 | "list": ["帮助", "邻里支持"]
17 | }
18 | ]
--------------------------------------------------------------------------------
/src/assets/data/search_titles.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "title": "搜索房源",
4 | "searchInfos": [
5 | {
6 | "title": "城市",
7 | "desc": "你想去哪个城市"
8 | },
9 | {
10 | "title": "入住退房日期",
11 | "desc": "请再日历中选择"
12 | },
13 | {
14 | "title": "关键字",
15 | "desc": "景点/住址/房源名"
16 | }
17 | ]
18 | },
19 | {
20 | "title": "搜索体验",
21 | "searchInfos": [
22 | {
23 | "title": "城市",
24 | "desc": "你想去哪个城市"
25 | },
26 | {
27 | "title": "日期",
28 | "desc": "你想合适出发"
29 | }
30 | ]
31 | }
32 | ]
33 |
34 |
--------------------------------------------------------------------------------
/src/assets/img/cover_01.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coderwhy/hy-react-airbnb/5043d75c3a869508ef4466da933546c73eda8132/src/assets/img/cover_01.jpeg
--------------------------------------------------------------------------------
/src/assets/svg/icon-arrow-left.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { styleStrToObj } from './utils'
3 |
4 | const IconArrowLeft = memo((props) => {
5 | const { width = 12, height = 12 } = props
6 |
7 | return (
8 |
9 | )
10 | })
11 |
12 | export default IconArrowLeft
--------------------------------------------------------------------------------
/src/assets/svg/icon-arrow-right.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { styleStrToObj } from './utils'
3 |
4 | const IconArrowRight = memo((props) => {
5 | const { width = 12, height = 12 } = props
6 |
7 | return (
8 |
9 | )
10 | })
11 |
12 | export default IconArrowRight
--------------------------------------------------------------------------------
/src/assets/svg/icon-close.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { styleStrToObj } from './utils'
3 |
4 | const IconClose = memo(() => {
5 | return (
6 |
7 | )
8 | })
9 |
10 | export default IconClose
--------------------------------------------------------------------------------
/src/assets/svg/icon-global.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 |
3 | const IconGlobal = memo(() => {
4 | return (
5 |
6 | )
7 | })
8 |
9 | export default IconGlobal
10 |
--------------------------------------------------------------------------------
/src/assets/svg/icon-more-arrow.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { styleStrToObj } from './utils'
3 |
4 | const IconMoreArrow = memo(() => {
5 | return (
6 |
7 | )
8 | })
9 |
10 | export default IconMoreArrow
--------------------------------------------------------------------------------
/src/assets/svg/icon-profile-avatar.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 |
3 | const IconProfileAvatar = memo(() => {
4 | return (
5 | //
8 | )
9 | })
10 |
11 | export default IconProfileAvatar
--------------------------------------------------------------------------------
/src/assets/svg/icon-profile-menu.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 |
3 | const IconProfileMenu = memo(() => {
4 | return (
5 |
6 | )
7 | })
8 |
9 | export default IconProfileMenu
--------------------------------------------------------------------------------
/src/assets/svg/icon-search-bar.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { styleStrToObj } from './utils'
3 |
4 | const IconSearchBar = memo(() => {
5 | return (
6 |
7 | )
8 | })
9 |
10 | export default IconSearchBar
--------------------------------------------------------------------------------
/src/assets/svg/icon-triangle-bottom.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { styleStrToObj } from './utils'
3 |
4 | const IconTriangleBottom = memo(() => {
5 | return (
6 |
7 | )
8 | })
9 |
10 | export default IconTriangleBottom
--------------------------------------------------------------------------------
/src/assets/svg/icon-triangle-top.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { styleStrToObj } from './utils'
3 |
4 | const IconTriangleTop = memo(() => {
5 | return (
6 |
7 | )
8 | })
9 |
10 | export default IconTriangleTop
--------------------------------------------------------------------------------
/src/assets/svg/icon_logo.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 |
3 | const IconLogo = memo(() => {
4 | return (
5 |
6 | )
7 | })
8 |
9 | export default IconLogo
--------------------------------------------------------------------------------
/src/assets/svg/utils/index.js:
--------------------------------------------------------------------------------
1 | function styleStrToObj(str){
2 | const obj = {}
3 | const s = str.toLowerCase().replace(/-(.)/g, function (m, g) {
4 | return g.toUpperCase();
5 | }).replace(/;\s?$/g,"").split(/:|;/g);
6 | for (var i = 0; i < s.length; i += 2) {
7 | obj[s[i].replace(/\s/g,"")] = s[i+1].replace(/^\s+|\s+$/g,"");
8 | }
9 | return obj;
10 | }
11 |
12 | export {
13 | styleStrToObj
14 | }
--------------------------------------------------------------------------------
/src/assets/theme/index.js:
--------------------------------------------------------------------------------
1 | const theme = {
2 | color: {
3 | primaryColor: "#FF385C",
4 | secondaryColor: "#00848A",
5 | textColor: "#484848",
6 | textColorSecondary: "#222222"
7 | },
8 | fontSize: {
9 | small: "12px",
10 | normal: "14px",
11 | large: "16px"
12 | },
13 | mixin: {
14 | boxShadow: `
15 | transition: box-shadow 0.2s ease;
16 | &:hover {
17 | box-shadow: 0 2px 4px rgba(0,0,0,0.18);
18 | }
19 | `
20 | }
21 | }
22 |
23 | export default theme
24 |
--------------------------------------------------------------------------------
/src/base-ui/Indicator/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useEffect, useRef } from 'react'
2 | import { IndicatorWrapper } from './style'
3 |
4 | const Indicator = memo((props) => {
5 | const { selectIndex } = props
6 | const scrollRef = useRef()
7 |
8 | useEffect(() => {
9 | const selectItemEl = scrollRef.current.children[selectIndex]
10 | const selectItemWidth = selectItemEl.clientWidth
11 | const selectItemOffset = selectItemEl.offsetLeft
12 |
13 | const scrollElWidth = scrollRef.current.clientWidth
14 | const scrollElScroll = scrollRef.current.scrollWidth
15 |
16 | let distance = selectItemWidth * 0.5 + selectItemOffset - scrollElWidth * 0.5
17 | if (distance < 0) distance = 0
18 | if (distance > scrollElScroll - scrollElWidth) distance = scrollElScroll - scrollElWidth
19 | scrollRef.current.style.transform = `translate(${-distance}px)`
20 | }, [selectIndex])
21 |
22 | return (
23 |
24 |
25 | {
26 | props.children
27 | }
28 |
29 |
30 | )
31 | })
32 |
33 | Indicator.propTypes = {}
34 |
35 | export default Indicator
--------------------------------------------------------------------------------
/src/base-ui/Indicator/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const IndicatorWrapper = styled.div`
4 | overflow: hidden;
5 |
6 | .scroll {
7 | position: relative;
8 | display: flex;
9 | transition: transform 200ms ease;
10 |
11 | > * {
12 | flex-shrink: 0;
13 | }
14 | }
15 | `
--------------------------------------------------------------------------------
/src/base-ui/picture-browser/index.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import React, { memo, useEffect, useState } from 'react'
3 | import { SwitchTransition, CSSTransition } from 'react-transition-group'
4 |
5 | import IconClose from '@/assets/svg/icon-close'
6 | import IconArrowLeft from '@/assets/svg/icon-arrow-left'
7 | import IconArrowRight from '@/assets/svg/icon-arrow-right'
8 | import { BrowserWrapper } from './style'
9 | import Indicator from '../Indicator'
10 | import classNames from 'classnames'
11 | import IconTriangleBottom from '@/assets/svg/icon-triangle-bottom'
12 | import IconTriangleTop from '@/assets/svg/icon-triangle-top'
13 |
14 | const PictureBrowser = memo((props) => {
15 | const { pictureUrls = [], closeClick } = props
16 | const [selectIndex, setSelectIndex] = useState(0)
17 | const [isNext, setIsNext] = useState(true)
18 | const [showList, setShowList] = useState(true)
19 | useEffect(() => {
20 | document.body.style.overflow = "hidden"
21 | }, [])
22 |
23 |
24 | /** 事件处理的逻辑 */
25 | function closeBtnClickHandle() {
26 | document.body.style.overflow = "auto"
27 | closeClick()
28 | }
29 |
30 | function controlClickHandle(isNext = true) {
31 | let newIndex = isNext ? selectIndex + 1: selectIndex - 1
32 | if (newIndex < 0) newIndex = pictureUrls.length - 1
33 | if (newIndex > pictureUrls.length - 1) newIndex = 0
34 | setSelectIndex(newIndex)
35 | setIsNext(isNext)
36 | }
37 |
38 | function imgItemClickHandle(index) {
39 | setSelectIndex(index)
40 | setIsNext(index > selectIndex)
41 | }
42 |
43 | function toggleShowListHandle() {
44 | setShowList(!showList)
45 | }
46 |
47 | return (
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
controlClickHandle(false)}>
57 |
58 |
59 |
controlClickHandle(true)}>
60 |
61 |
62 |
63 |
64 |
65 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | {selectIndex+1}/{pictureUrls.length}:
80 | room Apartment图片{selectIndex+1}
81 |
82 |
83 | 隐藏照片列表
84 | { showList ? : }
85 |
86 |
87 |
88 |
89 | {
90 | pictureUrls.map((item, index) => {
91 | return (
92 | imgItemClickHandle(index)}
96 | >
97 |

98 |
99 | )
100 | })
101 | }
102 |
103 |
104 |
105 |
106 |
107 | )
108 | })
109 |
110 | PictureBrowser.propTypes = {
111 | pictureUrls: PropTypes.array
112 | }
113 |
114 | export default PictureBrowser
--------------------------------------------------------------------------------
/src/base-ui/picture-browser/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const BrowserWrapper = styled.div`
4 | position: fixed;
5 | z-index: 999;
6 | left: 0;
7 | right: 0;
8 | top: 0;
9 | bottom: 0;
10 | background-color: rgb(33,33,33);
11 | opacity: 1;
12 | display: flex;
13 | flex-direction: column;
14 |
15 | .top {
16 | position: relative;
17 | height: 86px;
18 |
19 | .close-btn {
20 | position: absolute;
21 | top: 15px;
22 | right: 25px;
23 | }
24 | }
25 |
26 | .slider {
27 | position: relative;
28 | display: flex;
29 | justify-content: center;
30 | align-items: center;
31 | flex: 1;
32 | overflow: hidden;
33 |
34 | .container {
35 | position: relative;
36 | height: 100%;
37 | overflow: hidden;
38 | width: 100% !important;
39 | max-width: 105vh !important;
40 |
41 | img {
42 | position: absolute;
43 | top: 0;
44 | left: 0;
45 | right: 0;
46 | margin: 0 auto;
47 | height: 100%;
48 | user-select: none;
49 | }
50 | }
51 |
52 | .control {
53 | position: absolute;
54 | z-index: 1;
55 | left: 0;
56 | right: 0;
57 | top: 0;
58 | display: flex;
59 | justify-content: space-between;
60 | bottom: 0;
61 | color: #fff;
62 |
63 | .btn {
64 | display: flex;
65 | justify-content: center;
66 | align-items: center;
67 | width: 83px;
68 | height: 100%;
69 | }
70 | }
71 |
72 | .fade-enter {
73 | transform: translate(${props => props.isNext ? "100%":"-100%"});
74 | opacity: 0;
75 | }
76 |
77 | .fade-enter-active {
78 | opacity: 1;
79 | transform: translate(0);
80 | transition: all 150ms ease;
81 | }
82 |
83 | .fade-exit {
84 | opacity: 1;
85 | }
86 |
87 | .fade-exit-active {
88 | opacity: 0;
89 | transition: all 150ms ease;
90 | }
91 | }
92 |
93 | .preview {
94 | display: flex;
95 | justify-content: center;
96 | height: 100px;
97 | margin-top: 10px;
98 |
99 | .info {
100 | position: absolute;
101 | bottom: 10px;
102 | max-width: 105vh;
103 | color: #fff;
104 |
105 | .desc {
106 | display: flex;
107 | justify-content: space-between;
108 |
109 | .toggle {
110 | cursor: pointer;
111 | }
112 | }
113 |
114 | .list {
115 | margin-top: 3px;
116 | overflow: hidden;
117 | transition: height 300ms ease;
118 |
119 | .item {
120 | margin-right: 15px;
121 | cursor: pointer;
122 |
123 | img {
124 | height: 67px;
125 | opacity: 0.5;
126 | }
127 |
128 | &.active {
129 | img {
130 | opacity: 1;
131 | }
132 | }
133 | }
134 | }
135 | }
136 | }
137 | `
--------------------------------------------------------------------------------
/src/base-ui/scroll-view/index.jsx:
--------------------------------------------------------------------------------
1 | import IconArrowLeft from '@/assets/svg/icon-arrow-left'
2 | import IconArrowRight from '@/assets/svg/icon-arrow-right'
3 | import React, { memo, useEffect, useRef, useState } from 'react'
4 | import { ScrollWrapper } from './style'
5 |
6 | const ScrollView = memo((props) => {
7 | /** 记录正在显示的是哪一个 */
8 | const [posIndex, setPosIndex] = useState(0)
9 | const [showLeft, setShowLeft] = useState(false)
10 | const [showRight, setShowRight] = useState(true)
11 |
12 | /** 滚动区域的值 */
13 | const scrollRef = useRef()
14 | const totalDistanceRef = useRef(0)
15 | useEffect(() => {
16 | const scrollWidth = scrollRef.current.scrollWidth
17 | const clientWidth = scrollRef.current.clientWidth
18 | totalDistanceRef.current = scrollWidth - clientWidth
19 | setShowRight(totalDistanceRef.current > 0)
20 | }, [props.children])
21 |
22 | /** 事件处理 */
23 | function leftClickHandle() {
24 | scrollPosition(posIndex-1)
25 | }
26 |
27 | function rightClickHandle() {
28 | scrollPosition(posIndex + 1)
29 | }
30 |
31 | function scrollPosition(index) {
32 | const scrollLeft = scrollRef.current.children[index].offsetLeft
33 | scrollRef.current.style.transform = `translate(-${scrollLeft}px)`
34 | setPosIndex(index)
35 | if (scrollLeft > totalDistanceRef.current) {
36 | setShowRight(false)
37 | }
38 | setShowRight(totalDistanceRef.current > scrollLeft)
39 | setShowLeft(scrollLeft > 0)
40 | }
41 |
42 | return (
43 |
44 | {showLeft && (
45 |
46 |
47 |
48 | )}
49 | {showRight && (
50 |
51 |
52 |
53 | )}
54 |
55 |
56 | {props.children}
57 |
58 |
59 |
60 | )
61 | })
62 |
63 |
64 | export default ScrollView
--------------------------------------------------------------------------------
/src/base-ui/scroll-view/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const ScrollWrapper = styled.div`
4 | position: relative;
5 | padding: 8px 0;
6 |
7 | .content {
8 | overflow: hidden;
9 |
10 | .scroll {
11 | display: flex;
12 | white-space: nowrap;
13 | transition: transform 200ms ease;
14 | }
15 | }
16 |
17 | .control {
18 | position: absolute;
19 | z-index: 9;
20 | display: flex;
21 | justify-content: center;
22 | align-items: center;
23 | width: 28px;
24 | height: 28px;
25 | border-radius: 50%;
26 | text-align: center;
27 | border-width: 2px;
28 | border-style: solid;
29 | border-color: #fff;
30 | background: #fff;
31 | box-shadow: 0px 1px 1px 1px rgba(0,0,0,.14);
32 | cursor: pointer;
33 |
34 | &.left {
35 | left: 0;
36 | top: 50%;
37 | transform: translate(-50%, -50%);
38 | }
39 |
40 | &.right {
41 | right: 0;
42 | top: 50%;
43 | transform: translate(50%, -50%);
44 | }
45 | }
46 | `
--------------------------------------------------------------------------------
/src/components/app-footer/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { FooterWrapper } from './style'
3 | import footerData from "@/assets/data/footer.json"
4 |
5 | const AppFooter = memo(() => {
6 | return (
7 |
8 |
9 |
10 | {
11 | footerData.map(item => {
12 | return (
13 |
14 |
{item.name}
15 |
16 | {
17 | item.list.map(iten => {
18 | return
{iten}
19 | })
20 | }
21 |
22 |
23 | )
24 | })
25 | }
26 |
27 |
© 2022 Airbnb, Inc. All rights reserved.条款 · 隐私政策 · 网站地图 · 全国旅游投诉渠道 12301
28 |
29 |
30 | )
31 | })
32 |
33 | export default AppFooter
--------------------------------------------------------------------------------
/src/components/app-footer/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const FooterWrapper = styled.div`
4 | margin-top: 100px;
5 | border-top: 1px solid #EBEBEB;
6 |
7 | .wrapper {
8 | width: 1080px;
9 | margin: 0 auto;
10 | box-sizing: border-box;
11 | padding: 48px 24px;
12 | }
13 |
14 | .service {
15 | display: flex;
16 |
17 | .item {
18 | flex: 1;
19 |
20 | .name {
21 | margin-bottom: 16px;
22 | font-weight: 700;
23 | }
24 |
25 | .list {
26 | .iten {
27 | margin-top: 6px;
28 | color: #767676;
29 | cursor: pointer;
30 | &:hover {
31 | text-decoration: underline;
32 | }
33 | }
34 | }
35 | }
36 | }
37 |
38 | .statement {
39 | margin-top: 30px;
40 | border-top: 1px solid #EBEBEB;
41 | padding: 20px;
42 | color: #767676;
43 | text-align: center;
44 | }
45 | `
--------------------------------------------------------------------------------
/src/components/app-header/c-cpns/header-center/c-cpns/search-sections/index.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import React, { memo } from 'react'
3 | import { SectionsWrapper } from './style'
4 |
5 | const SearchSections = memo((props) => {
6 | const { searchInfos } = props
7 |
8 | return (
9 |
10 | {
11 | searchInfos.map((item, index) => {
12 | return (
13 |
14 |
15 |
{item.title}
16 |
{item.desc}
17 |
18 | { index !== searchInfos.length -1 &&
}
19 |
20 | )
21 | })
22 | }
23 |
24 | )
25 | })
26 |
27 | SearchSections.propTypes = {
28 | searchInfos: PropTypes.array
29 | }
30 |
31 | export default SearchSections
--------------------------------------------------------------------------------
/src/components/app-header/c-cpns/header-center/c-cpns/search-sections/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const SectionsWrapper = styled.div`
4 | display: flex;
5 | width: 850px;
6 | height: 66px;
7 | border-radius: 32px;
8 | border: 1px solid #ddd;
9 | background-color: #fff;
10 |
11 | .item {
12 | flex: 1;
13 | display: flex;
14 | align-items: center;
15 | border-radius: 32px;
16 |
17 | .info {
18 | flex: 1;
19 | display: flex;
20 | flex-direction: column;
21 | justify-content: center;
22 | padding: 0 30px;
23 |
24 | .title {
25 | font-size: 12px;
26 | font-weight: 800;
27 | color: #222;
28 | }
29 |
30 | .desc {
31 | font-size: 14px;
32 | color: #666;
33 | }
34 | }
35 |
36 | .divider {
37 | height: 32px;
38 | width: 1px;
39 | background-color: #ddd;
40 | }
41 |
42 | &:hover {
43 | background-color: #eee;
44 | }
45 | }
46 | `
--------------------------------------------------------------------------------
/src/components/app-header/c-cpns/header-center/c-cpns/search-tabs/index.jsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames'
2 | import PropTypes from 'prop-types'
3 | import React, { memo, useState } from 'react'
4 | import { TabsWrapper } from './style'
5 |
6 | const SearchTabs = memo((props) => {
7 | const { titles, tabClick } = props
8 | const [currentIndex, setCurrentIndex] = useState(0)
9 |
10 | function itemClickHandle(index) {
11 | setCurrentIndex(index)
12 | if (tabClick) tabClick(index)
13 | }
14 |
15 | return (
16 |
17 | {
18 | titles.map((item, index) => {
19 | return (
20 | itemClickHandle(index)}
24 | >
25 | {item}
26 |
27 |
28 | )
29 | })
30 | }
31 |
32 | )
33 | })
34 |
35 | SearchTabs.propTypes = {
36 | titles: PropTypes.array
37 | }
38 |
39 | export default SearchTabs
--------------------------------------------------------------------------------
/src/components/app-header/c-cpns/header-center/c-cpns/search-tabs/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const TabsWrapper = styled.div`
4 | display: flex;
5 | color: ${props => props.theme.isAlpha ? "#fff": "#222"};
6 |
7 | .item {
8 | position: relative;
9 | width: 64px;
10 | height: 20px;
11 | margin: 10px 16px;
12 | font-size: 16px;
13 | cursor: pointer;
14 |
15 | &.active .bottom {
16 | position: absolute;
17 | top: 28px;
18 | left: 0;
19 | width: 64px;
20 | height: 2px;
21 | /* background-color: ${props => props.theme.color}; */
22 | background-color: ${props => props.theme.isAlpha ? "#fff": "#333"};
23 | }
24 | }
25 | `
--------------------------------------------------------------------------------
/src/components/app-header/c-cpns/header-center/index.jsx:
--------------------------------------------------------------------------------
1 | import IconSearchBar from '@/assets/svg/icon-search-bar'
2 | import React, { memo, useState } from 'react'
3 | import { CSSTransition } from "react-transition-group"
4 | import { CenterWrapper } from './style'
5 | import searchTitles from "@/assets/data/search_titles.json"
6 | import SearchTabs from './c-cpns/search-tabs'
7 | import SearchSections from './c-cpns/search-sections'
8 |
9 | const HeaderCenter = memo((props) => {
10 | const { isSearch, searchBarClick } = props
11 | const [currentTab, setCurrentTab] = useState(0)
12 |
13 | /** 过滤数据 */
14 | const titles = searchTitles.map(item => item.title)
15 |
16 | /** 事件处理 */
17 | function tabClickHandle(index) {
18 | setCurrentTab(index)
19 | }
20 |
21 | return (
22 |
23 |
29 | searchBarClick()}>
30 |
搜索房源和体验
31 |
32 |
33 |
34 |
35 |
36 |
42 |
48 |
49 |
50 | )
51 | })
52 |
53 | export default HeaderCenter
--------------------------------------------------------------------------------
/src/components/app-header/c-cpns/header-center/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 |
3 | export const CenterWrapper = styled.div`
4 | position: relative;
5 | display: flex;
6 | justify-content: center;
7 | height: 48px;
8 |
9 | .search-bar {
10 | position: absolute;
11 | display: flex;
12 | justify-content: space-between;
13 | align-items: center;
14 | width: 300px;
15 | height: 48px;
16 | box-sizing: border-box;
17 | padding: 0 8px;
18 | border: 1px solid #ddd;
19 | border-radius: 24px;
20 | cursor: pointer;
21 | will-change: transform, opacity;
22 |
23 | ${props => props.theme.mixin.boxShadow};
24 |
25 | .text {
26 | padding: 0 16px;
27 | color: #222;
28 | font-weight: 600;
29 | }
30 |
31 | .icon {
32 | display: flex;
33 | align-items: center;
34 | justify-content: center;
35 | width: 32px;
36 | height: 32px;
37 | border-radius: 50%;
38 | color: #fff;
39 | background-color: ${props => props.theme.color.primaryColor};
40 | }
41 | }
42 |
43 | .search-detail {
44 | position: relative;
45 | transform-origin: 50% 0;
46 | will-change: transform, opacity;
47 | /* transition: all 250ms linear; */
48 |
49 | .infos {
50 | position: absolute;
51 | top: 60px;
52 | left: 50%;
53 | transform: translateX(-50%);
54 | }
55 | }
56 |
57 | .detail-exit {
58 | transform: scale(1.0) translateY(0);
59 | opacity: 1;
60 | }
61 |
62 | .detail-exit-active {
63 | transition: all 250ms ease;
64 | transform: scale(0.35, 0.727273) translateY(-58px);
65 | opacity: 0;
66 | }
67 |
68 | .detail-enter {
69 | transform: scale(0.35, 0.727273) translateY(-58px);
70 | opacity: 0;
71 | }
72 |
73 | .detail-enter-active {
74 | transform: scale(1.0) translateY(0);
75 | opacity: 1;
76 | transition: all 250ms ease;
77 | }
78 |
79 | .bar-enter {
80 | transform: scale(2.85714, 1.375) translateY(58px);
81 | opacity: 0;
82 | }
83 |
84 | .bar-enter-active {
85 | transition: all 250ms ease;
86 | transform: scale(1.0) translateY(0);
87 | opacity: 1;
88 | }
89 |
90 | .bar-exit {
91 | opacity: 0;
92 | }
93 | `
94 |
--------------------------------------------------------------------------------
/src/components/app-header/c-cpns/header-left/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import IconLogo from '@/assets/svg/icon_logo'
3 | import { LeftWrapper } from './style'
4 | import { useNavigate } from 'react-router-dom'
5 |
6 | const HeaderLeft = memo(() => {
7 | const navigate = useNavigate()
8 | function logoClickHandle() {
9 | navigate("/home")
10 | }
11 |
12 | return (
13 |
14 |
15 |
16 | )
17 | })
18 |
19 | export default HeaderLeft
--------------------------------------------------------------------------------
/src/components/app-header/c-cpns/header-left/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 |
3 | export const LeftWrapper = styled.div`
4 | flex: 1;
5 | display: flex;
6 | color: ${props => props.theme.isAlpha ? "#fff": props.theme.color.primaryColor};
7 | `
8 |
9 |
--------------------------------------------------------------------------------
/src/components/app-header/c-cpns/header-right/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { RightWrapper } from './style'
3 | import IconGlobal from '@/assets/svg/icon-global'
4 | import IconProfileMenu from '@/assets/svg/icon-profile-menu'
5 | import IconProfileAvatar from '@/assets/svg/icon-profile-avatar'
6 |
7 | const HeaderRight = memo(() => {
8 | return (
9 |
10 |
11 | 登录
12 | 注册
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | )
24 | })
25 |
26 | export default HeaderRight
--------------------------------------------------------------------------------
/src/components/app-header/c-cpns/header-right/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 |
3 | export const RightWrapper = styled.div`
4 | flex: 1;
5 | display: flex;
6 | justify-content: flex-end;
7 | align-items: center;
8 | color: ${props => props.theme.isAlpha ? "#fff": "#484848"};
9 | font-weight: 600;
10 |
11 | .btns {
12 | display: flex;
13 | align-items: center;
14 |
15 | .btn {
16 | height: 18px;
17 | line-height: 18px;
18 | box-sizing: content-box;
19 | padding: 12px 15px;
20 | cursor: pointer;
21 | border-radius: 22px;
22 |
23 | &:hover {
24 | background-color: #f5f5f5;
25 | }
26 | }
27 | }
28 |
29 | .profile {
30 | display: flex;
31 | width: 77px;
32 | height: 42px;
33 | justify-content: space-evenly;
34 | align-items: center;
35 | box-sizing: border-box;
36 | border: 1px solid #ccc;
37 | color: #484848;
38 | border-radius: 25px;
39 | background-color: #fff;
40 | cursor: pointer;
41 |
42 | /* transition: box-shadow 0.2s ease;
43 | &:hover {
44 | box-shadow: 0 2px 4px rgba(0,0,0,0.18);
45 | } */
46 |
47 | ${props => props.theme.mixin.boxShadow}
48 | }
49 | `
--------------------------------------------------------------------------------
/src/components/app-header/index.jsx:
--------------------------------------------------------------------------------
1 | import { useScrollPosition } from '@/hooks/useScrollPosition'
2 | import { ThemeProvider } from "styled-components"
3 | import classNames from 'classnames'
4 | import React, { memo, useEffect, useRef, useState } from 'react'
5 | import { useSelector } from 'react-redux'
6 | import HeaderCenter from './c-cpns/header-center'
7 | import HeaderLeft from './c-cpns/header-left'
8 | import HeaderRight from './c-cpns/header-right'
9 | import { HeaderWrapper, SearchAreaPlaceholder } from './style'
10 |
11 | const AppHeader = memo((props) => {
12 | const [isSearch, setIsSearch] = useState(false)
13 | const [isAlpha, setIsAlpha] = useState(false)
14 |
15 | /** redux中获取数据 */
16 | const { headerConfig } = useSelector((state) => ({
17 | headerConfig: state.main.headerConfig
18 | }))
19 | const { isFixed, isHome } = headerConfig
20 |
21 | /** 其他hooks的逻辑 */
22 | const { scrollY } = useScrollPosition()
23 | if (isHome && scrollY === 0 && !isSearch) {
24 | setIsAlpha(true)
25 | setIsSearch(true)
26 | }
27 | if (isHome && isAlpha && scrollY > 0 && isSearch) {
28 | setIsAlpha(false)
29 | setIsSearch(false)
30 | }
31 |
32 | const prevY = useRef()
33 | useEffect(() => { prevY.current = 0 }, [])
34 | if (!isSearch) prevY.current = scrollY
35 | if (Math.abs(prevY.current - scrollY) > 30 && isSearch) setIsSearch(false)
36 |
37 | /** 事件处理逻辑 */
38 | function searchBarClickHandle() {
39 | setIsSearch(true)
40 | }
41 |
42 | return (
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | { isSearch && !isAlpha && setIsSearch(false)}>
}
54 |
55 |
56 | )
57 | })
58 |
59 | export default AppHeader
--------------------------------------------------------------------------------
/src/components/app-header/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 |
3 | export const HeaderWrapper = styled.div`
4 | &.fixed {
5 | position: fixed;
6 | z-index: 99;
7 | top: 0;
8 | left: 0;
9 | right: 0;
10 | }
11 |
12 | .content {
13 | position: relative;
14 | z-index: 9;
15 | transition: all 250ms ease;
16 | border-bottom: 1px solid #eee;
17 | border-bottom-color: ${props => props.theme.isAlpha ? "rgba(238,238,238,0)": "rgba(238,238,238,1)"};
18 | background-color: ${props => props.theme.isAlpha ? "rgba(255,255,255,0)": "rgba(255,255,255,1)"};
19 | }
20 |
21 | .top {
22 | display: flex;
23 | align-items: center;
24 | height: 80px;
25 | padding: 0 24px;
26 | }
27 |
28 | .cover {
29 | position: fixed;
30 | left: 0;
31 | right: 0;
32 | top: 0;
33 | bottom: 0;
34 | background-color: rgba(0,0,0,.3);
35 | }
36 | `
37 |
38 | export const SearchAreaPlaceholder = styled.div`
39 | height: ${props => props.isSearch ? "100px": "0"};
40 | transition: height 250ms ease;
41 | `
42 |
--------------------------------------------------------------------------------
/src/components/longfor-item/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { ItemWrapper } from './style'
3 |
4 | const LongforItem = memo((props) => {
5 | const { itemData } = props
6 |
7 | return (
8 |
9 |
10 |

11 |
12 |
13 |
{itemData.city}
14 |
均价 {itemData.price}
15 |
16 |
17 |
18 | )
19 | })
20 |
21 | export default LongforItem
--------------------------------------------------------------------------------
/src/components/longfor-item/style.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const ItemWrapper = styled.div`
4 | flex-shrink: 0;
5 | width: 20%;
6 |
7 | .inner {
8 | position: relative;
9 | padding: 8px;
10 | }
11 |
12 | .cover {
13 | width: 100%;
14 | }
15 |
16 | .bg-cover {
17 | position: absolute;
18 | left: 8px;
19 | right: 8px;
20 | bottom: 0;
21 | height: 60%;
22 | background-image: linear-gradient(-180deg, rgba(0, 0, 0, 0) 3%, rgb(0, 0, 0) 100%)
23 | }
24 |
25 | .info {
26 | position: absolute;
27 | left: 8px;
28 | right: 8px;
29 | bottom: 0;
30 | display: flex;
31 | flex-direction: column;
32 | justify-content: center;
33 | align-items: center;
34 | padding: 0 24px 32px;
35 | color: #fff;
36 |
37 | .city {
38 | font-size: 18px;
39 | font-weight: 600;
40 | }
41 |
42 | .price {
43 | font-size: 14px;
44 | margin-top: 5px;
45 | }
46 | }
47 | `
48 |
--------------------------------------------------------------------------------
/src/components/room-item/index.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import React, { memo, useRef, useState } from 'react'
3 | import Rating from '@mui/material/Rating';
4 | import { Carousel } from 'antd';
5 |
6 | import { ItemWrapper } from './style'
7 | import IconArrowLeft from '@/assets/svg/icon-arrow-left';
8 | import IconArrowRight from '@/assets/svg/icon-arrow-right';
9 | import Indicator from '@/base-ui/Indicator';
10 | import classNames from 'classnames';
11 |
12 | const RoomItem = memo((props) => {
13 | const { itemData, itemWidth = "33.3333%", itemClick } = props
14 | const [selectIndex, setSelectIndex] = useState(0)
15 | const swiperRef = useRef()
16 |
17 | function controlClickHandle(isNext = true) {
18 | if (isNext) swiperRef.current.next()
19 | else swiperRef.current.prev()
20 |
21 | let newIndex = isNext ? selectIndex + 1: selectIndex - 1
22 | if (newIndex < 0) newIndex = itemData.picture_urls.length - 1
23 | if (newIndex > itemData.picture_urls.length - 1) newIndex = 0
24 | setSelectIndex(newIndex)
25 | }
26 |
27 | function itemClickHandle() {
28 | if (itemClick) itemClick()
29 | }
30 |
31 | return (
32 |
33 |
34 | {
35 | !itemData.picture_urls ?
36 |

37 |
:
38 |
39 |
40 |
controlClickHandle(false)}>
41 |
42 |
43 |
controlClickHandle(true)}>
44 |
45 |
46 |
47 |
48 |
49 | {
50 | itemData.picture_urls.map((item, index) => {
51 | return (
52 |
53 |
54 |
55 | )
56 | })
57 | }
58 |
59 |
60 |
61 | {
62 | itemData.picture_urls.map((item, index) => {
63 | return (
64 |
65 |

66 |
67 | )
68 | })
69 | }
70 |
71 |
72 | }
73 |
{itemData.verify_info.messages.join("·")}
74 |
{itemData.name}
75 |
¥{itemData.price}/晚
76 |
77 |
81 | {itemData.reviews_count}
82 | { itemData.bottom_info && ·{itemData.bottom_info.content} }
83 |
84 |
85 |
86 | )
87 | })
88 |
89 | RoomItem.propTypes = {
90 | itemData: PropTypes.object
91 | }
92 |
93 | export default RoomItem
--------------------------------------------------------------------------------
/src/components/room-item/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const ItemWrapper = styled.div`
4 | box-sizing: border-box;
5 | width: ${props => props.itemWidth};
6 | padding: 8px;
7 | margin: 8px 0;
8 |
9 | .inner {
10 | width: 100%;
11 | }
12 |
13 | .slider {
14 | position: relative;
15 | cursor: pointer;
16 |
17 | &:hover {
18 | .control {
19 | display: flex;
20 | }
21 | }
22 |
23 | .control {
24 | position: absolute;
25 | z-index: 1;
26 | left: 0;
27 | right: 0;
28 | top: 0;
29 | display: none;
30 | justify-content: space-between;
31 | bottom: 0;
32 | color: #fff;
33 | /* background-color: skyblue; */
34 |
35 | .btn {
36 | display: flex;
37 | justify-content: center;
38 | align-items: center;
39 | width: 83px;
40 | height: 100%;
41 | background: linear-gradient(to left, transparent 0%, rgba(0, 0, 0, 0.25) 100%);
42 |
43 | &.right {
44 | background: linear-gradient(to right, transparent 0%, rgba(0, 0, 0, 0.25) 100%);
45 | }
46 | }
47 | }
48 |
49 | .indicator {
50 | position: absolute;
51 | z-index: 9;
52 | width: 30%;
53 | left: 0;
54 | right: 0;
55 | bottom: 10px;
56 | margin: 0 auto;
57 |
58 | .item {
59 | display: flex;
60 | justify-content: center;
61 | align-items: center;
62 | width: 20%;
63 |
64 | .dot {
65 | width: 6px;
66 | height: 6px;
67 | background-color: #fff;
68 | border-radius: 50%;
69 |
70 | &.active {
71 | width: 8px;
72 | height: 8px;
73 | }
74 | }
75 | }
76 | }
77 | }
78 |
79 | .cover {
80 | position: relative;
81 | box-sizing: border-box;
82 | padding: 66.66% 8px 0;
83 | border-radius: 3px;
84 | overflow: hidden;
85 |
86 | .ant-carousel {
87 | position: absolute;
88 | left: 0;
89 | top: 0;
90 | width: 100%;
91 | height: 100%;
92 | }
93 |
94 | .item {
95 | height: 100%;
96 |
97 | img {
98 | width: 100%;
99 | height: 100%;
100 | }
101 | }
102 |
103 | > img {
104 | position: absolute;
105 | left: 0;
106 | top: 0;
107 | width: 100%;
108 | height: 100%;
109 | object-fit: cover;
110 | }
111 | }
112 |
113 | .desc {
114 | margin: 10px 0 5px;
115 | font-size: 12px;
116 | font-weight: 700;
117 | color: #39576a;
118 | }
119 |
120 | .name {
121 | font-size: 16px;
122 | font-weight: 700;
123 |
124 | overflow: hidden;
125 | text-overflow: ellipsis;
126 | display: -webkit-box;
127 | -webkit-line-clamp: 2;
128 | -webkit-box-orient: vertical;
129 | }
130 |
131 | .price {
132 | margin: 8px 0;
133 | }
134 |
135 | .bottom {
136 | display: flex;
137 | align-items: center;
138 | font-size: 12px;
139 | font-weight: 600;
140 | color: ${props => props.theme.color.textColor};
141 |
142 | .count {
143 | margin: 0 2px 0 4px;
144 | }
145 |
146 | .MuiRating-decimal {
147 | margin-right: -3px;
148 | }
149 | }
150 | `
151 |
--------------------------------------------------------------------------------
/src/components/section-footer/index.jsx:
--------------------------------------------------------------------------------
1 | import IconMoreArrow from '@/assets/svg/icon-more-arrow'
2 | import PropTypes from 'prop-types'
3 | import React, { memo } from 'react'
4 | import { useNavigate } from 'react-router-dom'
5 | import { FooterWrapper } from './style'
6 |
7 | const SectionFooter = memo((props) => {
8 | const { name } = props
9 |
10 | let showName = "查看全部"
11 | if (name) {
12 | showName = `查看更多${name}房源`
13 | }
14 |
15 | const navigate = useNavigate()
16 | function showEntireHandle() {
17 | navigate("/entire")
18 | }
19 |
20 | return (
21 |
22 | {showName}
23 |
24 |
25 | )
26 | })
27 |
28 | SectionFooter.propTypes = {
29 | name: PropTypes.string
30 | }
31 |
32 | export default SectionFooter
--------------------------------------------------------------------------------
/src/components/section-footer/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const FooterWrapper = styled.div`
4 | display: inline-flex;
5 | align-items: center;
6 | margin: 15px 0 10px;
7 | font-size: 17px;
8 | font-weight: 600;
9 | color: ${props => props.name ? props.theme.color.secondaryColor: "#000"};
10 | cursor: pointer;
11 |
12 | .text {
13 | margin-right: 5px;
14 | }
15 |
16 | &:hover {
17 | .text {
18 | text-decoration: underline;
19 | }
20 | }
21 | `
--------------------------------------------------------------------------------
/src/components/section-header/index.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import React, { memo } from 'react'
3 | import { HeaderWrapper } from './style'
4 |
5 | const SectionHeader = memo((props) => {
6 | const { title, subtitle } = props
7 |
8 | return (
9 |
10 | {title}
11 | { subtitle && {subtitle}
}
12 |
13 | )
14 | })
15 |
16 | SectionHeader.propTypes = {
17 | title: PropTypes.string,
18 | subtitle: PropTypes.string
19 | }
20 |
21 | export default SectionHeader
--------------------------------------------------------------------------------
/src/components/section-header/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const HeaderWrapper = styled.div`
4 | color: #222;
5 |
6 | .title {
7 | font-size: 22px;
8 | font-weight: 700;
9 | margin-bottom: 16px;
10 | }
11 |
12 | .subtitle {
13 | font-size: 16px;
14 | margin-bottom: 20px;
15 | }
16 | `
--------------------------------------------------------------------------------
/src/components/section-rooms/index.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import React, { memo } from 'react'
3 | import RoomItem from '../room-item'
4 | import { RoomWrapper } from './style'
5 |
6 | const SectionRooms = memo((props) => {
7 | const { roomList, itemWidth } = props
8 |
9 | return (
10 |
11 | {
12 | roomList.map(item => {
13 | return
14 | })
15 | }
16 |
17 | )
18 | })
19 |
20 | SectionRooms.propTypes = {
21 | roomList: PropTypes.array
22 | }
23 |
24 | export default SectionRooms
--------------------------------------------------------------------------------
/src/components/section-rooms/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const RoomWrapper = styled.div`
4 | display: flex;
5 | flex-wrap: wrap;
6 | margin: 0 -8px 0;
7 | `
--------------------------------------------------------------------------------
/src/components/section-tabs/index.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import React, { memo, useState } from 'react'
3 | import classNames from 'classnames'
4 | import { TabsWrapper } from './style'
5 | import ScrollView from '@/base-ui/scroll-view'
6 |
7 | const SectionTabs = memo((props) => {
8 | const { tabNames = [], tabClick } = props
9 | const [currentIndex, setCurrentIndex] = useState(0)
10 |
11 | function itemClickHandle(index, name) {
12 | setCurrentIndex(index)
13 | tabClick(index, name)
14 | }
15 |
16 | return (
17 |
18 |
19 | {
20 | tabNames.map((item, index) => {
21 | return (
22 | itemClickHandle(index, item)}
26 | >
27 | {item}
28 |
29 | )
30 | })
31 | }
32 |
33 |
34 | )
35 | })
36 |
37 | SectionTabs.propTypes = {
38 | tabNames: PropTypes.array
39 | }
40 |
41 | export default SectionTabs
--------------------------------------------------------------------------------
/src/components/section-tabs/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const TabsWrapper = styled.div`
4 | .item {
5 | box-sizing: border-box;
6 | flex-basis: 120px;
7 | flex-shrink: 0;
8 | padding: 14px 16px;
9 | margin-right: 16px;
10 | border-radius: 3px;
11 | font-size: 17px;
12 | text-align: center;
13 | border: 0.5px solid #D8D8D8;
14 | white-space: nowrap;
15 | cursor: pointer;
16 | ${props => props.theme.mixin.boxShadow};
17 |
18 | &:last-child {
19 | margin-right: 0;
20 | }
21 |
22 | &.active {
23 | color: #fff;
24 | background-color: ${props => props.theme.color.secondaryColor};
25 | }
26 | }
27 | `
--------------------------------------------------------------------------------
/src/hooks/index.js:
--------------------------------------------------------------------------------
1 | import useScrollTop from "./useScrollTop";
2 |
3 | export {
4 | useScrollTop
5 | }
6 |
--------------------------------------------------------------------------------
/src/hooks/useScrollPosition.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { throttle } from "underscore"
3 |
4 | export function useScrollPosition() {
5 | const [scrollX, setScrollX] = useState(0)
6 | const [scrollY, setScrollY] = useState(0)
7 |
8 | useEffect(() => {
9 | const handleScroll = throttle(function() {
10 | setScrollX(window.scrollX)
11 | setScrollY(window.scrollY)
12 | }, 100)
13 | window.addEventListener("scroll", handleScroll)
14 | return () => {
15 | window.removeEventListener("scroll", handleScroll)
16 | }
17 | }, [])
18 |
19 | return { scrollX, scrollY }
20 | }
--------------------------------------------------------------------------------
/src/hooks/useScrollTop.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { useLocation } from "react-router-dom";
3 |
4 | export default function useScrollTop() {
5 | const { pathname } = useLocation()
6 | useEffect(() => {
7 | window.scrollTo(0, 0)
8 | }, [pathname])
9 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import { HashRouter } from "react-router-dom"
4 | import { Provider } from "react-redux"
5 | import { ThemeProvider } from "styled-components"
6 | import theme from "./assets/theme"
7 | import App from './App';
8 | import "normalize.css"
9 | import "@/assets/css/index.less"
10 | import "antd/dist/antd.less"
11 | import store from './store';
12 |
13 | const root = ReactDOM.createRoot(document.getElementById('root'));
14 | root.render(
15 |
16 | loading...}>
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | );
25 |
26 |
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Navigate } from "react-router-dom"
3 | const Home = React.lazy(() => import("../views/home"))
4 | // import Home from "@/views/home"
5 | const Entire = React.lazy(() => import("../views/entire"))
6 | const Detail = React.lazy(() => import("../views/detail"))
7 |
8 | const routes = [
9 | {
10 | path: "/",
11 | element:
12 | },
13 | {
14 | path: "/home",
15 | element:
16 | },
17 | {
18 | path: "/entire",
19 | element:
20 | },
21 | {
22 | path: "/detail",
23 | element:
24 | }
25 | ]
26 |
27 | export default routes
28 |
--------------------------------------------------------------------------------
/src/services/index.js:
--------------------------------------------------------------------------------
1 | import hyRequest from "./request"
2 |
3 |
4 | export default hyRequest
5 |
--------------------------------------------------------------------------------
/src/services/modules/entire.js:
--------------------------------------------------------------------------------
1 | import hyRequest from "..";
2 |
3 | export function getEntireRoomList(offset, size = 20) {
4 | return hyRequest.get({
5 | url: "/entire/list",
6 | params: {
7 | offset,
8 | size
9 | }
10 | })
11 | }
12 |
--------------------------------------------------------------------------------
/src/services/modules/home.js:
--------------------------------------------------------------------------------
1 | import hyRequest from "..";
2 |
3 | export function getHomeDiscountData() {
4 | return hyRequest.get({
5 | url: "/home/discount"
6 | })
7 | }
8 |
9 | export function getHomeHotRecommendData() {
10 | return hyRequest.get({
11 | url: "/home/hotrecommenddest"
12 | })
13 | }
14 |
15 | export function getHomeHighScoreData() {
16 | return hyRequest.get({
17 | url: "/home/highscore"
18 | })
19 | }
20 |
21 | export function getHomeGoodPriceData() {
22 | return hyRequest.get({
23 | url: "/home/goodprice"
24 | })
25 | }
26 |
27 | export function getHomePlusData() {
28 | return hyRequest.get({
29 | url: "/home/plus"
30 | })
31 | }
32 |
33 | export function getHomeLongforData() {
34 | return hyRequest.get({
35 | url: "/home/longfor"
36 | })
37 | }
38 |
--------------------------------------------------------------------------------
/src/services/request/config.js:
--------------------------------------------------------------------------------
1 | export const BASE_URL = "http://codercba.com:1888/airbnb/api"
2 | export const TIMEOUT = 10000
3 |
--------------------------------------------------------------------------------
/src/services/request/index.js:
--------------------------------------------------------------------------------
1 | import axios from "axios"
2 | import { BASE_URL, TIMEOUT } from "./config"
3 |
4 | class HYRequest {
5 | constructor(baseURL, timeout = 10000) {
6 | this.instance = axios.create({ baseURL, timeout })
7 |
8 | this.instance.interceptors.response.use((res) => {
9 | return res.data
10 | }, err => {
11 | return err
12 | })
13 | }
14 |
15 | request(config) {
16 | return this.instance.request(config)
17 | }
18 |
19 | get(config) {
20 | return this.request({ ...config, method: "get" })
21 | }
22 |
23 | post(config) {
24 | return this.request({ ...config, method: "post" })
25 | }
26 | }
27 |
28 | export default new HYRequest(BASE_URL, TIMEOUT)
29 |
--------------------------------------------------------------------------------
/src/store/features/detail.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit"
2 |
3 | const detailSlice = createSlice({
4 | name: "detail",
5 | initialState: {
6 | detailInfo: {
7 | "_id": "63043046432f9033d454117e",
8 | "id": "47961247",
9 | "picture_url": "https://z1.muscache.cn/im/pictures/c70bced4-e764-4967-bfd0-ab30f013278b.jpg?aki_policy=large",
10 | "picture_urls": [
11 | "https://z1.muscache.cn/im/pictures/c70bced4-e764-4967-bfd0-ab30f013278b.jpg?aki_policy=large",
12 | "https://z1.muscache.cn/im/pictures/ac22a9c3-84b3-405c-84d5-f37d73927a03.jpg?aki_policy=large",
13 | "https://z1.muscache.cn/im/pictures/ca927574-cfdf-4c7e-a8f2-c07bb89bd397.jpg?aki_policy=large",
14 | "https://z1.muscache.cn/im/pictures/8eebbae5-16a2-48c0-8796-469b2cf88779.jpg?aki_policy=large",
15 | "https://z1.muscache.cn/im/pictures/af00929d-7b8f-442b-ba23-fe98e122e6bb.jpg?aki_policy=large",
16 | "https://z1.muscache.cn/im/pictures/f6ee473c-bcb7-4327-a10f-8b2226c39b10.jpg?aki_policy=large",
17 | "https://z1.muscache.cn/im/pictures/bc52d349-05ba-487b-8480-1b6c8138a327.jpg?aki_policy=large",
18 | "https://z1.muscache.cn/im/pictures/a719057c-c675-428e-8f91-818cbe242d5f.jpg?aki_policy=large",
19 | "https://z1.muscache.cn/im/pictures/4533b0fa-3e03-424f-8829-24d70df74ca1.jpg?aki_policy=large",
20 | "https://z1.muscache.cn/im/pictures/49652a6c-9c35-4b1c-b9e3-de8a9303d0ed.jpg?aki_policy=large",
21 | "https://z1.muscache.cn/im/pictures/a9abc305-5f70-47db-8f4f-a01c9715e954.jpg?aki_policy=large",
22 | "https://z1.muscache.cn/im/pictures/9774321c-f3fb-44be-b58a-908cd2eb3e6d.jpg?aki_policy=large",
23 | "https://z1.muscache.cn/im/pictures/a8a4ee46-75a4-44a6-8633-ac222d04e992.jpg?aki_policy=large",
24 | "https://z1.muscache.cn/im/pictures/aee52e41-cb3d-4db7-b1c8-7b2eb489869d.jpg?aki_policy=large",
25 | "https://z1.muscache.cn/im/pictures/0e608f8e-5ece-4893-9449-d8da72e28ae1.jpg?aki_policy=large",
26 | "https://z1.muscache.cn/im/pictures/da7b67b3-3402-4bd8-8010-58bd6d937208.jpg?aki_policy=large",
27 | "https://z1.muscache.cn/im/pictures/a718197a-2b2b-469b-9581-abec3b88c333.jpg?aki_policy=large",
28 | "https://z1.muscache.cn/im/pictures/f2712cce-f725-4939-bc66-99f39f895fa4.jpg?aki_policy=large",
29 | "https://z1.muscache.cn/im/pictures/0ad7619f-af93-436a-ab14-db49d1f6a8bd.jpg?aki_policy=large",
30 | "https://z1.muscache.cn/im/pictures/93014d7a-e0ca-4393-8279-c8da7354be34.jpg?aki_policy=large",
31 | "https://z1.muscache.cn/im/pictures/23476730-e127-4d12-a78b-ea28b3dcbf05.jpg?aki_policy=large"
32 | ],
33 | "verify_info": {
34 | "messages": [
35 | "整套公寓型住宅",
36 | "1室1卫1床"
37 | ],
38 | "text_color": "#767676"
39 | },
40 | "name": "【大浴缸投影房】北京路步行街/6号线地铁",
41 | "price": 426,
42 | "price_format": "¥426",
43 | "star_rating": 5,
44 | "star_rating_color": "#FF5A5F",
45 | "reviews_count": 50,
46 | "reviews": [
47 | {
48 | "comments": "民宿位置很好,出去转个弯就是北京路商业街,很方便。房间的投影是有会员的,可以看很多影片!ps.一定要记得关好门窗!不然晚上有蚊子骚扰😭",
49 | "created_at": "2022-05-27T00:00:00Z",
50 | "is_translated": false,
51 | "localized_date": "2022年5月",
52 | "reviewer_image_url": "https://a0.muscache.com/im/pictures/user/1f61f128-ac6a-4b4c-b936-1ea12a7d8491.jpg?aki_policy=x_medium",
53 | "review_id": 635694717024855900
54 | }
55 | ],
56 | "bottom_info": null,
57 | "lat": 23.12157,
58 | "lng": 113.27195,
59 | "image_url": "/moreitems/d57fab90263f0d681e3cb6b2c9cf8001.jpg"
60 | }
61 | },
62 | reducers: {
63 | changeDetailInfoActon(state, { payload }) {
64 | state.detailInfo = payload
65 | }
66 | }
67 | })
68 |
69 | export const { changeDetailInfoActon } = detailSlice.actions
70 | export default detailSlice.reducer
71 |
--------------------------------------------------------------------------------
/src/store/features/entire.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit"
2 |
3 | const entireSlice = createSlice({
4 | name: "entire",
5 | initialState: {
6 | name: "coderwhy"
7 | },
8 | reducers: {
9 | changeNameAction(state, { payload }) {
10 | state.name = payload
11 | }
12 | }
13 | })
14 |
15 | export const { changeNameAction } = entireSlice.actions
16 |
17 | export default entireSlice.reducer
18 |
--------------------------------------------------------------------------------
/src/store/features/entire/actionCreators.js:
--------------------------------------------------------------------------------
1 | import { getEntireRoomList } from "@/services/modules/entire";
2 | import * as actionTypes from "./constants";
3 |
4 | export const changeLoadingAction = (isLoading) => ({
5 | type: actionTypes.CHANGE_LOADING,
6 | isLoading
7 | })
8 |
9 | export const changeCurrentPageAction = (currentPage) => ({
10 | type: actionTypes.CHANGE_CURRENT_PAGE,
11 | currentPage
12 | })
13 |
14 | export const changeTotalCountAction = (totalCount) => ({
15 | type: actionTypes.CHANGE_TOTAL_COUNT,
16 | totalCount
17 | })
18 |
19 | export const changeRoomListAction = (roomList) => ({
20 | type: actionTypes.CHANGE_ROOM_LIST,
21 | roomList
22 | })
23 |
24 |
25 | export const fetchEntireDataAction = (page = 0) => {
26 | return async dispatch => {
27 | // 设置isLoading
28 | dispatch(changeLoadingAction(true))
29 |
30 | const res = await getEntireRoomList(page * 20)
31 | dispatch(changeLoadingAction(false))
32 | // 保存数据
33 | dispatch(changeCurrentPageAction(page))
34 | dispatch(changeTotalCountAction(res.totalCount))
35 | dispatch(changeRoomListAction(res.list))
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/store/features/entire/constants.js:
--------------------------------------------------------------------------------
1 | export const CHANGE_CURRENT_PAGE = "entire/change_current_page"
2 | export const CHANGE_TOTAL_COUNT = "entire/change_total_count"
3 | export const CHANGE_ROOM_LIST = "entire/change_room_list"
4 | export const CHANGE_LOADING = 'entire/loading'
5 |
--------------------------------------------------------------------------------
/src/store/features/entire/index.js:
--------------------------------------------------------------------------------
1 | import reducer from "./reducer";
2 |
3 | export default reducer
4 |
--------------------------------------------------------------------------------
/src/store/features/entire/reducer.js:
--------------------------------------------------------------------------------
1 | import * as actionTypes from "./constants"
2 |
3 | const initialState = {
4 | isLoading: false,
5 | currentPage: 0,
6 | roomList: [],
7 | totalCount: 0
8 | }
9 |
10 |
11 | function reducer(state = initialState, action) {
12 | switch(action.type) {
13 | case actionTypes.CHANGE_LOADING:
14 | return { ...state, isLoading: action.isLoading }
15 | case actionTypes.CHANGE_CURRENT_PAGE:
16 | return { ...state, currentPage: action.currentPage }
17 | case actionTypes.CHANGE_TOTAL_COUNT:
18 | return { ...state, totalCount: action.totalCount }
19 | case actionTypes.CHANGE_ROOM_LIST:
20 | return { ...state, roomList: action.roomList }
21 | default:
22 | return state
23 | }
24 | }
25 |
26 | export default reducer
27 |
--------------------------------------------------------------------------------
/src/store/features/home.js:
--------------------------------------------------------------------------------
1 | import { getHomeDiscountData, getHomeGoodPriceData, getHomeHighScoreData, getHomeHotRecommendData, getHomeLongforData, getHomePlusData } from '@/services/modules/home'
2 | import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
3 |
4 | const fetchHomeAllDataAction = createAsyncThunk("fetchData", (payload, { dispatch }) => {
5 | // 1.发送第一个网络请求
6 | getHomeDiscountData().then(res => {
7 | dispatch(changeDiscountInfoAction(res))
8 | })
9 |
10 | getHomeHotRecommendData().then(res => {
11 | dispatch(changeHotRecommendInfoAction(res))
12 | })
13 |
14 | getHomeHighScoreData().then(res => {
15 | dispatch(changeHighScoreInfoAction(res))
16 | })
17 |
18 | getHomeGoodPriceData().then(res => {
19 | dispatch(changeGoodPriceInfoAction(res))
20 | })
21 |
22 | getHomePlusData().then(res => {
23 | dispatch(changePlusInfoAction(res))
24 | })
25 |
26 | getHomeLongforData().then(res => {
27 | dispatch(changeLongforInfoAction(res))
28 | })
29 | })
30 |
31 | const homeSlice = createSlice({
32 | name: "home",
33 | initialState: {
34 | discountInfo: {},
35 | hotRecommendInfo: {},
36 | highScoreInfo: {},
37 | goodPriceInfo: {},
38 | plusInfo: {},
39 | longForInfo: {}
40 | },
41 | reducers: {
42 | changeDiscountInfoAction(state, { payload }) {
43 | state.discountInfo = payload
44 | },
45 | changeHotRecommendInfoAction(state, { payload }) {
46 | state.hotRecommendInfo = payload
47 | },
48 | changeHighScoreInfoAction(state, { payload }) {
49 | state.highScoreInfo = payload
50 | },
51 | changeGoodPriceInfoAction(state, { payload }) {
52 | state.goodPriceInfo = payload
53 | },
54 | changePlusInfoAction(state, { payload }) {
55 | state.plusInfo = payload
56 | },
57 | changeLongforInfoAction(state, { payload }) {
58 | state.longForInfo = payload
59 | }
60 | }
61 | })
62 |
63 | export default homeSlice.reducer
64 | export const {
65 | changeDiscountInfoAction,
66 | changeHotRecommendInfoAction,
67 | changeHighScoreInfoAction ,
68 | changeGoodPriceInfoAction,
69 | changePlusInfoAction,
70 | changeLongforInfoAction
71 | } = homeSlice.actions
72 | export { fetchHomeAllDataAction }
73 |
--------------------------------------------------------------------------------
/src/store/features/home/actionCreators.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coderwhy/hy-react-airbnb/5043d75c3a869508ef4466da933546c73eda8132/src/store/features/home/actionCreators.js
--------------------------------------------------------------------------------
/src/store/features/home/constants.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coderwhy/hy-react-airbnb/5043d75c3a869508ef4466da933546c73eda8132/src/store/features/home/constants.js
--------------------------------------------------------------------------------
/src/store/features/home/index.js:
--------------------------------------------------------------------------------
1 | import reducer from "./reducer";
2 |
3 | export default reducer
4 |
5 |
--------------------------------------------------------------------------------
/src/store/features/home/reducer.js:
--------------------------------------------------------------------------------
1 | const initialState = {
2 | counter: 100
3 | }
4 |
5 | function reducer(state = initialState, action) {
6 | switch (action.type) {
7 | case "increment":
8 | return {...state, counter: state.counter + 1}
9 | default:
10 | return state
11 | }
12 | }
13 |
14 |
15 | export default reducer
16 |
--------------------------------------------------------------------------------
/src/store/features/main.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit"
2 |
3 | const mainSlice = createSlice({
4 | name: "main",
5 | initialState: {
6 | headerConfig: {
7 | isFixed: false,
8 | isHome: false
9 | }
10 | },
11 | reducers: {
12 | changeHeaderConfigAction(state, { payload }) {
13 | state.headerConfig = payload
14 | }
15 | }
16 | })
17 |
18 | export const {
19 | changeHeaderConfigAction
20 | } = mainSlice.actions
21 | export default mainSlice.reducer
22 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit"
2 | import homeReducer from "./features/home"
3 | // import entireRedducer from "./features/entire"
4 | import entireReducer from "./features/entire/index"
5 | import detailReducer from "./features/detail"
6 | import mainReducer from "./features/main"
7 |
8 | const store = configureStore({
9 | reducer: {
10 | home: homeReducer,
11 | entire: entireReducer,
12 | detail: detailReducer,
13 | main: mainReducer
14 | }
15 | })
16 |
17 | export default store
18 |
--------------------------------------------------------------------------------
/src/views/detail/c-cpns/detail-pictures/index.jsx:
--------------------------------------------------------------------------------
1 | import PictureBrowser from '@/base-ui/picture-browser'
2 | import PropTypes from 'prop-types'
3 | import React, { memo, useState } from 'react'
4 | import { PicturesWrapper } from './style'
5 |
6 | const DetailPictures = memo((props) => {
7 | const { pictureUrls } = props
8 | const [showBrowser, setShowBrowser] = useState(false)
9 |
10 | function showBrowserHandle() {
11 | setShowBrowser(true)
12 | }
13 |
14 | function handleCloseClick() {
15 | setShowBrowser(false)
16 | }
17 |
18 | return (
19 |
20 |
21 |
22 |
23 |

24 |
25 |
26 |
27 |
28 | {
29 | pictureUrls?.slice(1, 5).map((item, index) => {
30 | return (
31 |
32 |

33 |
34 |
35 | )
36 | })
37 | }
38 |
39 |
40 | 查看照片
41 | { showBrowser && }
42 |
43 | )
44 | })
45 |
46 | DetailPictures.propTypes = {
47 | pictureUrls: PropTypes.array
48 | }
49 |
50 | export default DetailPictures
--------------------------------------------------------------------------------
/src/views/detail/c-cpns/detail-pictures/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const PicturesWrapper = styled.div`
4 | position: relative;
5 |
6 | > .top {
7 | display: flex;
8 | height: 600px;
9 | background-color: #000;
10 |
11 | .cover {
12 | opacity: 1 !important;
13 | }
14 |
15 | .item:hover {
16 | .cover {
17 | opacity: 0 !important;
18 | }
19 | }
20 | }
21 |
22 | .left, .right {
23 | width: 50%;
24 | height: 100%;
25 |
26 | .item {
27 | position: relative;
28 | height: 100%;
29 | overflow: hidden;
30 | cursor: pointer;
31 |
32 | img {
33 | width: 100%;
34 | height: 100%;
35 | object-fit: cover;
36 |
37 | transition: transform 0.3s ease-in;
38 | }
39 |
40 | .cover {
41 | position: absolute;
42 | left: 0;
43 | right: 0;
44 | top: 0;
45 | bottom: 0;
46 | background-color: rgba(0,0,0,.2);
47 | opacity: 0;
48 | transition: opacity 200ms ease;
49 | }
50 |
51 | &:hover {
52 | img {
53 | transform: scale(1.1);
54 | }
55 | }
56 | }
57 | }
58 |
59 | .right {
60 | display: flex;
61 | flex-wrap: wrap;
62 |
63 | .item {
64 | width: 50%;
65 | height: 50%;
66 | box-sizing: border-box;
67 | border: 1px solid #000;
68 | }
69 | }
70 |
71 | .show-btn {
72 | position: absolute;
73 | z-index: 99;
74 | right: 15px;
75 | bottom: 15px;
76 | line-height: 22px;
77 | padding: 6px 15px;
78 | border-radius: 4px;
79 | background-color: #fff;
80 | cursor: pointer;
81 | }
82 | `
--------------------------------------------------------------------------------
/src/views/detail/index.jsx:
--------------------------------------------------------------------------------
1 | import { changeHeaderConfigAction } from '@/store/features/main'
2 | import React, { memo, useEffect } from 'react'
3 | import { shallowEqual, useDispatch, useSelector } from 'react-redux'
4 | import DetailPictures from './c-cpns/detail-pictures'
5 | import { DetailWrapper } from './style'
6 |
7 | const Detail = memo((props) => {
8 | const { detailInfo } = useSelector((state) => ({
9 | detailInfo: state.detail.detailInfo
10 | }), shallowEqual)
11 | const dispatch = useDispatch()
12 | useEffect(() => {
13 | dispatch(changeHeaderConfigAction({ isFixed: false, isHome: false }))
14 | }, [dispatch])
15 |
16 | return (
17 |
18 |
19 |
20 | )
21 | })
22 |
23 | Detail.propTypes = {}
24 |
25 | export default Detail
--------------------------------------------------------------------------------
/src/views/detail/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const DetailWrapper = styled.div`
4 | .demo01 {
5 | width: 100px;
6 |
7 | button {
8 | margin: 0 8px;
9 | }
10 | }
11 | `
--------------------------------------------------------------------------------
/src/views/entire/c-cpns/entire-filter/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useState } from 'react'
2 | import filterData from "@/assets/data/filter_data.json"
3 | import { FilterWrapper } from './style'
4 | import classNames from 'classnames'
5 |
6 | const EntireFilter = memo(() => {
7 | const [selectItems, setSelectItems] = useState([])
8 |
9 | function selectItemHandle(item) {
10 | const newItems = [...selectItems]
11 | if (newItems.includes(item)) {
12 | const index = newItems.findIndex(name => item === name)
13 | newItems.splice(index, 1)
14 | } else {
15 | newItems.push(item)
16 | }
17 | setSelectItems(newItems)
18 | }
19 |
20 | return (
21 |
22 |
23 | {
24 | filterData.map(item => {
25 | return (
26 |
selectItemHandle(item)}
30 | >
31 | {item}
32 |
33 | )
34 | })
35 | }
36 |
37 |
38 | )
39 | })
40 |
41 | export default EntireFilter
42 |
--------------------------------------------------------------------------------
/src/views/entire/c-cpns/entire-filter/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const FilterWrapper =styled.div`
4 | position: fixed;
5 | z-index: 9;
6 | left: 0;
7 | right: 0;
8 | top: 80px;
9 |
10 | display: flex;
11 | align-items: center;
12 | height: 48px;
13 | padding-left: 16px;
14 | border-bottom: 1px solid #f2f2f2;
15 | background-color: #fff;
16 |
17 | .filter {
18 | display: flex;
19 |
20 | .item {
21 | margin: 0 4px 0 8px;
22 | padding: 6px 12px;
23 | border: 1px solid #dce0e0;
24 | border-radius: 4px;
25 | color: #484848;
26 | cursor: pointer;
27 |
28 | &.active {
29 | background: #008489;
30 | border: 1px solid #008489;
31 | color: #ffffff;
32 | }
33 | }
34 | }
35 | `
--------------------------------------------------------------------------------
/src/views/entire/c-cpns/entire-pagination/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import Pagination from '@mui/material/Pagination';
3 |
4 | import { PaginationWrapper } from './style'
5 | import { useDispatch, useSelector } from 'react-redux';
6 | import { fetchEntireDataAction } from '@/store/features/entire/actionCreators';
7 |
8 | const EntirePagination = memo(() => {
9 | const { currentPage, totalCount } = useSelector((state) => ({
10 | currentPage: state.entire.currentPage,
11 | totalCount: state.entire.totalCount
12 | }))
13 |
14 | const count = Math.ceil(totalCount / 20)
15 | const start = currentPage * 20 + 1
16 | const end = (currentPage + 1) * 20
17 |
18 | const dispatch = useDispatch()
19 | function pageChangeHandle(event, newPage) {
20 | window.scrollTo(0, 0)
21 | dispatch(fetchEntireDataAction(newPage-1))
22 | }
23 |
24 | return (
25 |
26 |
27 |
28 |
29 | 第 {start} - {end} 个房源, 共超过 {totalCount} 个
30 |
31 |
32 |
33 | )
34 | })
35 |
36 | export default EntirePagination
--------------------------------------------------------------------------------
/src/views/entire/c-cpns/entire-pagination/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 |
4 | export const PaginationWrapper = styled.div`
5 | display: flex;
6 | justify-content: center;
7 | margin-top: 30px;
8 |
9 | .page-info {
10 | text-align: center;
11 |
12 | .info {
13 | margin-top: 20px;
14 | }
15 | }
16 |
17 | .MuiPaginationItem-icon {
18 | font-size: 20px;
19 | }
20 |
21 | .MuiPaginationItem-page{
22 | margin: 0 9px;
23 |
24 | &:hover {
25 | text-decoration: underline;
26 | }
27 | }
28 |
29 | .MuiPaginationItem-page.Mui-selected {
30 | background-color: #222;
31 | color: #fff;
32 |
33 | &:hover {
34 | background-color: #222;
35 | }
36 | }
37 | `
--------------------------------------------------------------------------------
/src/views/entire/c-cpns/entire-rooms/index.jsx:
--------------------------------------------------------------------------------
1 | import RoomItem from '@/components/room-item'
2 | import { changeDetailInfoActon } from '@/store/features/detail'
3 | import React, { memo } from 'react'
4 | import { shallowEqual, useDispatch, useSelector } from 'react-redux'
5 | import { useNavigate } from 'react-router-dom'
6 | import { RoomsWrapper } from './style'
7 |
8 | const EntireRooms = memo(() => {
9 | const { roomList, isLoading } = useSelector((state) => ({
10 | roomList: state.entire.roomList,
11 | isLoading: state.entire.isLoading
12 | }), shallowEqual)
13 |
14 | const navitate = useNavigate()
15 | const dispatch = useDispatch()
16 | function handleItemClick(item) {
17 | navitate("/detail")
18 | dispatch(changeDetailInfoActon(item))
19 | }
20 |
21 | return (
22 |
23 |
24 | {
25 | roomList.map((item, index) => {
26 | return (
27 | handleItemClick(item)}
32 | />
33 | )
34 | })
35 | }
36 |
37 | { isLoading && }
38 |
39 | )
40 | })
41 |
42 | export default EntireRooms
43 |
--------------------------------------------------------------------------------
/src/views/entire/c-cpns/entire-rooms/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 |
3 | export const RoomsWrapper = styled.div`
4 | position: relative;
5 | padding: 30px 20px;
6 |
7 | .list {
8 | display: flex;
9 | flex-wrap: wrap;
10 | }
11 |
12 | > .cover {
13 | position: absolute;
14 | left: 0;
15 | right: 0;
16 | top: 0;
17 | bottom: 0;
18 | background-color: rgba(255,255,255, .8);
19 | }
20 | `
--------------------------------------------------------------------------------
/src/views/entire/index.jsx:
--------------------------------------------------------------------------------
1 | import { fetchEntireDataAction } from '@/store/features/entire/actionCreators'
2 | import { changeHeaderConfigAction } from '@/store/features/main'
3 | import React, { memo, useEffect } from 'react'
4 | import { useDispatch } from 'react-redux'
5 | import EntireFilter from './c-cpns/entire-filter'
6 | import EntirePagination from './c-cpns/entire-pagination'
7 | import EntireRooms from './c-cpns/entire-rooms'
8 | import { EntireWrapper } from './style'
9 |
10 | const Entire = memo((props) => {
11 | const dispatch = useDispatch()
12 | useEffect(() => {
13 | dispatch(fetchEntireDataAction())
14 | dispatch(changeHeaderConfigAction({ isFixed: true, isHome: false }))
15 | }, [dispatch])
16 |
17 | return (
18 |
19 |
20 |
21 |
22 |
23 | )
24 | })
25 |
26 | Entire.propTypes = {}
27 |
28 | export default Entire
29 |
--------------------------------------------------------------------------------
/src/views/entire/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const EntireWrapper = styled.div`
4 | margin-top: 128px;
5 | `
--------------------------------------------------------------------------------
/src/views/home/c-cpns/home-banner/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { BannerWrapper } from './style'
3 |
4 | const HomeBanner = memo(() => {
5 | return (
6 |
7 |
8 |
9 | )
10 | })
11 |
12 | export default HomeBanner
13 |
--------------------------------------------------------------------------------
/src/views/home/c-cpns/home-banner/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const BannerWrapper = styled.div`
4 | height: 529px;
5 | background: url(${require("@/assets/img/cover_01.jpeg")}) center / cover;
6 |
7 | .cover {
8 | position: absolute;
9 | left: 0;
10 | right: 0;
11 | top: 0;
12 | bottom: 0;
13 | background: linear-gradient(to bottom,rgba(0, 0, 0, .3) 0%,rgba(0, 0, 0, .0) 300px,rgba(0, 0, 0, 0) 100%);
14 | }
15 | `
16 |
--------------------------------------------------------------------------------
/src/views/home/c-cpns/home-longfor/index.jsx:
--------------------------------------------------------------------------------
1 | import ScrollView from '@/base-ui/scroll-view'
2 | import LongforItem from '@/components/longfor-item'
3 | import SectionHeader from '@/components/section-header'
4 | import React, { memo } from 'react'
5 | import { LongForWrapper } from './style'
6 |
7 | const HomeLongFor = memo((props) => {
8 | const { title, subtitle, list: longforList = [] } = props.infoData
9 |
10 | return (
11 |
12 |
13 |
14 |
15 | {
16 | longforList.map(item => {
17 | return ()
18 | })
19 | }
20 |
21 |
22 |
23 | )
24 | })
25 |
26 | export default HomeLongFor
--------------------------------------------------------------------------------
/src/views/home/c-cpns/home-longfor/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const LongForWrapper = styled.div`
4 | margin-top: 30px;
5 |
6 | .longfor-list {
7 | margin: 0 -8px;
8 | }
9 | `
--------------------------------------------------------------------------------
/src/views/home/c-cpns/home-section-v1/index.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import React, { memo, useState, useEffect, useCallback } from 'react'
3 | import SectionHeader from '@/components/section-header'
4 | import SectionTabs from '@/components/section-tabs'
5 | import SectionRooms from '@/components/section-rooms'
6 | import SectionFooter from '@/components/section-footer'
7 | import { SectionV1Wrapper } from './style'
8 |
9 | const HomeSectionV1 = memo((props) => {
10 | const { infoData } = props
11 | const { dest_address = [], dest_list = {} } = infoData
12 | const destNames = dest_address.map(item => item.name)
13 | const [roomList, setRoomList] = useState([])
14 | const [name, setName] = useState("")
15 |
16 | /**第一次的设置值 */
17 | useEffect(() => {
18 | const name = Object.keys(infoData.dest_list??{})[0]
19 | if (!name) return
20 | const roomList = infoData.dest_list[name]
21 | setRoomList(roomList)
22 | setName(name)
23 | }, [infoData.dest_list])
24 |
25 | /** 事件监听 */
26 | const tabClickHandle = useCallback(function(index, name) {
27 | setRoomList(dest_list[name])
28 | setName(name)
29 | }, [dest_list])
30 |
31 | return (
32 |
33 |
34 |
35 |
36 |
37 |
38 | )
39 | })
40 |
41 | HomeSectionV1.propTypes = {
42 | infoData: PropTypes.object
43 | }
44 |
45 | export default HomeSectionV1
--------------------------------------------------------------------------------
/src/views/home/c-cpns/home-section-v1/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const SectionV1Wrapper = styled.div`
4 | margin-top: 36px;
5 | `
--------------------------------------------------------------------------------
/src/views/home/c-cpns/home-section-v2/index.jsx:
--------------------------------------------------------------------------------
1 | import SectionFooter from '@/components/section-footer'
2 | import SectionHeader from '@/components/section-header'
3 | import SectionRooms from '@/components/section-rooms'
4 | import React, { memo } from 'react'
5 | import { SectionV2Wrapper } from './style'
6 |
7 | const HomeSectionV2 = memo((props) => {
8 | const { infoData } = props
9 | const { title, subtitle, list: roomList = [] } = infoData
10 |
11 | return (
12 |
13 |
14 |
15 |
16 |
17 | )
18 | })
19 |
20 | export default HomeSectionV2
--------------------------------------------------------------------------------
/src/views/home/c-cpns/home-section-v2/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 |
3 | export const SectionV2Wrapper = styled.div`
4 | margin-top: 50px;
5 | `
--------------------------------------------------------------------------------
/src/views/home/c-cpns/home-section-v3/index.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import React, { memo } from 'react'
3 | import SectionHeader from '@/components/section-header'
4 | import { SectionV3Wrapper } from './style'
5 | import ScrollView from '@/base-ui/scroll-view'
6 | import RoomItem from '@/components/room-item'
7 |
8 | const HomeSectionV3 = memo((props) => {
9 | const { infoData } = props
10 | const { title, subtitle, list: roomList = [] } = infoData
11 |
12 | return (
13 |
14 |
15 |
16 |
17 | {
18 | roomList.map(item => {
19 | return
20 | })
21 | }
22 |
23 |
24 |
25 | )
26 | })
27 |
28 | HomeSectionV3.propTypes = {
29 | infoData: PropTypes.object
30 | }
31 |
32 | export default HomeSectionV3
--------------------------------------------------------------------------------
/src/views/home/c-cpns/home-section-v3/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const SectionV3Wrapper = styled.div`
4 | margin-top: 30px;
5 |
6 | .room-list {
7 | margin: 0 -8px;
8 | }
9 | `
--------------------------------------------------------------------------------
/src/views/home/index.jsx:
--------------------------------------------------------------------------------
1 | import { fetchHomeAllDataAction } from '@/store/features/home'
2 | import { changeHeaderConfigAction } from '@/store/features/main'
3 | import React, { memo, useEffect } from 'react'
4 | import { shallowEqual, useDispatch, useSelector } from 'react-redux'
5 |
6 | import HomeBanner from './c-cpns/home-banner'
7 | import HomeLongFor from './c-cpns/home-longfor'
8 | import HomeSectionV1 from './c-cpns/home-section-v1'
9 | import HomeSectionV2 from './c-cpns/home-section-v2'
10 | import HomeSectionV3 from './c-cpns/home-section-v3'
11 | import { HomeWrapper } from './style'
12 |
13 | const Home = memo((props) => {
14 |
15 | /** 从redux中获取数据 */
16 | const { discountInfo, hotRecommendInfo, highScoreInfo, goodPriceInfo, plusInfo, longForInfo } = useSelector((state) => ({
17 | discountInfo: state.home.discountInfo,
18 | hotRecommendInfo: state.home.hotRecommendInfo,
19 | highScoreInfo: state.home.highScoreInfo,
20 | goodPriceInfo: state.home.goodPriceInfo,
21 | plusInfo: state.home.plusInfo,
22 | longForInfo: state.home.longForInfo
23 | }), shallowEqual)
24 |
25 | /** 派发事件,发送网络请求 */
26 | const dispatch = useDispatch()
27 | useEffect(() => {
28 | dispatch(fetchHomeAllDataAction())
29 | dispatch(changeHeaderConfigAction({ isFixed: true, isHome: true }))
30 | }, [dispatch])
31 |
32 | return (
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | )
45 | })
46 |
47 | Home.propTypes = {}
48 |
49 | export default Home
--------------------------------------------------------------------------------
/src/views/home/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 |
3 | export const HomeWrapper = styled.div`
4 | > .content {
5 | width: 1032px;
6 | margin: 0 auto;
7 | }
8 | `
9 |
--------------------------------------------------------------------------------