实现: 随着向下滚动标签出现并横向滑动到某个tab相应标题选中,点击tab标题滑动到相应内容
博客园不能录视频只能图片模拟成品,如果想要看视频效果留言给我
实现如下:
点击查看代码
// index.tsx文件
// 所涉及代码
import React, { useEffect, useState } from 'react'
import { View, Image, Video, Swiper, SwiperItem, Button, ScrollView } from '@tarojs/components'
import { usePageScroll, createSelectorQuery, pageScrollTo, useReady, useRouter } from '@tarojs/taro'
import { useThrottleFn } from 'ahooks'
import c from 'classnames'
import Skeleton from 'taro-skeleton'
import MainLayout from '@/layout/MainLayout'
import { fetchDeatil, IResourceList } from '@/apis/detail'
import { projectArrs, ITaptaneTopArr } from './const'
import './index.scss'
const Detail: React.FC = () => {
const [projectDetail, setProjectDetail] = useState<any>({})
const [project, setProject] = useState({})
const [tabFixed, setTabFixed] = useState(false)
const [top, setTop] = useState(0)
const [tabScrollTop, setScrollTop] = useState(0)
const [taptaneTopArr, setTaptaneTopArr] = useState([])
const [projectArr, setProjectArr] = useState(projectArrs)
const [navScrollLeft, setNavScrollLeft] = useState(0)
const query = createSelectorQuery()
const router = useRouter()
const scrollHanlder = res => {
const { scrollTop } = res
setTop(scrollTop)
if (scrollTop > tabScrollTop) {
setTabFixed(true)
} else {
setTabFixed(false)
}
for (let i = 0; i < projectArr.length; i++) {
let currentTaptaneTop = 0
let nextTaptaneTop = 0
let currentItem = projectArr[i]
if (i != projectArr.length - 1) {
let nextItem = projectArr[i + 1]
for (const list of taptaneTopArr) {
if (currentItem.id === list.id) {
currentTaptaneTop = list.taptaneTop
}
if (nextItem.id === list.id) {
nextTaptaneTop = list.taptaneTop
}
}
if (scrollTop >= currentTaptaneTop - 47 - 15 && scrollTop < nextTaptaneTop - 47 - 15) {
if (i === 0) {
setNavScrollLeft(-16 + Math.random())
}
currentItem.isSelected = true
} else {
currentItem.isSelected = false
}
} else {
for (const list of taptaneTopArr) {
if (currentItem.id === list.id) {
currentTaptaneTop = list.taptaneTop
}
}
if (scrollTop >= currentTaptaneTop - 47 - 15) {
setNavScrollLeft(state => {
currentItem.isSelected = true
return 400 + Math.random()
})
} else {
currentItem.isSelected = false
}
}
}
setProjectArr([...projectArr])
}
const { run } = useThrottleFn(scrollHanlder, { wait: 1 })
usePageScroll(res => run(res))
useReady(() => {
// id是tabs距离顶部的距离
query
.select('#tabs')
.boundingClientRect(res => {
setScrollTop(res?.top)
})
.exec()
})
const { run: onTab } = useThrottleFn(
e => {
const { id } = e.target.dataset
pageScrollTo({
selector: `#${id}`,
offsetTop: -40,
// duration: 30
})
},
{ wait: 300 }
)
useEffect(() => {
const id = Number(router.params.id)
fetchDeatil(id).then((data: any) => {
setProjectDetail(data.projectDetail)
setProject(data.project)
setTimeout(() => {
const arr: ITaptaneTopArr[] = []
projectArr.map(item => {
query.select(`#${item.id}`).boundingClientRect(res => {
arr.push({ id: item.id, taptaneTop: res.top })
})
})
query.exec()
setTaptaneTopArr(arr)
}, 10)
})
}, [])
return (
<MainLayout className="detail">
{project?.auditState == 2 && (
<>
<ScrollView
className={c('scrollview', tabFixed ? 'tabtop' : '')}
id="tabs"
scrollX
enhanced
showScrollbar
refresherDefaultStyle={'white'}
scrollWithAnimation
scrollAnchoring
scrollLeft={navScrollLeft}
onClick={onTab}
>
<View className="wrapView" style={{ paddingRight: 1 }}>
{projectArr.map((list, index) => (
<View
key={list.id}
data-id={list.id}
data-index={index}
className={c('tabtaneCls', list.isSelected ? 'active' : '')}
>
{list.title}
</View>
))}
</View>
</ScrollView>
{projectArr.map(item => (
<Skeleton loading={false} title row={6} key={item.key}>
<View className="content" id={item.id}>
<View className="title">{item.title}</View>
<View className="table">
{item.lists.map(item => (
<View key={item.key} className="tr">
<View className="label">{item.label}</View>
<View className="value">{projectDetail[item.key]}</View>
</View>
))}
</View>
</View>
</Skeleton>
))}
<View className="paceHolderBom">
<View style={{ height: '150PX', background: '#fff' }} />
</View>
</>
)}
</MainLayout>
)
}
export default Detail
const.ts文件
type Txt = number | string;
export interface Idesc {
label: string;
key: string;
render?: (text: Txt) => Txt;
}
const salaryArr: Idesc[] = [
{
label: '薪资',
key: 'postSalary',
},
{
label: '工资计算',
key: 'salaryCalculationMethod',
},
{
label: '发薪时间',
key: 'payday',
},
{
label: '多久转正',
key: 'howLongCorrection'
},
{
label: '发工资银行卡',
key: 'bankCardName',
},
{
label: '其他津贴',
key: 'otherAllowances',
},
{
label: '多久可以离职',
key: 'howLongLeave',
},
{
label: '提前多久打离职报告',
key: 'advancePrintResignationReport',
},
{
label: '工作需缴纳费用',
key: 'workDeposit',
},
// {
// label: '更新日期',
// key: 'updateTime',
// render(text) {
// return moment(text).format('YYYY-MM-DD');
// },
// },
{
label: '合同签订',
key: 'contractSigningMethod',
},
{
label: '企业福利',
key: 'enterpriseWelfare'
},
{
label: '是否有试用期',
key: 'isProbationPeriod',
},
{
label: '是否缴社保',
key: 'isSocialSecurity',
render(text) {
return text;
},
},
{
label: '是否好离职',
key: 'easyLeave',
},
{
label: '是否有培训',
key: 'isTraining',
},
{
label: '自离是否有工资',
key: 'selfLeaveSalary',
},
{
label: '是否扣商报',
key: 'isDeductBusinessDaily',
},
];
const requestArr: Idesc[] = [
{
label: '年龄',
key: 'postSalary',
},
{
label: '性别',
key: 'salaryCalculationMethod',
},
{
label: '学历',
key: 'payday',
},
{
label: '身高',
key: 'howLongCorrection'
},
{
label: '视力',
key: 'bankCardName',
},
{
label: '地域',
key: 'otherAllowances',
},
// {
// label: '民族',
// key: 'howLongLeave',
// },
{
label: '自离几次是黑名单',
key: 'advancePrintResignationReport',
},
{
label: '前几个月不能进厂',
key: 'workDeposit',
},
{
label: '超龄是否可以协调',
key: 'contractSigningMethod',
},
{
label: '残疾人是否可以',
key: 'enterpriseWelfare'
},
{
label: '纹身烟疤是否可以',
key: 'isProbationPeriod',
},
{
label: '身份证过期是否可以',
key: 'isSocialSecurity',
render(text) {
return text;
},
}
];
const workArr: Idesc[] = [
{
label: '工作性质',
key: 'jobNature',
},
{
label: '工作内容',
key: 'workContent',
},
{
label: '车间环境',
key: 'workshopEnvironment',
},
{
label: '岗位',
key: 'whatPositions'
},
{
label: '工作服',
key: 'coverall',
},
{
label: '生产产品',
key: 'production',
},
{
label: '工作方式',
key: 'operationMode',
},
{
label: '上班形式',
key: 'workForm',
},
{
label: '月休息天数',
key: 'monthlyRestDays',
},
{
label: '工作时间',
key: 'workingHours',
},
{
label: '是否流水线',
key: 'isAssemblyLine'
},
{
label: '是否双休',
key: 'isWeekend',
}
];
const envArr: Idesc[] = [
{
label: '餐食提供',
key: 'mealProvision',
},
{
label: '餐食标准',
key: 'mealStandard',
},
{
label: '餐食扣款',
key: 'mealDeductMethod',
},
{
label: '餐食补助',
key: 'mealAllowance'
},
{
label: '住宿',
key: 'accommodation',
},
{
label: '外宿补贴',
key: 'accommodationAllowance',
},
{
label: '班车费用',
key: 'shuttleBusCost',
},
{
label: '水电费',
key: 'waterAndElectricity',
},
{
label: '是否可以刷卡吃饭',
key: 'eatCard',
},
{
label: '非工作时厂区可否吃饭',
key: 'eatInFactory',
},
{
label: '是否有班车',
key: 'isShuttleBus'
}
];
const interviewArr: Idesc[] = [
{
label: '面试地点',
key: 'collectionPlace',
},
{
label: '面试时间',
key: 'interviewTime',
},
{
label: '面试后体检时间',
key: 'physicalExaminationAfterInterview',
},
{
label: '面试后报到时间',
key: 'reportTimeAfterInterview'
},
{
label: '报到资料',
key: 'carryData',
},
{
label: '工人到达时间',
key: 'requiredArrivalTime',
},
{
label: '面试资料',
key: 'interviewInformation',
},
{
label: '面试项目',
key: 'interviewItems',
},
{
label: '体检费用',
key: 'physicalExaminationCostMethod',
},
{
label: '体检医院',
key: 'physicalExaminationHospital',
},
{
label: '是否提供接站',
key: 'isPickUpStation'
},
{
label: '临时身份证可否',
key: 'isTemporaryIdCard',
},
{
label: '身份证消磁可否',
key: 'isDegaussingIdCard'
},
{
label: '是否查学信网',
key: 'checkEducationNetwork',
},
{
label: '是否查纹身烟疤',
key: 'checkTattooScar'
},
{
label: '是否查案底',
key: 'checkCriminalRecord',
},
{
label: '是否需要社会工证明',
key: 'socialWorkerCertificate'
},
{
label: '是否安排临时住宿',
key: 'temporaryAccommodation',
},
{
label: '是否需要体检',
key: 'isPhysicalExamination'
},
{
label: '自带体检报告健康证可否',
key: 'selfPhysicalExaminationReport',
},
{
label: '是否需要健康证',
key: 'isHealthCertificate'
}
];
const firmArr: Idesc[] = [
{
label: '企业名称',
key: '',
},
{
label: '企业性质',
key: 'enterpriseNature',
},
{
label: '企业规模',
key: 'enterpriseScale',
},
{
label: '生产产品',
key: 'enterpriseProduct'
},
{
label: '乘车路线',
key: 'busLine',
},
{
label: '企业简介',
key: 'enterpriseIntroduction',
},
{
label: '周边是否有商场',
key: 'nearbyMarket',
},
{
label: '厂区是否偏僻',
key: 'isFactoryRemote',
},
{
label: '附近是否有幼儿园',
key: 'nearbyKindergarten',
}
];
export const projectArrs = [
{
title: '薪资福利',
lists: salaryArr,
key: 'salary',
id: 'salary',
isSelected: false
},
{
title: '招聘要求',
lists: requestArr,
key: 'request',
id: 'request',
isSelected: false
},
{
title: '工作内容',
lists: workArr,
key: 'work',
id: 'work',
isSelected: false
},
{
title: '食宿环境',
lists: envArr,
key: 'environment',
id: 'environment',
isSelected: false
},
{
title: '面试体检',
lists: interviewArr,
key: 'interview',
id: 'interview',
isSelected: false
},
{
title: '企业信息',
lists: firmArr,
key: 'firm',
id: 'firm',
isSelected: false
}
]
enum SalaryUnitType {
MOON = 1,
DAY,
HOUR
}
export const salaryUnit = {
[SalaryUnitType.MOON]: '/月',
[SalaryUnitType.DAY]: '/日',
[SalaryUnitType.HOUR]: '/时'
}
index.scss
// 所涉及的样式
.nav {
display: none;
background-color: #F6F6F6;
border-top: 1PX solid rgba(0, 0, 0, 0.1);
border-bottom: 1PX solid rgba(0, 0, 0, 0.1);
.tabtaneCls{
position: relative;
font-size: 28px;
font-weight: 400;
color: #666666;
line-height: 20px;
&::after {
position: absolute;
right: 20px;
bottom: -8PX;
left: 20px;
border-bottom: 2px solid transparent;
transition: border-color .3s cubic-bezier(.645,.045,.355,1);
content: "";
}
}
.active {
color:#FF6600;
font-weight: bolder;
&::after {
border-bottom: 2PX solid #FF6600;
}
}
}
// }
.scrollview {
display: none;
height: 94px;
white-space: nowrap;
z-index: 9999;
// padding-left: 16PX;
background-color: #F6F6F6;
border-top: 1PX solid rgba(0, 0, 0, 0.1);
// border-bottom: 1PX solid rgba(0, 0, 0, 0.1);
.tabtaneCls {
display: inline-block;
line-height: 47PX;
position: relative;
font-size: 28px;
font-weight: 400;
color: #666666;
&::after {
position: absolute;
right: 20px;
bottom: 5PX;
left: 20px;
border-bottom: 2px solid transparent;
transition: border-color .3s cubic-bezier(.645,.045,.355,1);
content: "";
}
&:nth-child(1) {
margin-left: 16PX;
}
&:nth-child(n+2) {
margin-left: 20PX;
}
&:last-child{
margin-right: 20PX;
}
}
.active {
color:#FF6600;
font-weight: bolder;
&::after {
border-bottom: 2PX solid #FF6600;
}
}
}
.paceHolderBom {
background: #fff;
padding-bottom: calc(26PX + env(safe-area-inset-bottom))
}
.tabtop {
display: flex;
align-items: center;
justify-content: space-around;
margin-top: 6PX;
width: 100%;
height: 94px;
background: #FFFFFF;
box-shadow: 0px 1px 0px 0px #E9E9E9;
position: fixed;
top: 0;
margin-top: 0;
}
.wrapView::-webkit-scrollbar {
display: none!important;
}