React 实现点击字母与滑动城市联动

前言

        本来刚接触react后想边学边写个项目实战一下,然后就想做个联动功能实战一下,一做才知道一大堆问题跟vue差距很大,然后就去百度到处找别人怎么写,一找,要不就是不全,乱用组件,要不就是乱写一通根本用不了,于是,这里才想记录一下,代码在此全部贴上。

挂个效果图先

功能分析

将所有城市按首字母进行顺序排列,点击右侧对应的字母快,左侧对应的城市会对应的滑动到顶部,滑动左侧的位置时,右侧的字母也会对应进行高亮。

!!!贴个全部代码先

代码内容(基本上没有用到什么其他组件,就一个react-vant,自己看手册安装就行,以下有链接)

react-vant地址:https://react-vant.3lang.dev/guide/quickstart

import SmHeader from "@/components/SmHeader"
import { useEffect, useState, useRef } from "react"
import { Replay } from '@react-vant/icons'
import { getRequest } from "@/hooks/network"
import { Skeleton, Empty } from 'react-vant';

import "./index.less"

export default function LocationCity() {
	const scrollRef = useRef(null)
	const repositioningRef = useRef(null)
	const hotsRef = useRef(null)
	const currentRef = useRef(null)

	const [isInit, setIsInit] = useState(true)
	const [hotsList, setHotsList] = useState([])
	const [zimu, setZimu] = useState([])
	const [city, setCity] = useState({})
	const [zimuIndex, setZimuIndex] = useState(0)  //右侧字母索引,方便进行点击高亮

	const [scrollHeight, setScrollHeight] = useState([])  //左侧城市高度
	const [residueHeight, setResidueHeight] = useState("") //所在城市之外多余高度,为了点击让城市根据首字母在顶部开始展示

	// 获取城市列表
	const fetchCity = () => {
		getRequest('/cities.json').then(res => {
			setHotsList(res.hot)
			if (res.cts.length) {
				let format_city = {}
				let zm_list = []
				res.cts.forEach(item => {
					let zm = item.py.charAt(0).toLocaleUpperCase()
					if (!format_city[zm]) {
						zm_list.push(zm)
						format_city[zm] = []
					}
					format_city[zm].push(item)
				})
				zm_list.sort()
				zm_list.unshift("Top")
				setZimu(zm_list)
				setCity(format_city)
			}
			setIsInit(false)
		})
	}

	// 获取左侧城市高度
	const getHeight = () => {
		const repositioning_Height = repositioningRef.current && repositioningRef.current.clientHeight  //重新定位高度
		const hots_Height = hotsRef.current && hotsRef.current.clientHeight   //热门城市高度
		const current_Height = currentRef.current && currentRef.current.clientHeight  //所在城市标题高度
		const residue_Height = repositioning_Height + hots_Height + current_Height //多余总高度
		setResidueHeight(residue_Height)

		let height = 0
		let cityHeight = []
		let cityItems = document.querySelectorAll(".city-item")

		cityItems.forEach(item => {
			height += item.clientHeight
			cityHeight.push(height)
		})

		setScrollHeight(cityHeight)
	}

	// 点击右侧字母定位左侧位置
	const onSelectZimu = (index) => {
		setZimuIndex(index)
		scrollRef.current.scrollTo({
			top: index === 0 ? 0 : residueHeight + scrollHeight[index - 1],
			behavior: 'smooth'
		})
	}

	useEffect(() => {
		fetchCity()
	}, []);

	// 此处是为了解决getHeight内一直获取不到页面挂载后dom的问题,这里给一个初始值,当数据加载完后才来调用getHeight()
	useEffect(() => {
		if (!isInit) {
			getHeight()
		}
	}, [isInit]);

	useEffect(() => {
		if (!isInit) {
			// 滚动定位右侧字母列
			const handleScroll = () => {
				if (scrollRef.current) {
					const scrollTop = scrollRef.current.scrollTop;
					for (let i = 0; i < scrollHeight.length; i++) {
						let current_scroll_top = scrollTop - residueHeight + 1
						if (scrollHeight[i] < current_scroll_top && current_scroll_top < scrollHeight[i + 1]) {
							setZimuIndex(i + 1)
							break
						} else if (current_scroll_top < scrollHeight[i]) {
							setZimuIndex(0)
							break
						}
					}
				}
			}

			const scrollElement = scrollRef.current;
			if (scrollElement) {
				scrollElement.addEventListener('scroll', handleScroll);
				return () => {
					scrollElement.removeEventListener('scroll', handleScroll);
				}
			}
		}
	}, [isInit, scrollHeight, residueHeight]);

	return (
		<div className="location-city-page" ref={scrollRef}>
			<SmHeader title="定位城市"></SmHeader>
			<Skeleton row={20} loading={isInit}>
				<div className="repositioning" ref={repositioningRef}>
					<div className="position-name">广州</div>
					<div className="position-refresh">
						<Replay fontSize="16" />
						重新定位
					</div>
				</div>
				<div className="container hots-city" ref={hotsRef}>
					<div className="public-header">
						<div className="title">热门城市</div>
					</div>
					{
						hotsList.length ? (
							<div className="flex flex-wrap mt-[15px]">
								{hotsList.map((item, index) => (
									<div className="city-tag" key={index}>{item}</div>
								))}
							</div>
						) : (<Empty description="暂无数据" />)
					}
				</div>
				<div className="public-header current-city" style={{ padding: "10px 14px" }} ref={currentRef}>
					<div className="title">所在城市</div>
				</div>
				{
					zimu.length ? (
						<div className="city-lists">
							{zimu.map((zm, idx) => (
								<div key={idx} className="city-item">
									<div className="zm-row">{zm}</div>
									{city[zm] && city[zm].map((item, index) => (
										<div className="city-row" key={index}>{item.nm}</div>
									))}
								</div>
							))}
						</div>
					) : (<Empty description="暂无数据" />)
				}

				{/* 定位字母导航 */}
				{
					zimu.length && (<div className="right-navs">
						{zimu.map((item, index) => (
							<div className={[zimuIndex === index ? "active" : ""]} key={index} onClick={() => onSelectZimu(index)}>{item}</div>
						))}
					</div>)
				}
			</Skeleton>
		</div>
	)
}

