feat: 时间线
This commit is contained in:
parent
5b3f929c39
commit
eb593ef590
|
@ -0,0 +1,5 @@
|
||||||
|
.recommends {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1.5fr 1fr;
|
||||||
|
grid-column-gap: 20px;
|
||||||
|
}
|
47
apps/web-main/src/view/Course/components/Recommand/index.tsx
Normal file
47
apps/web-main/src/view/Course/components/Recommand/index.tsx
Normal 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;
|
|
@ -1,5 +1,4 @@
|
||||||
.timescroll {
|
.timeline {
|
||||||
position: relative;
|
|
||||||
width: 40px;
|
width: 40px;
|
||||||
|
|
||||||
.caret {
|
.caret {
|
|
@ -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;
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in New Issue
Block a user