├── src
├── hooks
│ ├── index.js
│ ├── useScrollTop.js
│ └── useScrollPosition.js
├── views
│ ├── detail
│ │ ├── c-cpns
│ │ │ ├── detail-infos
│ │ │ │ ├── style.js
│ │ │ │ └── index.jsx
│ │ │ └── detail-pictures
│ │ │ │ ├── index.jsx
│ │ │ │ └── style.js
│ │ ├── style.js
│ │ └── index.jsx
│ ├── entire
│ │ ├── style.js
│ │ ├── c-cpns
│ │ │ ├── entire-rooms
│ │ │ │ ├── style.js
│ │ │ │ └── index.jsx
│ │ │ ├── entire-pagination
│ │ │ │ ├── style.js
│ │ │ │ └── index.jsx
│ │ │ └── entire-filter
│ │ │ │ ├── style.js
│ │ │ │ └── index.jsx
│ │ └── index.jsx
│ ├── demo
│ │ ├── style.js
│ │ └── index.jsx
│ └── home
│ │ ├── c-cpns
│ │ ├── home-section-v2
│ │ │ ├── style.js
│ │ │ └── index.jsx
│ │ ├── home-section-v1
│ │ │ ├── style.js
│ │ │ └── index.jsx
│ │ ├── home-longfor
│ │ │ ├── style.js
│ │ │ └── index.jsx
│ │ ├── home-section-v3
│ │ │ ├── style.js
│ │ │ └── index.jsx
│ │ └── home-banner
│ │ │ ├── style.js
│ │ │ └── index.jsx
│ │ ├── style.js
│ │ └── index.jsx
├── utils
│ ├── index.js
│ └── is-empty-object.js
├── store
│ ├── modules
│ │ ├── entire
│ │ │ ├── index.js
│ │ │ ├── constants.js
│ │ │ ├── reducer.js
│ │ │ └── actionCreators.js
│ │ ├── main.js
│ │ ├── home.js
│ │ └── detail.js
│ └── index.js
├── assets
│ ├── img
│ │ └── cover_01.jpeg
│ ├── css
│ │ ├── index.less
│ │ ├── variables.less
│ │ ├── common.less
│ │ └── reset.less
│ ├── data
│ │ ├── filter_data.json
│ │ ├── footer.json
│ │ └── search_titles.json
│ ├── svg
│ │ ├── utils
│ │ │ └── index.js
│ │ ├── icon-more-arrow.jsx
│ │ ├── icon-triangle-arrow-bottom.jsx
│ │ ├── icon-triangle-arrow-top.jsx
│ │ ├── icon_menu.jsx
│ │ ├── icon-arrow-right.jsx
│ │ ├── icon-arrow-left.jsx
│ │ ├── icon-search-bar.jsx
│ │ ├── icon-close.jsx
│ │ ├── icon_avatar.jsx
│ │ ├── icon_global.jsx
│ │ └── icon_logo.jsx
│ └── theme
│ │ └── index.js
├── services
│ ├── index.js
│ ├── request
│ │ ├── config.js
│ │ └── index.js
│ └── modules
│ │ ├── entire.js
│ │ └── home.js
├── components
│ ├── section-rooms
│ │ ├── style.js
│ │ └── index.jsx
│ ├── section-header
│ │ ├── style.js
│ │ └── index.jsx
│ ├── app-header
│ │ ├── c-cpns
│ │ │ ├── header-left
│ │ │ │ ├── style.js
│ │ │ │ └── index.jsx
│ │ │ ├── header-center
│ │ │ │ ├── c-cpns
│ │ │ │ │ ├── search-tabs
│ │ │ │ │ │ ├── style.js
│ │ │ │ │ │ └── index.jsx
│ │ │ │ │ └── search-sections
│ │ │ │ │ │ ├── index.jsx
│ │ │ │ │ │ └── style.js
│ │ │ │ ├── index.jsx
│ │ │ │ └── style.js
│ │ │ └── header-right
│ │ │ │ ├── index.jsx
│ │ │ │ └── style.js
│ │ ├── style.js
│ │ └── index.jsx
│ ├── section-footer
│ │ ├── style.js
│ │ └── index.jsx
│ ├── section-tabs
│ │ ├── style.js
│ │ └── index.jsx
│ ├── longfor-item
│ │ ├── index.jsx
│ │ └── style.js
│ ├── app-footer
│ │ ├── style.js
│ │ └── index.jsx
│ └── room-item
│ │ ├── style.js
│ │ └── index.jsx
├── base-ui
│ ├── indicator
│ │ ├── style.js
│ │ └── index.jsx
│ ├── scroll-view
│ │ ├── style.js
│ │ └── index.jsx
│ └── picture-browser
│ │ ├── style.js
│ │ └── index.jsx
├── App.jsx
├── index.js
└── router
│ └── index.js
├── public
├── favicon.ico
├── logo192.png
├── logo512.png
├── robots.txt
├── manifest.json
└── index.html
├── jsconfig.json
├── .gitignore
├── craco.config.js
├── package.json
└── README.md
/src/hooks/index.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/views/detail/c-cpns/detail-infos/style.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | export * from "./is-empty-object"
2 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coderwhy/hy_airbnb/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coderwhy/hy_airbnb/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coderwhy/hy_airbnb/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/store/modules/entire/index.js:
--------------------------------------------------------------------------------
1 | import reducer from "./reducer";
2 |
3 | export default reducer
--------------------------------------------------------------------------------
/src/utils/is-empty-object.js:
--------------------------------------------------------------------------------
1 | export function isEmptyO(obj) {
2 | return !!Object.keys(obj).length
3 | }
--------------------------------------------------------------------------------
/src/assets/img/cover_01.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coderwhy/hy_airbnb/HEAD/src/assets/img/cover_01.jpeg
--------------------------------------------------------------------------------
/src/assets/css/index.less:
--------------------------------------------------------------------------------
1 | @import "./reset.less";
2 | @import "./common.less";
3 | // @import '~antd/dist/antd.less';
4 |
--------------------------------------------------------------------------------
/src/services/index.js:
--------------------------------------------------------------------------------
1 | import hyRequest from "./request"
2 |
3 | export default hyRequest
4 | export * from "./modules/home"
5 |
--------------------------------------------------------------------------------
/src/views/entire/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const EntireWrapper = styled.div`
4 |
5 | `
--------------------------------------------------------------------------------
/src/views/detail/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 |
4 | export const DetailWrapper = styled.div`
5 |
6 | `
--------------------------------------------------------------------------------
/src/services/request/config.js:
--------------------------------------------------------------------------------
1 | export const BASE_URL = "http://codercba.com:1888/airbnb/api"
2 | export const TIMEOUT = 10000
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/css/variables.less:
--------------------------------------------------------------------------------
1 | @textColor: #484848;
2 | @textColorSecondary: #222;
3 |
4 |
5 | :root {
6 | --primary-color: #ff385c;
7 | }
8 |
--------------------------------------------------------------------------------
/src/views/demo/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 |
3 | export const DemoWrapper = styled.div`
4 | .list {
5 | width: 100px;
6 | }
7 | `
--------------------------------------------------------------------------------
/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: 30px;
5 | `
--------------------------------------------------------------------------------
/src/assets/data/filter_data.json:
--------------------------------------------------------------------------------
1 | [
2 | "人数",
3 | "可免费取消",
4 | "房源类型",
5 | "价格",
6 | "位置区域",
7 | "闪定",
8 | "卧室/床数",
9 | "促销/优惠",
10 | "更多筛选条件"
11 | ]
--------------------------------------------------------------------------------
/src/views/home/c-cpns/home-section-v1/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 |
4 | export const SectionV1Wrapper = styled.div`
5 | margin-top: 30px;
6 | `
7 |
--------------------------------------------------------------------------------
/src/components/section-rooms/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 |
3 | export const RoomsWrapper = styled.div`
4 | display: flex;
5 | flex-wrap: wrap;
6 | margin: 0 -8px;
7 | `
--------------------------------------------------------------------------------
/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 | color: #484848;
5 | line-height: 1.15;
6 | }
--------------------------------------------------------------------------------
/src/views/detail/c-cpns/detail-infos/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 |
3 | const DetailInfos = memo(() => {
4 | return (
5 |
DetailInfos
6 | )
7 | })
8 |
9 | export default DetailInfos
--------------------------------------------------------------------------------
/src/views/home/c-cpns/home-longfor/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 |
4 | export const LongforWrapper = styled.div`
5 | margin-top: 30px;
6 |
7 | .longfor-list {
8 | display: flex;
9 | margin: 0 -8px;
10 | }
11 | `
--------------------------------------------------------------------------------
/src/views/home/c-cpns/home-section-v3/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 |
4 | export const SectionV3Wrapper = styled.div`
5 | .room-list {
6 | margin: 0 -8px;
7 | /* display: flex;
8 | flex-wrap: wrap; */
9 | }
10 | `
--------------------------------------------------------------------------------
/src/views/home/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 |
4 | export const HomeWrapper = styled.div`
5 | > .content {
6 | width: 1032px;
7 | margin: 0 auto;
8 |
9 | > div {
10 | margin-top: 30px;
11 | }
12 | }
13 | `
14 |
--------------------------------------------------------------------------------
/src/services/modules/entire.js:
--------------------------------------------------------------------------------
1 | import hyRequest from "..";
2 |
3 | export function getEntireRoomList(offset = 0, size = 20) {
4 | return hyRequest.get({
5 | url: "entire/list",
6 | params: {
7 | offset,
8 | size
9 | }
10 | })
11 | }
12 |
--------------------------------------------------------------------------------
/src/store/modules/entire/constants.js:
--------------------------------------------------------------------------------
1 | export const CHANGE_CURRENT_PAGE = "entire/change_current_page"
2 | export const CHANGE_ROOM_LIST = "entire/change_room_list"
3 | export const CHANGE_TOTAL_COUNT = "entire/change_total_count"
4 | export const CHANGE_IS_LOADING = "entire/change_is_loading"
5 |
--------------------------------------------------------------------------------
/src/views/home/c-cpns/home-banner/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | // import coverImg from "@/assets/img/cover_01.jpeg"
3 |
4 | export const BannerWrapper = styled.div`
5 | height: 529px;
6 | background: url(${require("@/assets/img/cover_01.jpeg")}) center/cover;
7 | `
8 |
--------------------------------------------------------------------------------
/src/hooks/useScrollTop.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react"
2 | import { useLocation } from "react-router-dom"
3 |
4 | export default function useScrollTop() {
5 | const location = useLocation()
6 | useEffect(() => {
7 | window.scrollTo(0, 0)
8 | }, [location.pathname])
9 | }
10 |
--------------------------------------------------------------------------------
/src/base-ui/indicator/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 |
4 | export const IndicatorWrapper = styled.div`
5 | overflow: hidden;
6 |
7 | .i-content {
8 | display: flex;
9 | position: relative;
10 | transition: transform 200ms ease;
11 |
12 | > * {
13 | flex-shrink: 0;
14 | }
15 | }
16 | `
--------------------------------------------------------------------------------
/src/components/section-header/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 |
4 | export const HeaderWrapper = styled.div`
5 | color: #222;
6 |
7 | .title {
8 | font-size: 22px;
9 | font-weight: 700;
10 | margin-bottom: 16px;
11 | }
12 |
13 | .subtitle {
14 | font-size: 16px;
15 | margin-bottom: 20px;
16 | }
17 | `
--------------------------------------------------------------------------------
/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 | align-items: center;
7 | color: ${props => props.theme.isAlpha ? "#fff": props.theme.color.primaryColor};
8 |
9 | .logo {
10 | margin-left: 24px;
11 | cursor: pointer;
12 | }
13 | `
14 |
--------------------------------------------------------------------------------
/src/views/home/c-cpns/home-banner/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { BannerWrapper } from './style'
3 | // import coverImg from "@/assets/img/cover_01.jpeg"
4 |
5 | const HomeBanner = memo(() => {
6 | return (
7 |
8 | {/*
*/}
9 |
10 | )
11 | })
12 |
13 | export default HomeBanner
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/assets/css/reset.less:
--------------------------------------------------------------------------------
1 | @import "./variables.less";
2 |
3 | body, button, dd, dl, dt, form, h1, h2, h3, h4, h5, h6, hr, input, li, ol, p, pre, td, textarea, th, ul {
4 | padding: 0;
5 | margin: 0;
6 | }
7 |
8 | a {
9 | color: @textColor;
10 | text-decoration: none;
11 | }
12 |
13 |
14 | img {
15 | vertical-align: top;
16 | }
17 |
18 | ul, li {
19 | list-style: none;
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/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 | ]
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/src/assets/svg/utils/index.js:
--------------------------------------------------------------------------------
1 | function styleStrToObject(styleStr) {
2 | const obj = {}
3 | const s = styleStr.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 |
10 | return obj
11 | }
12 |
13 | export default styleStrToObject
14 |
--------------------------------------------------------------------------------
/src/assets/theme/index.js:
--------------------------------------------------------------------------------
1 | const theme = {
2 | color: {
3 | primaryColor: "#ff385c",
4 | secondaryColor: "#00848A"
5 | },
6 | text: {
7 | primaryColor: "#484848",
8 | secondaryColor: "#222"
9 | },
10 | mixin: {
11 | boxShadow: `
12 | transition: box-shadow 200ms ease;
13 | &:hover {
14 | box-shadow: 0 2px 4px rgba(0,0,0,.18);
15 | }
16 | `
17 | }
18 | }
19 |
20 | export default theme
21 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit"
2 | import mainReducer from "./modules/main"
3 | import homeReducer from "./modules/home"
4 | import entireReducer from "./modules/entire"
5 | import detailReducer from "./modules/detail"
6 |
7 | const store = configureStore({
8 | reducer: {
9 | main: mainReducer,
10 | home: homeReducer,
11 | entire: entireReducer,
12 | detail: detailReducer
13 | }
14 | })
15 |
16 | export default store
17 |
--------------------------------------------------------------------------------
/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/store/modules/main.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit"
2 |
3 |
4 | const mainSlice = createSlice({
5 | name: "main",
6 | initialState: {
7 | headerConfig: {
8 | isFixed: false,
9 | topAlpha: false
10 | }
11 | },
12 | reducers: {
13 | changeHeaderConfigAction(state, { payload }) {
14 | state.headerConfig = payload
15 | }
16 | }
17 | })
18 |
19 | export const { changeHeaderConfigAction } = mainSlice.actions
20 | export default mainSlice.reducer
21 |
--------------------------------------------------------------------------------
/src/components/section-footer/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 |
4 | export const FooterWrapper = styled.div`
5 | display: flex;
6 | margin-top: 10px;
7 |
8 | .info {
9 | display: flex;
10 | align-items: center;
11 | cursor: pointer;
12 |
13 | font-size: 17px;
14 | font-weight: 700;
15 | color: ${props => props.color};
16 |
17 | &:hover {
18 | text-decoration: underline;
19 | }
20 |
21 | .text {
22 | margin-right: 6px;
23 | }
24 | }
25 | `
--------------------------------------------------------------------------------
/src/assets/svg/icon-triangle-arrow-bottom.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import styleStrToObject from './utils'
3 |
4 | const IconTriangleArrowBottom = memo(() => {
5 | return (
6 |
7 | )
8 | })
9 |
10 | export default IconTriangleArrowBottom
--------------------------------------------------------------------------------
/src/assets/svg/icon-triangle-arrow-top.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import styleStrToObject from './utils'
3 |
4 | const IconTriangleArrowTop = memo(() => {
5 | return (
6 |
7 | )
8 | })
9 |
10 | export default IconTriangleArrowTop
--------------------------------------------------------------------------------
/src/assets/svg/icon_menu.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import styleStrToObject from './utils'
3 |
4 | const IconMenu = memo(() => {
5 | return (
6 |
7 | )
8 | })
9 |
10 | export default IconMenu
--------------------------------------------------------------------------------
/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/app-header/c-cpns/header-left/index.jsx:
--------------------------------------------------------------------------------
1 | import IconLogo from '@/assets/svg/icon_logo'
2 | import React, { memo } from 'react'
3 | import { useNavigate } from 'react-router-dom'
4 | import { LeftWrapper } from './style'
5 |
6 | const HeaderLeft = memo(() => {
7 |
8 | const navigate = useNavigate()
9 | function logoClickHandle() {
10 | navigate("/home")
11 | }
12 |
13 | return (
14 |
15 |
16 |
17 |
18 |
19 | )
20 | })
21 |
22 | export default HeaderLeft
--------------------------------------------------------------------------------
/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-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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/views/entire/c-cpns/entire-rooms/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 |
4 | export const RoomsWrapper = styled.div`
5 | position: relative;
6 | padding: 30px 20px;
7 | margin-top: 128px;
8 |
9 | .title {
10 | font-size: 22px;
11 | font-weight: 700;
12 | color: #222;
13 | margin: 0 0 10px 10px;
14 | }
15 |
16 | .list {
17 | display: flex;
18 | flex-wrap: wrap;
19 | }
20 |
21 | > .cover {
22 | position: absolute;
23 | left: 0;
24 | right: 0;
25 | top: 0;
26 | bottom: 0;
27 | background-color: rgba(255,255,255,.8);
28 | }
29 | `
30 |
--------------------------------------------------------------------------------
/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/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/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo, Suspense } 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/useScrollTop'
6 | import routes from './router'
7 |
8 | const App = memo(() => {
9 | useScrollTop()
10 |
11 | return (
12 |
13 |
14 |
15 |
16 | {useRoutes(routes)}
17 |
18 |
19 |
20 |
21 | )
22 | })
23 |
24 | export default App
25 |
--------------------------------------------------------------------------------
/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.isAlpha ? "#fff": "#333"};
22 | }
23 | }
24 | `
--------------------------------------------------------------------------------
/src/components/section-rooms/index.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import React, { memo } from 'react'
3 |
4 | import RoomItem from '../room-item'
5 | import { RoomsWrapper } from './style'
6 |
7 | const SectionRooms = memo((props) => {
8 | const { roomList = [], itemWidth } = props
9 |
10 | return (
11 |
12 | {
13 | roomList.slice(0, 8)?.map(item => {
14 | return
15 | })
16 | }
17 |
18 | )
19 | })
20 |
21 | SectionRooms.propTypes = {
22 | roomList: PropTypes.array
23 | }
24 |
25 | export default SectionRooms
--------------------------------------------------------------------------------
/src/hooks/useScrollPosition.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { throttle } from "underscore"
3 |
4 | export default function useScrollPosition() {
5 | // 状态来记录位置
6 | const [scrollX, setScrollX] = useState(0)
7 | const [scrollY, setScrollY] = useState(0)
8 |
9 | // 监听window滚动
10 | useEffect(() => {
11 | const handleScroll = throttle(function() {
12 | setScrollX(window.scrollX)
13 | setScrollY(window.scrollY)
14 | }, 100)
15 |
16 | window.addEventListener("scroll", handleScroll)
17 | return () => {
18 | window.removeEventListener("scroll", handleScroll)
19 | }
20 | }, [])
21 |
22 | // 返回
23 | return { scrollX, scrollY }
24 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React 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 |
7 | import App from '@/App'
8 | import "normalize.css"
9 | import "antd/dist/antd.less"
10 | import "./assets/css/index.less"
11 | import store from './store'
12 | import theme from './assets/theme'
13 |
14 | const root = ReactDOM.createRoot(document.getElementById('root'));
15 | root.render(
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 |
--------------------------------------------------------------------------------
/src/views/detail/index.jsx:
--------------------------------------------------------------------------------
1 | import { changeHeaderConfigAction } from '@/store/modules/main'
2 | import React, { memo, useEffect } from 'react'
3 | import { useDispatch } from 'react-redux'
4 | import DetailInfos from './c-cpns/detail-infos'
5 | import DetailPictures from './c-cpns/detail-pictures'
6 | import { DetailWrapper } from './style'
7 |
8 | const Detail = memo(() => {
9 | const dispatch = useDispatch()
10 | useEffect(() => {
11 | dispatch(changeHeaderConfigAction({ isFixed: false, topAlpha: false }))
12 | }, [dispatch])
13 |
14 | return (
15 |
16 |
17 |
18 |
19 | )
20 | })
21 |
22 | export default Detail
23 |
--------------------------------------------------------------------------------
/src/assets/svg/icon-close.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import styleStrToObject from './utils'
3 |
4 | const IconClose = memo(() => {
5 | return (
6 |
7 | )
8 | })
9 |
10 | export default IconClose
--------------------------------------------------------------------------------
/src/components/section-tabs/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 |
4 | export const TabsWrapper = styled.div`
5 | .item {
6 | box-sizing: border-box;
7 | flex-basis: 120px;
8 | flex-shrink: 0;
9 | padding: 14px 16px;
10 | margin-right: 16px;
11 | border-radius: 3px;
12 | font-size: 17px;
13 | text-align: center;
14 | border: 0.5px solid #D8D8D8;
15 | white-space: nowrap;
16 | cursor: pointer;
17 | ${props => props.theme.mixin.boxShadow};
18 |
19 | &:last-child {
20 | margin-right: 0;
21 | }
22 |
23 | &.active {
24 | color: #fff;
25 | background-color: ${props => props.theme.color.secondaryColor};
26 | }
27 | }
28 | `
--------------------------------------------------------------------------------
/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 |
8 | .info {
9 | display: flex;
10 | flex-direction: column;
11 | align-items: center;
12 |
13 | .MuiPaginationItem-icon {
14 | font-size: 24px;
15 | }
16 |
17 | .MuiPaginationItem-page {
18 | margin: 0 9px;
19 |
20 | &:hover {
21 | text-decoration: underline;
22 | }
23 | }
24 |
25 | .MuiPaginationItem-page.Mui-selected {
26 | background-color: #222;
27 | color: #fff;
28 | }
29 |
30 | .desc {
31 | margin-top: 16px;
32 | color: #222;
33 | }
34 | }
35 | `
--------------------------------------------------------------------------------
/craco.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const CracoLessPlugin = require('craco-less');
3 |
4 | const resolve = pathname => path.resolve(__dirname, pathname)
5 |
6 | module.exports = {
7 | // less
8 | plugins: [
9 | {
10 | plugin: CracoLessPlugin,
11 | options: {
12 | lessLoaderOptions: {
13 | lessOptions: {
14 | modifyVars: { },
15 | javascriptEnabled: true,
16 | },
17 | },
18 | },
19 | },
20 | ],
21 | // webpack
22 | webpack: {
23 | alias: {
24 | "@": resolve("src"),
25 | "components": resolve("src/components"),
26 | "utils": resolve("src/utils"),
27 | // '@mui/styled-engine': '@mui/styled-engine-sc'
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/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) {
6 | this.instance = axios.create({
7 | baseURL,
8 | timeout
9 | })
10 |
11 | this.instance.interceptors.response.use((res) => {
12 | return res.data
13 | }, err => {
14 | return err
15 | })
16 | }
17 |
18 | request(config) {
19 | return this.instance.request(config)
20 | }
21 |
22 | get(config) {
23 | return this.request({ ...config, method: "get" })
24 | }
25 |
26 | post(config) {
27 | return this.request({ ...config, method: "post" })
28 | }
29 | }
30 |
31 |
32 | export default new HYRequest(BASE_URL, TIMEOUT)
33 |
34 |
--------------------------------------------------------------------------------
/src/assets/svg/icon_avatar.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import styleStrToObject from './utils'
3 |
4 | const IconAvatar = memo(() => {
5 | return (
6 |
7 | )
8 | })
9 |
10 | export default IconAvatar
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Demo from "@/views/demo"
2 | import React from "react"
3 | import { Navigate } from "react-router-dom"
4 |
5 | const Home = React.lazy(() => import("@/views/home"))
6 | // import Home from "@/views/home"
7 | const Entire = React.lazy(() => import("@/views/entire"))
8 | const Detail = React.lazy(() => import("@/views/detail"))
9 |
10 | const routes = [
11 | {
12 | path: "/",
13 | element:
14 | },
15 | {
16 | path: "/home",
17 | element:
18 | },
19 | {
20 | path: "/entire",
21 | element:
22 | },
23 | {
24 | path: "/detail",
25 | element:
26 | },
27 | {
28 | path: "/demo",
29 | element:
30 | }
31 | ]
32 |
33 | export default routes
34 |
--------------------------------------------------------------------------------
/src/views/home/c-cpns/home-section-v1/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 PropTypes from 'prop-types'
5 | import React, { memo } from 'react'
6 | import { SectionV1Wrapper } from './style'
7 |
8 | const HomeSectionV1 = memo((props) => {
9 | const { infoData } = props
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | )
17 | })
18 |
19 | HomeSectionV1.propTypes = {
20 | infoData: PropTypes.object
21 | }
22 |
23 | export default HomeSectionV1
--------------------------------------------------------------------------------
/src/views/entire/c-cpns/entire-filter/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 |
4 | export const FilterWrapper = styled.div`
5 | position: fixed;
6 | z-index: 9;
7 | top: 80px;
8 | left: 0;
9 | right: 0;
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/store/modules/entire/reducer.js:
--------------------------------------------------------------------------------
1 | import * as actionTypes from "./constants"
2 |
3 | const initialState = {
4 | currentPage: 0, // 当前页码
5 | roomList: [], // 房间列表
6 | totalCount: 0, // 总数据个数
7 |
8 | isLoading: false
9 | }
10 |
11 | function reducer(state = initialState, action) {
12 | switch(action.type) {
13 | case actionTypes.CHANGE_CURRENT_PAGE:
14 | return { ...state, currentPage: action.currentPage }
15 | case actionTypes.CHANGE_ROOM_LIST:
16 | return { ...state, roomList: action.roomList }
17 | case actionTypes.CHANGE_TOTAL_COUNT:
18 | return { ...state, totalCount: action.totalCount }
19 | case actionTypes.CHANGE_IS_LOADING:
20 | return { ...state, isLoading: action.isLoading }
21 | default:
22 | return state
23 | }
24 | }
25 |
26 |
27 | export default reducer
28 |
--------------------------------------------------------------------------------
/src/components/longfor-item/index.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import React, { memo } from 'react'
3 | import { ItemWrapper } from './style'
4 |
5 | const LongforItem = memo((props) => {
6 | const { itemData } = props
7 |
8 | return (
9 |
10 |
11 |
12 |

13 |
14 |
15 |
{itemData.city}
16 |
均价 {itemData.price}
17 |
18 |
19 |
20 |
21 | )
22 | })
23 |
24 | LongforItem.propTypes = {
25 | itemData: PropTypes.object
26 | }
27 |
28 | export default LongforItem
--------------------------------------------------------------------------------
/src/services/modules/home.js:
--------------------------------------------------------------------------------
1 | import hyRequest from "..";
2 |
3 | export function getHomeGoodPriceData() {
4 | return hyRequest.get({
5 | url: "/home/goodprice"
6 | })
7 | }
8 |
9 |
10 | export function getHomeHighScoreData() {
11 | return hyRequest.get({
12 | url: "/home/highscore"
13 | })
14 | }
15 |
16 |
17 | export function getHomeDiscountData() {
18 | return hyRequest.get({
19 | url: "/home/discount"
20 | })
21 | }
22 |
23 |
24 | export function getHomeHotRecommendData() {
25 | return hyRequest.get({
26 | url: "/home/hotrecommenddest"
27 | })
28 | }
29 |
30 |
31 | export function getHomeLongforData() {
32 | return hyRequest.get({
33 | url: "/home/longfor"
34 | })
35 | }
36 |
37 |
38 | export function getHomePlusData() {
39 | return hyRequest.get({
40 | url: "/home/plus"
41 | })
42 | }
43 |
--------------------------------------------------------------------------------
/src/views/entire/index.jsx:
--------------------------------------------------------------------------------
1 | import { fetchRoomListAction } from '@/store/modules/entire/actionCreators'
2 | import { changeHeaderConfigAction } from '@/store/modules/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(() => {
11 | // 发送网络请求, 获取数据, 并且保存当前的页面等等.....
12 | const dispatch = useDispatch()
13 | useEffect(() => {
14 | dispatch(fetchRoomListAction())
15 | dispatch(changeHeaderConfigAction({ isFixed: true, topAlpha: false }))
16 | }, [dispatch])
17 |
18 | return (
19 |
20 |
21 |
22 |
23 |
24 | )
25 | })
26 |
27 | export default Entire
28 |
--------------------------------------------------------------------------------
/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/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 PropTypes from 'prop-types'
5 | import React, { memo } from 'react'
6 | import { LongforWrapper } from './style'
7 |
8 | const HomeLongfor = memo((props) => {
9 | const { infoData } = props
10 |
11 | return (
12 |
13 |
14 |
15 |
16 | {
17 | infoData.list.map(item => {
18 | return
19 | })
20 | }
21 |
22 |
23 |
24 | )
25 | })
26 |
27 | HomeLongfor.propTypes = {
28 | infoData: PropTypes.object
29 | }
30 |
31 | export default HomeLongfor
--------------------------------------------------------------------------------
/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/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 showMessage = "显示全部"
11 | if (name) {
12 | showMessage = `显示更多${name}房源`
13 | }
14 |
15 | /** 事件处理的逻辑 */
16 | const navigate = useNavigate()
17 | function moreClickHandle() {
18 | navigate("/entire")
19 | }
20 |
21 | return (
22 |
23 |
24 | {showMessage}
25 |
26 |
27 |
28 | )
29 | })
30 |
31 | SectionFooter.propTypes = {
32 | name: PropTypes.string
33 | }
34 |
35 | export default SectionFooter
--------------------------------------------------------------------------------
/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/base-ui/scroll-view/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 |
4 | export const ViewWrapper = styled.div`
5 | position: relative;
6 | padding: 8px 0;
7 |
8 | .scroll {
9 | overflow: hidden;
10 |
11 | .scroll-content {
12 | display: flex;
13 | transition: transform 250ms 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/views/home/c-cpns/home-section-v3/index.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import React, { memo } from 'react'
3 |
4 | import SectionHeader from '@/components/section-header'
5 | import { SectionV3Wrapper } from './style'
6 | import RoomItem from '@/components/room-item'
7 | import ScrollView from '@/base-ui/scroll-view'
8 | import SectionFooter from '@/components/section-footer'
9 |
10 | const HomeSectionV3 = memo((props) => {
11 | const { infoData } = props
12 |
13 | return (
14 |
15 |
16 |
17 |
18 | {
19 | infoData.list.map(item => {
20 | return
21 | })
22 | }
23 |
24 |
25 |
26 |
27 | )
28 | })
29 |
30 | HomeSectionV3.propTypes = {
31 | infoData: PropTypes.object
32 | }
33 |
34 | export default HomeSectionV3
--------------------------------------------------------------------------------
/src/components/app-header/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 |
3 |
4 | export const HeaderWrapper = styled.div`
5 |
6 | &.fixed {
7 | position: fixed;
8 | z-index: 99;
9 | top: 0;
10 | left: 0;
11 | right: 0;
12 | }
13 |
14 | .content {
15 | position: relative;
16 | z-index: 19;
17 | transition: all 250ms ease;
18 | background-color: ${props => props.theme.isAlpha ? "rgba(255,255,255,0)": "rgba(255,255,255,1)"};
19 | border-bottom: 1px solid #eee;
20 | border-bottom-color: ${props => props.theme.isAlpha ? "rgba(233,233,233,0)": "rgba(233,233,233,1)"};
21 |
22 | .top {
23 | display: flex;
24 | align-items: center;
25 | height: 80px;
26 | }
27 | }
28 |
29 | .cover {
30 | position: fixed;
31 | z-index: 9;
32 | left: 0;
33 | right: 0;
34 | top: 0;
35 | bottom: 0;
36 | background-color: rgba(0,0,0,.3);
37 | }
38 | `
39 |
40 | export const SearchAreaWrapper = styled.div`
41 | transition: height 250ms ease;
42 | height: ${props => props.isSearch ? "100px": "0"};
43 | `
44 |
--------------------------------------------------------------------------------
/src/components/longfor-item/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 |
4 | export const ItemWrapper = styled.div`
5 | flex-shrink: 0;
6 | width: 20%;
7 |
8 | .inner {
9 | padding: 8px;
10 |
11 | .item-info {
12 | position: relative;
13 | border-radius: 3px;
14 | overflow: hidden;
15 | }
16 | }
17 |
18 | .cover {
19 | width: 100%;
20 | }
21 |
22 | .bg-cover {
23 | position: absolute;
24 | left: 0;
25 | right: 0;
26 | bottom: 0;
27 | height: 60%;
28 | background-image: linear-gradient(-180deg, rgba(0, 0, 0, 0) 3%, rgb(0, 0, 0) 100%)
29 | }
30 |
31 | .info {
32 | position: absolute;
33 | left: 8px;
34 | right: 8px;
35 | bottom: 0;
36 | display: flex;
37 | flex-direction: column;
38 | justify-content: center;
39 | align-items: center;
40 | padding: 0 24px 32px;
41 | color: #fff;
42 |
43 | .city {
44 | font-size: 18px;
45 | font-weight: 600;
46 | }
47 |
48 | .price {
49 | font-size: 14px;
50 | margin-top: 5px;
51 | }
52 | }
53 | `
--------------------------------------------------------------------------------
/src/assets/svg/icon_global.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import styleStrToObject from './utils'
3 |
4 | const IconGlobal = memo(() => {
5 | return (
6 |
7 | )
8 | })
9 |
10 | export default IconGlobal
--------------------------------------------------------------------------------
/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-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 | console.log("AppFooter")
7 |
8 | return (
9 |
10 |
11 |
12 | {
13 | footerData.map(item => {
14 | return (
15 |
16 |
{item.name}
17 |
18 | {
19 | item.list.map(iten => {
20 | return
{iten}
21 | })
22 | }
23 |
24 |
25 | )
26 | })
27 | }
28 |
29 |
© 2022 Airbnb, Inc. All rights reserved.条款 · 隐私政策 · 网站地图 · 全国旅游投诉渠道 12301
30 |
31 |
32 | )
33 | })
34 |
35 | export default AppFooter
--------------------------------------------------------------------------------
/src/views/demo/index.jsx:
--------------------------------------------------------------------------------
1 | import Indicator from '@/base-ui/indicator'
2 | import React, { memo, useState } from 'react'
3 | import { DemoWrapper } from './style'
4 |
5 | const Demo = memo(() => {
6 | const names = ["abc", "cba", "nba", "mba", "aaa", "bbb", "ccc", "ddd"]
7 | const [selectIndex, setSelectIndex] = useState(0)
8 |
9 | function toggleClickHandle(isNext = true) {
10 | let newIndex = isNext ? selectIndex + 1: selectIndex - 1
11 | if (newIndex < 0) newIndex = names.length - 1
12 | if (newIndex > names.length - 1) newIndex = 0
13 | setSelectIndex(newIndex)
14 | }
15 |
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | {
25 | names.map(item => {
26 | return
27 | })
28 | }
29 |
30 |
31 |
32 | )
33 | })
34 |
35 | export default Demo
--------------------------------------------------------------------------------
/src/components/section-tabs/index.jsx:
--------------------------------------------------------------------------------
1 | import ScrollView from '@/base-ui/scroll-view'
2 | import classNames from 'classnames'
3 | import PropTypes from 'prop-types'
4 | import React, { memo, useState } from 'react'
5 | import { TabsWrapper } from './style'
6 |
7 | const SectionTabs = memo((props) => {
8 | const { tabNames = [], tabClick } = props
9 | const [currentIndex, setCurrentIndex] = useState(0)
10 |
11 | function itemClickHandle(index, item) {
12 | setCurrentIndex(index)
13 | tabClick(index, item)
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/views/entire/c-cpns/entire-filter/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useState } from 'react'
2 | import { FilterWrapper } from './style'
3 | import filterData from "@/assets/data/filter_data.json"
4 | import classNames from 'classnames'
5 |
6 | const EntireFilter = memo(() => {
7 | const [selectItems, setSelectItems] = useState([])
8 |
9 | function itemClickHandle(item) {
10 | const newItems = [...selectItems]
11 | if (newItems.includes(item)) { // 移除操作
12 | const itemIndex = newItems.findIndex(filterItem => filterItem === item)
13 | newItems.splice(itemIndex, 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 |
itemClickHandle(item)}
30 | >
31 | {item}
32 |
33 | )
34 | })
35 | }
36 |
37 |
38 | )
39 | })
40 |
41 | export default EntireFilter
--------------------------------------------------------------------------------
/src/store/modules/entire/actionCreators.js:
--------------------------------------------------------------------------------
1 | import { getEntireRoomList } from "@/services/modules/entire"
2 | import * as actionTypes from "./constants"
3 |
4 | export const changeCurrentPageAction = (currentPage) => ({
5 | type: actionTypes.CHANGE_CURRENT_PAGE,
6 | currentPage
7 | })
8 |
9 | export const changeRoomListAction = (roomList) => ({
10 | type: actionTypes.CHANGE_ROOM_LIST,
11 | roomList
12 | })
13 |
14 | export const changeTotalCountAction = (totalCount) => ({
15 | type: actionTypes.CHANGE_TOTAL_COUNT,
16 | totalCount
17 | })
18 |
19 |
20 | export const changeIsLoadingAction = (isLoading) => ({
21 | type: actionTypes.CHANGE_IS_LOADING,
22 | isLoading
23 | })
24 |
25 |
26 | export const fetchRoomListAction = (page = 0) => {
27 | // 新的函数
28 | return async (dispatch, getState) => {
29 | // 0.修改currentPage
30 | dispatch(changeCurrentPageAction(page))
31 |
32 | // 1.根据页码获取最新的数据
33 | // const currentPage = getState().entire.currentPage
34 | dispatch(changeIsLoadingAction(true))
35 | const res = await getEntireRoomList(page * 20)
36 | dispatch(changeIsLoadingAction(false))
37 |
38 | // 2.获取到最新的数据, 保存redux的store中
39 | const roomList = res.list
40 | const totalCount = res.totalCount
41 | dispatch(changeRoomListAction(roomList))
42 | dispatch(changeTotalCountAction(totalCount))
43 | }
44 | }
45 |
46 |
--------------------------------------------------------------------------------
/src/base-ui/indicator/index.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import React, { memo, useEffect, useRef } from 'react'
3 | import { IndicatorWrapper } from './style'
4 |
5 | const Indicator = memo((props) => {
6 | const { selectIndex = 0 } = props
7 | const contentRef = useRef()
8 |
9 | useEffect(() => {
10 | // 1.获取selectIndex对应的item
11 | const selectItemEl = contentRef.current.children[selectIndex]
12 | const itemLeft = selectItemEl.offsetLeft
13 | const itemWidth = selectItemEl.clientWidth
14 | // 2.content的宽度
15 | const contentWidth = contentRef.current.clientWidth
16 | const contentScroll = contentRef.current.scrollWidth
17 | // 3.获取selectIndex要滚动的距离
18 | let distance = itemLeft + itemWidth * 0.5 - contentWidth * 0.5
19 | // 4.特殊情况的处理
20 | if (distance < 0) distance = 0 // 左边的特殊情况处理
21 | const totalDistance = contentScroll - contentWidth
22 | if (distance > totalDistance) distance = totalDistance // 右边的特殊情况处理
23 |
24 | // 5.改变位置即可
25 | contentRef.current.style.transform = `translate(${-distance}px)`
26 | }, [selectIndex])
27 |
28 | return (
29 |
30 |
31 | {
32 | props.children
33 | }
34 |
35 |
36 | )
37 | })
38 |
39 | Indicator.propTypes = {
40 | selectIndex: PropTypes.number
41 | }
42 |
43 | export default Indicator
--------------------------------------------------------------------------------
/src/views/home/c-cpns/home-section-v2/index.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import React, { memo, useState, useCallback } from 'react'
3 |
4 | import SectionHeader from '@/components/section-header'
5 | import SectionRooms from '@/components/section-rooms'
6 | import SectionTabs from '@/components/section-tabs'
7 | import { SectionV2Wrapper } from './style'
8 | import SectionFooter from '@/components/section-footer'
9 |
10 | const HomeSectionV2 = memo((props) => {
11 | /** 从props获取数据 */
12 | const { infoData } = props
13 |
14 | /** 定义内部的state */
15 | const initialName = Object.keys(infoData.dest_list)[0]
16 | const [name, setName] = useState(initialName)
17 | const tabNames = infoData.dest_address?.map(item => item.name)
18 | // useEffect(() => {
19 | // setName("xxxxx")
20 | // }, [infoData])
21 |
22 | /** 事件处理函数 */
23 | const tabClickHandle = useCallback(function (index, name) {
24 | setName(name)
25 | }, [])
26 |
27 | return (
28 |
29 |
30 |
31 |
32 |
33 |
34 | )
35 | })
36 |
37 | HomeSectionV2.propTypes = {
38 | infoData: PropTypes.object
39 | }
40 |
41 | export default HomeSectionV2
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "airbnb",
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.4.0",
20 | "react-scripts": "5.0.1",
21 | "react-transition-group": "^4.4.5",
22 | "styled-components": "^5.3.5",
23 | "underscore": "^1.13.4",
24 | "web-vitals": "^2.1.4"
25 | },
26 | "scripts": {
27 | "start": "craco start",
28 | "build": "craco build",
29 | "test": "craco test",
30 | "eject": "react-scripts eject"
31 | },
32 | "eslintConfig": {
33 | "extends": [
34 | "react-app",
35 | "react-app/jest"
36 | ]
37 | },
38 | "browserslist": {
39 | "production": [
40 | ">0.2%",
41 | "not dead",
42 | "not op_mini all"
43 | ],
44 | "development": [
45 | "last 1 chrome version",
46 | "last 1 firefox version",
47 | "last 1 safari version"
48 | ]
49 | },
50 | "devDependencies": {
51 | "@craco/craco": "^7.0.0-alpha.7",
52 | "craco-less": "^2.1.0-alpha.0"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/views/entire/c-cpns/entire-rooms/index.jsx:
--------------------------------------------------------------------------------
1 | import RoomItem from '@/components/room-item'
2 | import { changeDetailInfoAction } from '@/store/modules/detail'
3 | import React, { memo, useCallback } 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 | /** 从redux中获取roomList数据 */
10 | const { roomList, totalCount, isLoading } = useSelector((state) => ({
11 | roomList: state.entire.roomList,
12 | totalCount: state.entire.totalCount,
13 | isLoading: state.entire.isLoading
14 | }), shallowEqual)
15 |
16 | /** 事件处理 */
17 | const navigate = useNavigate()
18 | const dispatch = useDispatch()
19 | const itemClickHandle = useCallback((item) => {
20 | dispatch(changeDetailInfoAction(item))
21 | navigate("/detail")
22 | }, [navigate, dispatch])
23 |
24 | return (
25 |
26 | {totalCount}多处住所
27 |
28 | {
29 | roomList.map((item) => {
30 | return (
31 |
37 | )
38 | })
39 | }
40 |
41 |
42 | { isLoading && }
43 |
44 | )
45 | })
46 |
47 | export default EntireRooms
--------------------------------------------------------------------------------
/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 { shallowEqual, useDispatch, useSelector } from 'react-redux';
6 | import { fetchRoomListAction } from '@/store/modules/entire/actionCreators';
7 |
8 | const EntirePagination = memo(() => {
9 | const { totalCount, currentPage, roomList } = useSelector((state) => ({
10 | totalCount: state.entire.totalCount,
11 | currentPage: state.entire.currentPage,
12 | roomList: state.entire.roomList
13 | }), shallowEqual)
14 |
15 | // 小算法:必须掌握
16 | const totalPage = Math.ceil(totalCount / 20)
17 | const startCount = currentPage * 20 + 1
18 | const endCount = (currentPage + 1) * 20
19 |
20 | /** 事件处理的逻辑 */
21 | const dispatch = useDispatch()
22 | function pageChangeHandle(event, pageCount) {
23 | // 回到顶部
24 | window.scrollTo(0, 0)
25 | // 更新最新的页码: redux => currentPage
26 | // dispatch(changeCurrentPageAction(pageCount - 1))
27 | dispatch(fetchRoomListAction(pageCount - 1))
28 | }
29 |
30 | return (
31 |
32 | {
33 | !!roomList.length && (
34 |
35 |
36 |
37 | 第 {startCount} - {endCount} 个房源, 共超过 {totalCount} 个
38 |
39 |
40 | )
41 | }
42 |
43 | )
44 | })
45 |
46 | export default EntirePagination
--------------------------------------------------------------------------------
/src/views/detail/c-cpns/detail-pictures/index.jsx:
--------------------------------------------------------------------------------
1 | import PictureBrowser from '@/base-ui/picture-browser'
2 | import React, { memo, useState } from 'react'
3 | import { shallowEqual, useSelector } from 'react-redux'
4 | import { PicturesWrapper } from './style'
5 |
6 | const DetailPictures = memo(() => {
7 | /** 定义组件内部的状态 */
8 | const [showBrowser, setShowBrowser] = useState(false)
9 |
10 | /** redux获取数据 */
11 | const { detailInfo } = useSelector((state) => ({
12 | detailInfo: state.detail.detailInfo
13 | }), shallowEqual)
14 |
15 | return (
16 |
17 |
18 |
19 |
setShowBrowser(true)}>
20 |

21 |
22 |
23 |
24 |
25 | {
26 | detailInfo?.picture_urls?.slice(1, 5).map(item => {
27 | return (
28 |
setShowBrowser(true)}>
29 |

30 |
31 |
32 | )
33 | })
34 | }
35 |
36 |
37 | setShowBrowser(true)}>显示照片
38 | { showBrowser && (
39 | setShowBrowser(false)}
42 | />
43 | )}
44 |
45 | )
46 | })
47 |
48 | export default DetailPictures
--------------------------------------------------------------------------------
/src/components/app-header/c-cpns/header-center/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useState } from 'react'
2 | import { CSSTransition } from "react-transition-group"
3 |
4 | import SearchTitles from "@/assets/data/search_titles"
5 | import SearchTabs from './c-cpns/search-tabs'
6 | import IconSearchBar from '@/assets/svg/icon-search-bar'
7 | import { CenterWrapper } from './style'
8 | import SearchSections from './c-cpns/search-sections'
9 |
10 | const HeaderCenter = memo((props) => {
11 | const { isSearch, searchBarClick } = props
12 |
13 | const [tabIndex, setTabIndex] = useState(0)
14 | const titles = SearchTitles.map(item => item.title)
15 |
16 | function searchBarClickHandle() {
17 | if (searchBarClick) searchBarClick()
18 | }
19 |
20 | return (
21 |
22 |
28 |
29 |
30 | 搜索房源和体验
31 |
32 |
33 |
34 |
35 |
36 |
37 |
43 |
49 |
50 |
51 | )
52 | })
53 |
54 | export default HeaderCenter
--------------------------------------------------------------------------------
/src/views/detail/c-cpns/detail-pictures/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 |
4 | export const PicturesWrapper = styled.div`
5 | position: relative;
6 |
7 | > .pictures {
8 | display: flex;
9 | height: 600px;
10 | background-color: #000;
11 |
12 |
13 | &:hover {
14 | .cover {
15 | opacity: 1 !important;
16 | }
17 |
18 | .item:hover {
19 | .cover {
20 | opacity: 0 !important;
21 | }
22 | }
23 | }
24 | }
25 |
26 | .left, .right {
27 | width: 50%;
28 | height: 100%;
29 |
30 | .item {
31 | position: relative;
32 | height: 100%;
33 | overflow: hidden;
34 | cursor: pointer;
35 |
36 | img {
37 | width: 100%;
38 | height: 100%;
39 | object-fit: cover;
40 |
41 | transition: transform 0.3s ease-in;
42 | }
43 |
44 | .cover {
45 | position: absolute;
46 | left: 0;
47 | right: 0;
48 | top: 0;
49 | bottom: 0;
50 | background-color: rgba(0,0,0,.3);
51 | opacity: 0;
52 | transition: opacity 200ms ease;
53 | }
54 |
55 | &:hover {
56 | img {
57 | transform: scale(1.08);
58 | }
59 | }
60 | }
61 | }
62 |
63 | .right {
64 | display: flex;
65 | flex-wrap: wrap;
66 |
67 | .item {
68 | width: 50%;
69 | height: 50%;
70 | box-sizing: border-box;
71 | border: 1px solid #000;
72 | }
73 | }
74 |
75 | .show-btn {
76 | position: absolute;
77 | z-index: 99;
78 | right: 15px;
79 | bottom: 15px;
80 | line-height: 22px;
81 | padding: 6px 15px;
82 | border-radius: 4px;
83 | background-color: #fff;
84 | cursor: pointer;
85 | }
86 | `
--------------------------------------------------------------------------------
/src/components/app-header/c-cpns/header-right/index.jsx:
--------------------------------------------------------------------------------
1 | import IconAvatar from '@/assets/svg/icon_avatar'
2 | import IconGlobal from '@/assets/svg/icon_global'
3 | import IconMenu from '@/assets/svg/icon_menu'
4 | import React, { memo, useEffect, useState } from 'react'
5 | import { RightWrapper } from './style'
6 |
7 | const HeaderRight = memo(() => {
8 | /** 定义组件内部的状态 */
9 | const [ showPanel, setShowPanel ] = useState(false)
10 |
11 | /** 副作用代码 */
12 | useEffect(() => {
13 | function windowHandleClick() {
14 | setShowPanel(false)
15 | }
16 | window.addEventListener("click", windowHandleClick, true)
17 | return () => {
18 | window.removeEventListener("click", windowHandleClick, true)
19 | }
20 | }, [])
21 |
22 | /** 事件处理函数 */
23 | function profileClickHandle() {
24 | setShowPanel(true)
25 | }
26 |
27 | return (
28 |
29 |
30 | 登录
31 | 注册
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | { showPanel && (
42 |
43 |
47 |
48 |
出租房源
49 |
开展体验
50 |
帮助
51 |
52 |
53 | ) }
54 |
55 |
56 | )
57 | })
58 |
59 | export default HeaderRight
--------------------------------------------------------------------------------
/src/components/app-header/index.jsx:
--------------------------------------------------------------------------------
1 | import useScrollPosition from '@/hooks/useScrollPosition'
2 | import classNames from 'classnames'
3 | import React, { memo, useRef, useState } from 'react'
4 | import { shallowEqual, useSelector } from 'react-redux'
5 | import { ThemeProvider } from 'styled-components'
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, SearchAreaWrapper } from './style'
10 |
11 | const AppHeader = memo(() => {
12 | /** 定义组件内部的状态 */
13 | const [isSearch, setIsSearch] = useState(false)
14 |
15 | /** 从redux中获取数据 */
16 | const { headerConfig } = useSelector((state) => ({
17 | headerConfig: state.main.headerConfig
18 | }), shallowEqual)
19 | const { isFixed, topAlpha } = headerConfig
20 |
21 | /** 监听滚动的监听 */
22 | const { scrollY } = useScrollPosition()
23 | const prevY = useRef(0)
24 | // 在正常情况的情况下(搜索框没有弹出来), 不断记录值
25 | if (!isSearch) prevY.current = scrollY
26 | // 在弹出搜索功能的情况, 滚动的距离大于之前记录的距离的30
27 | if (isSearch && Math.abs(scrollY - prevY.current) > 30) setIsSearch(false)
28 |
29 |
30 | /** 透明度的逻辑 */
31 | const isAlpha = topAlpha && scrollY === 0
32 |
33 | return (
34 |
35 |
36 |
37 |
38 |
39 | setIsSearch(true)}/>
40 |
41 |
42 |
43 |
44 | { isSearch && setIsSearch(false)}>
}
45 |
46 |
47 | )
48 | })
49 |
50 | export default AppHeader
51 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/components/app-header/c-cpns/header-right/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 |
4 | export const RightWrapper = styled.div`
5 | flex: 1;
6 | display: flex;
7 | justify-content: flex-end;
8 | align-items: center;
9 |
10 | color: ${props => props.theme.text.primaryColor};
11 | font-weight: 600;
12 |
13 | .btns {
14 | display: flex;
15 | box-sizing: content-box;
16 | color: ${props => props.theme.isAlpha ? "#fff": props.theme.text.primaryColor};
17 |
18 | .btn {
19 | height: 18px;
20 | line-height: 18px;
21 | padding: 12px 15px;
22 | border-radius: 22px;
23 | cursor: pointer;
24 | box-sizing: content-box;
25 |
26 | &:hover {
27 | background-color: ${props => props.theme.isAlpha ? "rgba(255,255,255,.1)": "#f5f5f5"};
28 | }
29 | }
30 | }
31 |
32 | .profile {
33 | position: relative;
34 | display: flex;
35 | justify-content: space-evenly;
36 | align-items: center;
37 | width: 77px;
38 | height: 42px;
39 | margin-right: 24px;
40 | box-sizing: border-box;
41 | border: 1px solid #ccc;
42 | border-radius: 25px;
43 | background-color: #fff;
44 | color: ${props => props.theme.text.primaryColor};
45 | cursor: pointer;
46 |
47 | ${props => props.theme.mixin.boxShadow};
48 |
49 | .panel {
50 | position: absolute;
51 | top: 54px;
52 | right: 0;
53 | width: 240px;
54 | background-color: #fff;
55 | border-radius: 10px;
56 | box-shadow: 0 0 6px rgba(0,0,0,.2);
57 | color: #666;
58 |
59 | .top, .bottom {
60 | padding: 10px 0;
61 |
62 | .item {
63 | height: 40px;
64 | line-height: 40px;
65 | padding: 0 16px;
66 |
67 | &:hover {
68 | background-color: #f5f5f5;
69 | }
70 | }
71 | }
72 |
73 | .top {
74 | border-bottom: 1px solid #ddd;
75 | }
76 | }
77 | }
78 | `
--------------------------------------------------------------------------------
/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, useState } from 'react'
4 | import { useRef } from 'react'
5 | import { ViewWrapper } from './style'
6 |
7 | const ScrollView = memo((props) => {
8 | /** 定义内部的状态 */
9 | const [showLeft, setShowLeft] = useState(false)
10 | const [showRight, setShowRight] = useState(false)
11 | const [posIndex, setPosIndex] = useState(0)
12 | const totalDistanceRef = useRef()
13 |
14 | /** 组件渲染完毕, 判断是否显示右侧的按钮 */
15 | const scrollContentRef = useRef()
16 | useEffect(() => {
17 | const scrollWidth = scrollContentRef.current.scrollWidth // 一共可以滚动的宽度
18 | const clientWidth = scrollContentRef.current.clientWidth // 本身占据的宽度
19 | const totalDistance = scrollWidth - clientWidth
20 | totalDistanceRef.current = totalDistance
21 | setShowRight(totalDistance > 0)
22 | }, [props.children])
23 |
24 | /** 事件处理的逻辑 */
25 | function controlClickHandle(isRight) {
26 | const newIndex = isRight ? posIndex + 1: posIndex - 1
27 | const newEl = scrollContentRef.current.children[newIndex]
28 | const newOffsetLeft = newEl.offsetLeft
29 | scrollContentRef.current.style.transform = `translate(-${newOffsetLeft}px)`
30 | setPosIndex(newIndex)
31 | // 是否继续显示右侧的按钮
32 | setShowRight(totalDistanceRef.current > newOffsetLeft)
33 | setShowLeft(newOffsetLeft > 0)
34 | }
35 |
36 | return (
37 |
38 | { showLeft && (
39 | controlClickHandle(false)}>
40 |
41 |
42 | ) }
43 | { showRight && (
44 | controlClickHandle(true)}>
45 |
46 |
47 | ) }
48 |
49 |
50 |
51 | {props.children}
52 |
53 |
54 |
55 | )
56 | })
57 |
58 | ScrollView.propTypes = {}
59 |
60 | export default ScrollView
--------------------------------------------------------------------------------
/src/components/app-header/c-cpns/header-center/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 |
4 | export const CenterWrapper = styled.div`
5 | position: relative;
6 | display: flex;
7 | justify-content: center;
8 | height: 48px;
9 |
10 | .search-bar {
11 | position: absolute;
12 | display: flex;
13 | justify-content: space-between;
14 | align-items: center;
15 | width: 300px;
16 | height: 48px;
17 | box-sizing: border-box;
18 | padding: 0 8px;
19 | border: 1px solid #ddd;
20 | border-radius: 24px;
21 | cursor: pointer;
22 | ${props => props.theme.mixin.boxShadow};
23 |
24 | .text {
25 | padding: 0 16px;
26 | color: #222;
27 | font-weight: 600;
28 | }
29 |
30 | .icon {
31 | display: flex;
32 | align-items: center;
33 | justify-content: center;
34 | width: 32px;
35 | height: 32px;
36 | border-radius: 50%;
37 | color: #fff;
38 | background-color: ${props => props.theme.color.primaryColor};
39 | }
40 | }
41 |
42 | .search-detail {
43 | position: relative;
44 | transform-origin: 50% 0;
45 | will-change: transform, opacity;
46 |
47 | .infos {
48 | position: absolute;
49 | top: 60px;
50 | left: 50%;
51 | transform: translateX(-50%);
52 | }
53 | }
54 |
55 | .detail-exit {
56 | transform: scale(1.0) translateY(0);
57 | opacity: 1;
58 | }
59 |
60 | .detail-exit-active {
61 | transition: all 250ms ease;
62 | transform: scale(0.35, 0.727273) translateY(-58px);
63 | opacity: 0;
64 | }
65 |
66 | .detail-enter {
67 | transform: scale(0.35, 0.727273) translateY(-58px);
68 | opacity: 0;
69 | }
70 |
71 | .detail-enter-active {
72 | transform: scale(1.0) translateY(0);
73 | opacity: 1;
74 | transition: all 250ms ease;
75 | }
76 |
77 | .bar-enter {
78 | transform: scale(2.85714, 1.375) translateY(58px);
79 | opacity: 0;
80 | }
81 |
82 | .bar-enter-active {
83 | transition: all 250ms ease;
84 | transform: scale(1.0) translateY(0);
85 | opacity: 1;
86 | }
87 |
88 | .bar-exit {
89 | opacity: 0;
90 | }
91 | `
--------------------------------------------------------------------------------
/src/store/modules/home.js:
--------------------------------------------------------------------------------
1 | import { getHomeDiscountData, getHomeGoodPriceData, getHomeHighScoreData, getHomeHotRecommendData, getHomeLongforData, getHomePlusData } from '@/services'
2 | import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
3 |
4 | export const fetchHomeDataAction = createAsyncThunk("fetchdata", (payload, { dispatch }) => {
5 | getHomeGoodPriceData().then(res => {
6 | dispatch(changeGoodPriceInfoAction(res))
7 | })
8 | getHomeHighScoreData().then(res => {
9 | dispatch(changeHighScoreInfoAction(res))
10 | })
11 | getHomeDiscountData().then(res => {
12 | dispatch(changeDiscountInfoAction(res))
13 | })
14 | getHomeHotRecommendData().then(res => {
15 | dispatch(changeRecommendInfoAction(res))
16 | })
17 | getHomeLongforData().then(res => {
18 | dispatch(changeLongforInfoAction(res))
19 | })
20 | getHomePlusData().then(res => {
21 | dispatch(changePlusInfoAction(res))
22 | })
23 | })
24 |
25 |
26 | const homeSlice = createSlice({
27 | name: "home",
28 | initialState: {
29 | goodPriceInfo: {},
30 | highScoreInfo: {},
31 | discountInfo: {},
32 | recommendInfo: {},
33 | longforInfo: {},
34 | plusInfo: {}
35 | },
36 | reducers: {
37 | changeGoodPriceInfoAction(state, { payload }) {
38 | state.goodPriceInfo = payload
39 | },
40 | changeHighScoreInfoAction(state, { payload }) {
41 | state.highScoreInfo = payload
42 | },
43 | changeDiscountInfoAction(state, { payload }) {
44 | state.discountInfo = payload
45 | },
46 | changeRecommendInfoAction(state, { payload }) {
47 | state.recommendInfo = payload
48 | },
49 | changeLongforInfoAction(state, { payload }) {
50 | state.longforInfo = payload
51 | },
52 | changePlusInfoAction(state, { payload }) {
53 | state.plusInfo = payload
54 | }
55 | },
56 | extraReducers: {
57 | // [fetchHomeDataAction.fulfilled](state, { payload }) {
58 | // state.goodPriceInfo = payload
59 | // }
60 | }
61 | })
62 |
63 | export const {
64 | changeGoodPriceInfoAction,
65 | changeHighScoreInfoAction,
66 | changeDiscountInfoAction,
67 | changeRecommendInfoAction,
68 | changeLongforInfoAction,
69 | changePlusInfoAction
70 | } = homeSlice.actions
71 |
72 | export default homeSlice.reducer
73 |
--------------------------------------------------------------------------------
/src/views/home/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useEffect } from 'react'
2 | import { shallowEqual, useDispatch, useSelector } from 'react-redux'
3 |
4 | import { fetchHomeDataAction } from '@/store/modules/home'
5 | import HomeBanner from './c-cpns/home-banner'
6 | import { HomeWrapper } from './style'
7 | import HomeSectionV1 from './c-cpns/home-section-v1'
8 | import HomeSectionV2 from './c-cpns/home-section-v2'
9 | import { isEmptyO } from '@/utils'
10 | import HomeLongfor from './c-cpns/home-longfor'
11 | import HomeSectionV3 from './c-cpns/home-section-v3'
12 | import { changeHeaderConfigAction } from '@/store/modules/main'
13 |
14 | const Home = memo(() => {
15 | /** 从redux中获取数据 */
16 | const { goodPriceInfo, highScoreInfo, discountInfo, recommendInfo, longforInfo, plusInfo } = useSelector((state) => ({
17 | goodPriceInfo: state.home.goodPriceInfo,
18 | highScoreInfo: state.home.highScoreInfo,
19 | discountInfo: state.home.discountInfo,
20 | recommendInfo: state.home.recommendInfo,
21 | longforInfo: state.home.longforInfo,
22 | plusInfo: state.home.plusInfo
23 | }), shallowEqual)
24 |
25 | /** 派发异步的事件: 发送网络请求 */
26 | const dispatch = useDispatch()
27 | useEffect(() => {
28 | dispatch(fetchHomeDataAction("xxxx"))
29 | dispatch(changeHeaderConfigAction({ isFixed: true, topAlpha: true }))
30 | }, [dispatch])
31 |
32 | return (
33 |
34 |
35 |
36 | {/* 折扣数据 */}
37 | {/*
38 |
39 |
40 |
41 |
*/}
42 | { isEmptyO(discountInfo) &&
}
43 | { isEmptyO(recommendInfo) && }
44 | { isEmptyO(longforInfo) && }
45 | { isEmptyO(goodPriceInfo) && }
46 | { isEmptyO(highScoreInfo) && }
47 | { isEmptyO(plusInfo) && }
48 |
49 |
50 | )
51 | })
52 |
53 | export default Home
54 |
--------------------------------------------------------------------------------
/src/components/room-item/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 |
4 | export const ItemWrapper = styled.div`
5 | flex-shrink: 0;
6 | box-sizing: border-box;
7 | width: ${props => props.itemWidth};
8 | padding: 8px;
9 |
10 | .inner {
11 | width: 100%;
12 | }
13 |
14 | .cover {
15 | position: relative;
16 | box-sizing: border-box;
17 | padding: 66.66% 8px 0;
18 | border-radius: 3px;
19 | overflow: hidden;
20 |
21 | img {
22 | position: absolute;
23 | left: 0;
24 | top: 0;
25 | width: 100%;
26 | height: 100%;
27 | object-fit: cover;
28 | }
29 | }
30 |
31 | .slider {
32 | position: relative;
33 | cursor: pointer;
34 |
35 | &:hover {
36 | .control {
37 | display: flex;
38 | }
39 | }
40 |
41 | .control {
42 | position: absolute;
43 | z-index: 1;
44 | left: 0;
45 | right: 0;
46 | top: 0;
47 | display: none;
48 | justify-content: space-between;
49 | bottom: 0;
50 | color: #fff;
51 |
52 | .btn {
53 | display: flex;
54 | justify-content: center;
55 | align-items: center;
56 | width: 83px;
57 | height: 100%;
58 | background: linear-gradient(to left, transparent 0%, rgba(0, 0, 0, 0.25) 100%);
59 |
60 | &.right {
61 | background: linear-gradient(to right, transparent 0%, rgba(0, 0, 0, 0.25) 100%);
62 | }
63 | }
64 | }
65 |
66 | .indicator {
67 | position: absolute;
68 | z-index: 9;
69 | bottom: 10px;
70 | left: 0;
71 | right: 0;
72 | width: 30%;
73 | margin: 0 auto;
74 |
75 | .item {
76 | display: flex;
77 | justify-content: center;
78 | align-items: center;
79 | width: 14.29%;
80 |
81 | .dot {
82 | width: 6px;
83 | height: 6px;
84 | background-color: #fff;
85 | border-radius: 50%;
86 |
87 | &.active {
88 | width: 8px;
89 | height: 8px;
90 | }
91 | }
92 | }
93 | }
94 | }
95 |
96 | .desc {
97 | margin: 10px 0 5px;
98 | font-size: 12px;
99 | font-weight: 700;
100 | color: ${props => props.verifyColor};
101 | }
102 |
103 | .name {
104 | font-size: 16px;
105 | font-weight: 700;
106 |
107 | overflow: hidden;
108 | text-overflow: ellipsis;
109 | display: -webkit-box;
110 | -webkit-line-clamp: 2;
111 | -webkit-box-orient: vertical;
112 | }
113 |
114 | .price {
115 | margin: 8px 0;
116 | }
117 |
118 | .bottom {
119 | display: flex;
120 | align-items: center;
121 | font-size: 12px;
122 | font-weight: 600;
123 | color: ${props => props.theme.text.primaryColor};
124 |
125 | .count {
126 | margin: 0 2px 0 4px;
127 | }
128 |
129 | .MuiRating-decimal {
130 | margin-right: -2px;
131 | }
132 | }
133 | `
--------------------------------------------------------------------------------
/src/base-ui/picture-browser/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 |
4 | export const BrowserWrapper = styled.div`
5 | position: fixed;
6 | z-index: 999; // -1 1 9 99 999
7 | left: 0;
8 | right: 0;
9 | top: 0;
10 | bottom: 0;
11 | display: flex;
12 | flex-direction: column;
13 |
14 | background-color: #333;
15 |
16 | .top {
17 | position: relative;
18 | height: 86px;
19 |
20 | .close-btn {
21 | position: absolute;
22 | top: 15px;
23 | right: 25px;
24 | cursor: pointer;
25 | }
26 | }
27 |
28 | .slider {
29 | position: relative;
30 | display: flex;
31 | justify-content: center;
32 | flex: 1;
33 |
34 | .control {
35 | position: absolute;
36 | z-index: 1;
37 | left: 0;
38 | right: 0;
39 | top: 0;
40 | bottom: 0;
41 | display: flex;
42 | justify-content: space-between;
43 | bottom: 0;
44 | color: #fff;
45 |
46 | .btn {
47 | display: flex;
48 | justify-content: center;
49 | align-items: center;
50 | width: 83px;
51 | height: 100%;
52 | cursor: pointer;
53 | }
54 | }
55 |
56 | .picture {
57 | position: relative;
58 | height: 100%;
59 | overflow: hidden;
60 | width: 100%;
61 | max-width: 105vh;
62 |
63 | img {
64 | position: absolute;
65 | top: 0;
66 | left: 0;
67 | right: 0;
68 | margin: 0 auto;
69 | height: 100%;
70 | user-select: none;
71 | }
72 |
73 | /* 动画的样式 */
74 | .pic-enter {
75 | transform: translateX(${props => props.isNext ? "100%": "-100%"});
76 | opacity: 0;
77 | }
78 |
79 | .pic-enter-active {
80 | transform: translate(0);
81 | opacity: 1;
82 | transition: all 200ms ease;
83 | }
84 |
85 | .pic-exit {
86 | opacity: 1;
87 | }
88 |
89 | .pic-exit-active {
90 | opacity: 0;
91 | transition: all 200ms ease;
92 | }
93 | }
94 | }
95 |
96 | .preview {
97 | display: flex;
98 | justify-content: center;
99 | height: 100px;
100 | margin-top: 10px;
101 |
102 | .info {
103 | position: absolute;
104 | bottom: 10px;
105 | max-width: 105vh;
106 | color: #fff;
107 |
108 | .desc {
109 | display: flex;
110 | justify-content: space-between;
111 |
112 | .toggle {
113 | cursor: pointer;
114 | }
115 | }
116 |
117 | .list {
118 | margin-top: 3px;
119 | overflow: hidden;
120 | transition: height 300ms ease;
121 | height: ${props => props.showList ? "67px": "0"};
122 |
123 | .item {
124 | margin-right: 15px;
125 | cursor: pointer;
126 |
127 | img {
128 | height: 67px;
129 | opacity: 0.5;
130 | }
131 |
132 | &.active {
133 | img {
134 | opacity: 1;
135 | }
136 | }
137 | }
138 | }
139 | }
140 | }
141 | `
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/components/room-item/index.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import React, { memo, useRef, useState } from 'react'
3 | import { Carousel } from 'antd';
4 | import { Rating } from '@mui/material'
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 = "25%", itemClick } = props
14 | const [selectIndex, setSelectIndex] = useState(0)
15 | const sliderRef = useRef()
16 |
17 | /** 事件处理的逻辑 */
18 | function controlClickHandle(isNext = true, event) {
19 | // 上一个面板/下一个面板
20 | isNext ? sliderRef.current.next(): sliderRef.current.prev()
21 |
22 | // 最新的索引
23 | let newIndex = isNext ? selectIndex + 1: selectIndex - 1
24 | const length = itemData.picture_urls.length
25 | if (newIndex < 0) newIndex = length - 1
26 | if (newIndex > length - 1) newIndex = 0
27 | setSelectIndex(newIndex)
28 |
29 | // 阻止事件冒泡
30 | event.stopPropagation()
31 | }
32 |
33 | function itemClickHandle() {
34 | if (itemClick) itemClick(itemData)
35 | }
36 |
37 | /** 子元素的赋值 */
38 | const pictureElement = (
39 |
40 |

41 |
42 | )
43 |
44 | const sliderElement = (
45 |
46 |
47 |
controlClickHandle(false, e)}>
48 |
49 |
50 |
controlClickHandle(true, e)}>
51 |
52 |
53 |
54 |
55 |
56 | {
57 | itemData?.picture_urls?.map((item, index) => {
58 | return (
59 |
60 |
61 |
62 | )
63 | })
64 | }
65 |
66 |
67 |
68 | {
69 | itemData?.picture_urls?.map(item => {
70 | return (
71 |
72 |

73 |
74 | )
75 | })
76 | }
77 |
78 |
79 | )
80 |
81 | return (
82 |
87 |
88 | { !itemData.picture_urls ? pictureElement: sliderElement }
89 |
90 | {itemData.verify_info.messages.join(" · ")}
91 |
92 |
{itemData.name}
93 |
¥{itemData.price}/晚
94 |
95 |
96 |
102 | {itemData.reviews_count}
103 | {
104 | itemData.bottom_info && ·{itemData.bottom_info?.content}
105 | }
106 |
107 |
108 |
109 | )
110 | })
111 |
112 | RoomItem.propTypes = {
113 | itemData: PropTypes.object
114 | }
115 |
116 | export default RoomItem
--------------------------------------------------------------------------------
/src/base-ui/picture-browser/index.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import React, { memo, useEffect, useState } from 'react'
3 | import { CSSTransition, SwitchTransition } from "react-transition-group"
4 |
5 | import IconArrowLeft from '@/assets/svg/icon-arrow-left'
6 | import IconArrowRight from '@/assets/svg/icon-arrow-right'
7 | import IconClose from '@/assets/svg/icon-close'
8 | import { BrowserWrapper } from './style'
9 | import IconTriangleArrowBottom from '@/assets/svg/icon-triangle-arrow-bottom'
10 | import Indicator from '../indicator'
11 | import classNames from 'classnames'
12 | import IconTriangleArrowTop from '@/assets/svg/icon-triangle-arrow-top'
13 |
14 | const PictureBrowser = memo((props) => {
15 | const { pictureUrls, closeClick } = props
16 | const [currentIndex, setCurrentIndex] = useState(0)
17 | const [isNext, setIsNext] = useState(true)
18 | const [showList, setShowList] = useState(true)
19 |
20 | // 当图片浏览器展示出来时, 滚动的功能消失
21 | useEffect(() => {
22 | document.body.style.overflow = "hidden"
23 | return () => {
24 | document.body.style.overflow = "auto"
25 | }
26 | }, [])
27 |
28 | /** 事件监听的逻辑 */
29 | function closeBtnClickHandle() {
30 | if (closeClick) closeClick()
31 | }
32 |
33 | function controlClickHandle(isNext = true) {
34 | let newIndex = isNext ? currentIndex + 1: currentIndex - 1
35 | if (newIndex < 0) newIndex = pictureUrls.length - 1
36 | if (newIndex > pictureUrls.length - 1) newIndex = 0
37 |
38 | setCurrentIndex(newIndex)
39 | setIsNext(isNext)
40 | }
41 |
42 | function bottomItemClickHandle(index) {
43 | setIsNext(index > currentIndex)
44 | setCurrentIndex(index)
45 | }
46 |
47 | return (
48 |
49 |
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 | {currentIndex+1}/{pictureUrls.length}:
80 | room apartment图片{currentIndex+1}
81 |
82 |
setShowList(!showList)}>
83 | {showList ? "隐藏": "显示"}照片列表
84 | { showList? : }
85 |
86 |
87 |
88 |
89 | {
90 | pictureUrls.map((item, index) => {
91 | return (
92 | bottomItemClickHandle(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/store/modules/detail.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit"
2 |
3 | const detailSlice = createSlice({
4 | name: "detail",
5 | initialState: {
6 | detailInfo: {
7 | "_id": "63043046432f9033d45410dc",
8 | "id": "49165669",
9 | "picture_url": "https://z1.muscache.cn/im/pictures/miso/Hosting-49165669/original/11aa11ee-e01f-4a53-8ba5-26de2e69502c.jpeg?aki_policy=large",
10 | "picture_urls": [
11 | "https://z1.muscache.cn/im/pictures/miso/Hosting-49165669/original/11aa11ee-e01f-4a53-8ba5-26de2e69502c.jpeg?aki_policy=large",
12 | "https://z1.muscache.cn/im/pictures/miso/Hosting-49165669/original/6cd1f284-c970-4822-b63e-74d9c8dcc188.jpeg?aki_policy=large",
13 | "https://z1.muscache.cn/im/pictures/miso/Hosting-49165669/original/b05f8093-d099-4556-bec1-05a123840565.jpeg?aki_policy=large",
14 | "https://z1.muscache.cn/im/pictures/miso/Hosting-49165669/original/affebcd2-2c92-47f8-89ef-6c6c10ee156a.jpeg?aki_policy=large",
15 | "https://z1.muscache.cn/im/pictures/miso/Hosting-49165669/original/350f6b5c-d1b2-4346-8708-c9a65fe388b9.jpeg?aki_policy=large",
16 | "https://z1.muscache.cn/im/pictures/miso/Hosting-49165669/original/dfa54e99-48c1-42e4-8875-b4b5d0be63fe.jpeg?aki_policy=large",
17 | "https://z1.muscache.cn/im/pictures/miso/Hosting-49165669/original/3250244b-dcb1-417d-8543-c51b65418d78.jpeg?aki_policy=large",
18 | "https://z1.muscache.cn/im/pictures/miso/Hosting-49165669/original/15cbbe6e-ee45-4da6-bbb6-05318eda48d7.jpeg?aki_policy=large",
19 | "https://z1.muscache.cn/im/pictures/miso/Hosting-49165669/original/9f0f1ad6-2074-4704-a3da-fb0976297cb6.jpeg?aki_policy=large",
20 | "https://z1.muscache.cn/im/pictures/miso/Hosting-49165669/original/a400860b-d132-44d3-b7c1-4c47a6640265.jpeg?aki_policy=large",
21 | "https://z1.muscache.cn/im/pictures/miso/Hosting-49165669/original/45e59644-bbc9-4fb4-a885-efae1f13d44f.jpeg?aki_policy=large",
22 | "https://z1.muscache.cn/im/pictures/miso/Hosting-49165669/original/846f059b-7821-4310-b92c-86a277285b41.jpeg?aki_policy=large",
23 | "https://z1.muscache.cn/im/pictures/miso/Hosting-49165669/original/f254f426-4de8-4c59-8db8-fbb48b2b4c38.jpeg?aki_policy=large",
24 | "https://z1.muscache.cn/im/pictures/miso/Hosting-49165669/original/ad1db07c-95dd-4041-9c3a-3c0c466d611a.jpeg?aki_policy=large",
25 | "https://z1.muscache.cn/im/pictures/miso/Hosting-49165669/original/456e8e25-c637-4752-a8e2-5256d713a7eb.jpeg?aki_policy=large",
26 | "https://z1.muscache.cn/im/pictures/miso/Hosting-49165669/original/f7875f98-63a4-4ded-ac6c-b4aa2374c6cd.jpeg?aki_policy=large",
27 | "https://z1.muscache.cn/im/pictures/miso/Hosting-49165669/original/1161ec7a-bf9c-44dd-b1eb-cebe0721796e.jpeg?aki_policy=large",
28 | "https://z1.muscache.cn/im/pictures/miso/Hosting-49165669/original/7f1b5682-09c2-457e-b183-6a7bd6703f90.jpeg?aki_policy=large",
29 | "https://z1.muscache.cn/im/pictures/miso/Hosting-49165669/original/7ff09eb9-af03-4d24-807b-c93ca0754e33.jpeg?aki_policy=large",
30 | "https://z1.muscache.cn/im/pictures/miso/Hosting-49165669/original/f81ddeb3-9d53-4250-9704-2a9349000b38.jpeg?aki_policy=large",
31 | "https://z1.muscache.cn/im/pictures/miso/Hosting-49165669/original/17ff1524-d2e9-4290-a3b8-24a24075c387.jpeg?aki_policy=large",
32 | "https://z1.muscache.cn/im/pictures/miso/Hosting-49165669/original/37912333-a59e-4227-84fd-686aa5e2ecb1.jpeg?aki_policy=large",
33 | "https://z1.muscache.cn/im/pictures/miso/Hosting-49165669/original/77062756-1379-47c6-b3b0-17ae44565eba.jpeg?aki_policy=large",
34 | "https://z1.muscache.cn/im/pictures/miso/Hosting-49165669/original/148b1051-e0eb-4d10-8f61-c3c107b1c41f.jpeg?aki_policy=large",
35 | "https://z1.muscache.cn/im/pictures/miso/Hosting-49165669/original/a81c7cf5-573b-441e-8003-f7c6d48c3c1a.jpeg?aki_policy=large",
36 | "https://z1.muscache.cn/im/pictures/miso/Hosting-49165669/original/8609c745-7754-40d6-a2c5-6ca4ef3c7271.jpeg?aki_policy=large",
37 | "https://z1.muscache.cn/im/pictures/miso/Hosting-49165669/original/33d49e74-e484-4c0e-9726-754e0047eaad.jpeg?aki_policy=large",
38 | "https://z1.muscache.cn/im/pictures/miso/Hosting-49165669/original/b8800a81-2929-4061-97e0-01b1993087d2.jpeg?aki_policy=large",
39 | "https://z1.muscache.cn/im/pictures/miso/Hosting-49165669/original/3de6b974-2c98-421e-82e2-bf51b886fda3.jpeg?aki_policy=large",
40 | "https://z1.muscache.cn/im/pictures/miso/Hosting-49165669/original/2a223b62-efab-4561-8ce2-37e4e2971d38.jpeg?aki_policy=large"
41 | ],
42 | "verify_info": {
43 | "messages": [
44 | "整套公寓型住宅",
45 | "1室1卫2床"
46 | ],
47 | "text_color": "#767676"
48 | },
49 | "name": "【轻奢Loft投影房&烟火】汉溪长隆、广州南站/番禺天河城百货~万达广场/南村万博地铁站、大学城",
50 | "price": 370,
51 | "price_format": "¥370",
52 | "star_rating": 5,
53 | "star_rating_color": "#FF5A5F",
54 | "reviews_count": 53,
55 | "reviews": [
56 | {
57 | "comments": "房间空间很大,装修的风格很大气,床品有档次,当地的美食很多,在这玩的很开心",
58 | "created_at": "2022-06-16T00:00:00Z",
59 | "is_translated": false,
60 | "localized_date": "2022年6月",
61 | "reviewer_image_url": "https://a0.muscache.com/im/pictures/user/bd514790-41dd-4bd3-beb1-3ad2aea5152c.jpg?aki_policy=x_medium",
62 | "review_id": 650154898638268400
63 | }
64 | ],
65 | "bottom_info": null,
66 | "lat": 23.01087,
67 | "lng": 113.34489,
68 | "image_url": "/moreitems/ad0e5254433cb33ad77a035475f10782.jpg"
69 | }
70 | },
71 | reducers: {
72 | changeDetailInfoAction(state, { payload }) {
73 | state.detailInfo = payload
74 | }
75 | }
76 | })
77 |
78 | export const { changeDetailInfoAction } = detailSlice.actions
79 | export default detailSlice.reducer
80 |
--------------------------------------------------------------------------------
/src/assets/svg/icon_logo.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import styleStrToObject from './utils'
3 |
4 | const IconLogo = memo(() => {
5 | return (
6 |
7 | )
8 | })
9 |
10 | export default IconLogo
--------------------------------------------------------------------------------