样式less (有部分公共头部标题样式放其他地方就没挂上,还有一些变量是我自己声明的也没挂上,有需要的自己后续改一下就行)

.location-city-page{
	overflow-y: auto;   //因为点击字母为了让整体进行滚动,这里一定要设置y轴超出可以滚动
	height: 100vh;
	.repositioning{
		padding: var(--sm-padding-sm) var(--sm-padding-md);
		display: flex;
		justify-content: space-between;
		align-items: flex-start;
		border-bottom: 1px solid var(--sm-border-color);
		font-weight: bold;
		.position-name{
			flex: 1;
		}
		.position-refresh{
			margin-left: 20px;
			display: flex;
			align-items: center;
			color: var(--sm-primary-color-lighten);
		}
	}
	.city-tag{
		padding: 2px 13px;
		margin-bottom: 10px;
		margin-right: 10px;
		background-color: var(--sm-background-color);
	}
	.city-lists{
		.city-item{
			&:nth-child(1){
				display: none;
			}
			.zm-row{
				background-color: #ebebeb;
				padding: 5px var(--sm-padding-md);
				border-bottom: 1px solid var(--sm-border-color);	
			}
			.city-row{
				background-color: #fff;
				padding:8px var(--sm-padding-md);
				border-bottom: 1px solid var(--sm-border-color);
			}
		}
	}
	.right-navs{
		position: fixed;
		top: calc(50% + 23px);
		right: 5px;
		transform: translateY(-50%);
		display: flex;
		flex-direction: column;
		align-items: center;
		font-size: var(--sm-font-size-sm);
		padding: 20px 6px;
		background: var(--sm-primary-gradient-bottom);
		color: #fff;
		border-radius: 20px;
		.active{
			color: var(--sm-primary-color-lighten-second);
		}
	}
}

以上代码是全部,拿上基本可用。

以下是我开发遇到的问题:(按我本人开发并查完资料的理解)

小提醒:这里获取的城市数据结构是这样的,所以需要处理成自己需要的数据,顺便去掉没有对应的城市数据的首字母。

