前言
本来刚接触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的数据值。