feat: 时间线

This commit is contained in:
mozzie 2023-03-01 11:18:24 +08:00
parent 5b3f929c39
commit eb593ef590
7 changed files with 146 additions and 105 deletions

View File

@ -0,0 +1,5 @@
.recommends {
display: grid;
grid-template-columns: 1fr 1.5fr 1fr;
grid-column-gap: 20px;
}

View File

@ -0,0 +1,47 @@
import { useState } from "react";
import BsCard from "../../../../components/Card";
import "./index.less";
export const recommendListDefault = [
{
imgUrl:
"https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/a8c8cdb109cb051163646151a4a5083b.png~tplv-uwbnlip3yd-webp.webp",
title: "这个非常OK啊",
meta: {
desc: "推荐内容推荐内容推荐内容推荐内容推荐内容推荐内容推荐内容",
action: "Learn",
},
},
{
imgUrl:
"https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/e278888093bef8910e829486fb45dd69.png~tplv-uwbnlip3yd-webp.webp",
title: "这个非常OK啊",
meta: {
desc: "推荐内容推荐内容推荐内容推荐内容推荐内容推荐内容推荐内容",
action: "Learn",
},
},
{
imgUrl:
"https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/a8c8cdb109cb051163646151a4a5083b.png~tplv-uwbnlip3yd-webp.webp",
title: "这个非常OK啊",
meta: {
desc: "推荐内容推荐内容推荐内容推荐内容推荐内容推荐内容推荐内容",
action: "Learn",
},
},
];
function Recommends() {
const [recommendList, setRecommendList] = useState(recommendListDefault);
return (
<div className="recommends">
{recommendList.map((item, index) => (
<BsCard {...item} key={index} />
))}
</div>
);
}
export default Recommends;

View File