1、使用scrollTo()时,需要给要滚动的domd的样式增加一个overflow-y: auto;和高度,不然滚动不了。

2、react获取dom跟vue的区别,vue一般是页面挂载完后就可以通过ref获取,但是react中,虽说像useEffect也是在组件挂载后执行,但它并不是一直都是渲染完在执行,像这个案例,我一开始是这个用的:

那问题就来,因为fetchCity()是用于异步获取数据,所以这里直接用会导致getHeight()先加载, 再去加载fetchCity(),而getHeight()其实就是存储dom的一些的数据,那就会导致数据页面还没渲染出来就去拿dom,那自然就拿空了,所以我一开始试了好多种方式都不行,后面才了解到可以给定fetchCity()一个状态,当它加载完后再来调用这个getHeight(),下面是解决方案。

同理,当检测左侧城市块滚动的时候也需要如此操作才能获取更新后的dom的数据值。

  • 8
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
下面是一个简单的React实现四级联动的例子: ```jsx import React, { useState } from "react"; const options = { "浙江省": { "杭州市": { "西湖区": ["西湖街道", "灵隐街道"], "上城区": ["山北路街道", "南星街道"] }, "宁波市": { "海曙区": ["山东路街道", "白云街道"], "江东区": ["庄桥街道", "七塘街道"] } }, "江苏省": { "南京市": { "玄武区": ["梅园新村街道", "红山街道"], "秦淮区": ["夫子庙街道", "集庆门街道"] }, "苏州市": { "姑苏区": ["虎丘街道", "石路街道"], "吴区": ["香山街道", "越溪街道"] } } }; function Cascader() { const [province, setProvince] = useState("浙江省"); const [city, setCity] = useState("杭州市"); const [district, setDistrict] = useState("西湖区"); const [street, setStreet] = useState("西湖街道"); const handleProvinceChange = (e) => { setProvince(e.target.value); setCity(Object.keys(options[e.target.value])[0]); setDistrict(Object.keys(options[e.target.value][Object.keys(options[e.target.value])[0]])[0]); setStreet(options[e.target.value][Object.keys(options[e.target.value])[0]][Object.keys(options[e.target.value][Object.keys(options[e.target.value])[0]])[0]][0]); }; const handleCityChange = (e) => { setCity(e.target.value); setDistrict(Object.keys(options[province][e.target.value])[0]); setStreet(options[province][e.target.value][Object.keys(options[province][e.target.value])[0]][0]); }; const handleDistrictChange = (e) => { setDistrict(e.target.value); setStreet(options[province][city][e.target.value][0]); }; const handleStreetChange = (e) => { setStreet(e.target.value); }; return ( <div> <select value={province} onChange={handleProvinceChange}> {Object.keys(options).map((option) => ( <option key={option} value={option}> {option} </option> ))} </select> <select value={city} onChange={handleCityChange}> {Object.keys(options[province]).map((option) => ( <option key={option} value={option}> {option} </option> ))} </select> <select value={district} onChange={handleDistrictChange}> {Object.keys(options[province][city]).map((option) => ( <option key={option} value={option}> {option} </option> ))} </select> <select value={street} onChange={handleStreetChange}> {options[province][city][district].map((option) => ( <option key={option} value={option}> {option} </option> ))} </select> </div> ); } export default Cascader; ``` 在这个例子,我们使用了`useState`来管理四个状态:`province`、`city`、`district`和`street`,分别代表省、市、区和街道。我们通过`select`元素来实现四级联动,每一级的选项都是根据上一级的选项来动态生成的。 当省份选项发生改变时,我们需要根据选的省份来动态生成城市选项,同时也需要把城市、区和街道的选项重置为默认值。当城市选项发生改变时,我们需要根据选城市来动态生成区选项,并重置街道选项为默认值。当区选项发生改变时,我们需要根据选的区来动态生成街道选项。当街道选项发生改变时,我们只需要更新`street`状态的值即可。 上述代码只是一个简单的例子,实际情况下可能会更加复杂。但是原理是相同的:通过状态来实现四级联动,根据当前选的选项来动态生成下一级选项。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值