├── .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 | //
6 | 7 | //
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 |
43 | 44 |
45 | 46 |
47 |
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 | --------------------------------------------------------------------------------