@ -1,5 +1,4 @@
.timescroll { .timeline {
position: relative;
width: 40px; width: 40px;
.caret { .caret {

View File

@ -33,7 +33,7 @@ const filterYearOnce = (data: any[]) => {
}); });
}; };
function TimeScroll(props: IProps) { function Timeline(props: IProps) {
const [cursorStatic, setCursorStatic] = useState({ const [cursorStatic, setCursorStatic] = useState({
top: -4, top: -4,
color: "var(--color-fill-4)", color: "var(--color-fill-4)",
@ -43,6 +43,7 @@ function TimeScroll(props: IProps) {
color: "var(--color-border-3)", color: "var(--color-border-3)",
}); });
const [intervalPixel, setIntervalPixel] = useState<number>(1); const [intervalPixel, setIntervalPixel] = useState<number>(1);
const [timelineData, setTimelineData] = useState(filterYearOnce(props.data));
const cursorActiveRef = useRef<HTMLDivElement | null>(null); const cursorActiveRef = useRef<HTMLDivElement | null>(null);
const cursorStaticRef = useRef<HTMLDivElement | null>(null); const cursorStaticRef = useRef<HTMLDivElement | null>(null);
const orbitRef = useRef<HTMLDivElement | null>(null); const orbitRef = useRef<HTMLDivElement | null>(null);
@ -79,8 +80,6 @@ function TimeScroll(props: IProps) {
}); });
}, [cursorStatic.top]); }, [cursorStatic.top]);
const [timelineData, setTimelineData] = useState(filterYearOnce(props.data));
useEffect(() => { useEffect(() => {
if (props.data) { if (props.data) {
setTimelineData(filterYearOnce(props.data)); setTimelineData(filterYearOnce(props.data));
@ -104,7 +103,7 @@ function TimeScroll(props: IProps) {
return ( return (
<div <div
className={`timescroll ${props.className}`} className={`timeline ${props.className}`}
onMouseMove={onMouseMove} onMouseMove={onMouseMove}
onMouseDown={onMouseDown} onMouseDown={onMouseDown}
> >
@ -157,4 +156,4 @@ function TimeScroll(props: IProps) {
); );
} }
export default TimeScroll; export default Timeline;

View File

@ -3,30 +3,21 @@
flex-direction: column; flex-direction: column;
padding: 100px 0 0 0; padding: 100px 0 0 0;
height: 100vh; height: 100vh;
.recommends {
display: grid;
grid-template-columns: 1fr 1.5fr 1fr;
grid-column-gap: 20px;
}
.action-bar { .action-bar {
padding: 40px 0 20px 0; margin-bottom: 20px;
display: flex; text-align: right;
align-items: center;
justify-content: flex-end;
.table-action {
line-height: 1; line-height: 1;
color: var(--color-text-2); color: var(--color-text-2);
.xicon { .xicon {
display: initial; display: initial;
} }
} }
}
.timeline { .thumbnail-timeline {
flex: 1; flex: 1;
position: relative; position: relative;
.thumbnail { .thumbnail-container {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@ -52,7 +43,7 @@
display: grid; display: grid;
grid-column-gap: 10px; grid-column-gap: 10px;
grid-row-gap: 10px; grid-row-gap: 10px;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
.bs-card { .bs-card {
border-radius: 3px; border-radius: 3px;
.cover { .cover {
@ -64,11 +55,29 @@
} }
} }
} }
&.table {
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
}
&.tab {
grid-template-columns: 1fr;
.bs-card {
border-radius: 3px;
.cover {
height: 330px;
}
.mask {
p {
font-size: 15px;
} }
} }
} }
} }
.timescroll { }
}
}
}
.timeline-container {
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;

View File

@ -11,25 +11,25 @@ import {
Menu, Menu,
} from "@arco-design/web-react"; } from "@arco-design/web-react";
import BsCard from "../../components/Card"; import BsCard from "../../components/Card";
import TimeScroll, { IOnScrollParam } from "../../components/TimeScroll"; import Timeline, { IOnScrollParam } from "./components/Timeline";
import Tab20Regular from "@ricons/fluent/Tab20Regular"; import Tab20Regular from "@ricons/fluent/Tab20Regular";
import Table20Regular from "@ricons/fluent/Table20Regular"; import Table20Regular from "@ricons/fluent/Table20Regular";
import Filter20Regular from "@ricons/fluent/Filter20Regular"; import Filter20Regular from "@ricons/fluent/Filter20Regular";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { recommendListDefault, courseTimeListDefault } from "./mock"; import { courseTimeListDefault } from "./mock";
import { Icon } from "@ricons/utils"; import { Icon } from "@ricons/utils";
export default function Index() { export default function Index() {
const scrollContainerRef = useRef<HTMLElement | null>(null); const thumbnailRef = useRef<HTMLElement | null>(null);
const ratioRef = useRef<number>(1); const scale = useRef<number>(1); // thumbnail / timeline 高度比例
const [timescroll, setTimescroll] = useState({ const [timeline, setTimeline] = useState({
top: -4, top: -4,
}); });
const dropList = ( const dropList = (
<Menu> <Menu>
<Menu.Item key="1"></Menu.Item> <Menu.Item key="asec"></Menu.Item>
<Menu.Item key="2"></Menu.Item> <Menu.Item key="desc"></Menu.Item>
</Menu> </Menu>
); );
@ -38,59 +38,64 @@ export default function Index() {
key: "tab", key: "tab",
icon: <Tab20Regular />, icon: <Tab20Regular />,
active: false, active: false,
tip: "单格排列", tip: "使用大缩略图显示单个项目",
gridClass: "tab",
}, },
{ {
key: "table", key: "table",
icon: <Table20Regular />, icon: <Table20Regular />,
active: true, active: true,
tip: "缩略", tip: "列出更多项目",
gridClass: "table",
}, },
]); ]);
const [recommendList, setRecommendList] = useState(recommendListDefault);
const [courseTimeList, setCourseTimeList] = useState(courseTimeListDefault); const [courseTimeList, setCourseTimeList] = useState(courseTimeListDefault);
const onClickActionItem = (action: any) => { const onClickActionItem = (action: any) => {
setActions((p) => p.map((a) => ({ ...a, active: a.key === action.key }))); setActions((p) => p.map((a) => ({ ...a, active: a.key === action.key })));
}; };
const onTimeScroll = (p: IOnScrollParam) => { /**
* 线
*/
const onTimelineScroll = (p: IOnScrollParam) => {
const { top, height } = p; const { top, height } = p;
//左侧区域高度 //左侧区域高度
const { scrollHeight, clientHeight } = scrollContainerRef.current!; const { scrollHeight, clientHeight } = thumbnailRef.current!;
ratioRef.current = (scrollHeight - clientHeight) / height; scale.current = (scrollHeight - clientHeight) / height;
scrollContainerRef.current!.scrollTop = top * ratioRef.current; thumbnailRef.current!.scrollTop = top * scale.current;
}; };
const onScroll = () => { /**
const { scrollHeight, scrollTop } = scrollContainerRef.current!; *
*/
const onThumbnailScroll = () => {
const { scrollHeight, scrollTop } = thumbnailRef.current!;
const isTop = scrollTop === 0; const isTop = scrollTop === 0;
const isBottom = scrollTop === scrollHeight; const isBottom = scrollTop === scrollHeight;
const top = scrollTop / ratioRef.current; const top = isTop ? -4 : scrollTop / scale.current; // 修正顶部
setTimescroll({ top: isTop ? -4 : top }); setTimeline({ top });
}; };
return ( return (
<div className="container course"> <div className="container course">
<div className="recommends">
{recommendList.map((item, index) => (
<BsCard {...item} key={index} />
))}
</div>
<div className="action-bar"> <div className="action-bar">
<div className="table-action">
<Space> <Space>
{actions.map((action) => ( {actions.map((action) => (
<Tooltip key={action.key} content={action.tip}> <Tooltip key={action.key} content={action.tip}>
<Button <Button
type="text" type="text"
className={action.active ? "active" : ""}
onClick={() => onClickActionItem(action)} onClick={() => onClickActionItem(action)}
icon={ icon={
<Icon size={20} color="var(--color-text-2)"> <Icon
size={20}
color={
action.active
? "rgb(var(--primary-6))"
: "var(--color-text-2)"
}
>
{action.icon} {action.icon}
</Icon> </Icon>
} }
@ -109,17 +114,18 @@ export default function Index() {
</Dropdown> </Dropdown>
</Space> </Space>
</div> </div>
</div> <div className="thumbnail-timeline">
<div className="timeline"> <div className="thumbnail-container">
<div className="thumbnail"> <article ref={thumbnailRef} onScroll={onThumbnailScroll}>
<article ref={scrollContainerRef} onScroll={onScroll}>
{courseTimeList.map((item, index) => ( {courseTimeList.map((item, index) => (
<section key={index}> <section key={index}>
<div className="time"> <div className="time">
{item.year}{item.month} {item.year}{item.month}
</div> </div>
<div className="statistic">{item.data.length} </div> <div className="statistic">{item.data.length} </div>
<div className="grid"> <div
className={`grid ${actions.find((a) => a.active)?.gridClass}`}
>
{item.data.map((d: any) => ( {item.data.map((d: any) => (
<BsCard key={d.time} imgUrl={d.img} title={d.title} /> <BsCard key={d.time} imgUrl={d.img} title={d.title} />
))} ))}
@ -128,11 +134,11 @@ export default function Index() {
))} ))}
</article> </article>
</div> </div>
<TimeScroll <Timeline
className="timescroll" className="timeline-container"
data={courseTimeList} data={courseTimeList}
onScroll={onTimeScroll} onScroll={onTimelineScroll}
model={{ top: timescroll.top }} model={{ top: timeline.top }}
/> />
</div> </div>
</div> </div>

View File

@ -1,29 +1,5 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
export const recommendListDefault = [
{
imgUrl:
"https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/a8c8cdb109cb051163646151a4a5083b.png~tplv-uwbnlip3yd-webp.webp",
title: "这个非常OK啊",
desc: "推荐内容推荐内容推荐内容推荐内容推荐内容推荐内容推荐内容",
action: "开始学习",
},
{
imgUrl:
"https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/e278888093bef8910e829486fb45dd69.png~tplv-uwbnlip3yd-webp.webp",
title: "这个非常OK啊",
desc: "推荐内容推荐内容推荐内容推荐内容推荐内容推荐内容推荐内容",
action: "开始学习",
},
{
imgUrl:
"https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/a8c8cdb109cb051163646151a4a5083b.png~tplv-uwbnlip3yd-webp.webp",
title: "这个非常OK啊",
desc: "推荐内容推荐内容推荐内容推荐内容推荐内容推荐内容推荐内容",
action: "开始学习",
},
];
export const courseTimeList = [ export const courseTimeList = [
{ {
title: "这个非常OK啊1", title: "这个非常OK啊1",