feat: 时间线

This commit is contained in:
mozzie 2023-02-28 17:58:30 +08:00
parent 198c7132d1
commit d074849556
10 changed files with 387 additions and 83 deletions

View File

@ -13,7 +13,9 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "6.8.0", "react-router-dom": "6.8.0",
"@arco-design/web-react": "2.45.0" "@arco-design/web-react": "2.45.0",
"@ricons/fluent": "0.12.0",
"dayjs": "1.11.7"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.0.27", "@types/react": "^18.0.27",

View File

@ -1,6 +1,10 @@
@import "normalize.css"; @import "normalize.css";
@import "@arco-design/web-react/dist/css/arco.css"; @import "@arco-design/web-react/dist/css/arco.css";
* {
box-sizing: border-box;
}
html, html,
body { body {
position: relative; position: relative;

View File

@ -1,8 +1,15 @@
.bs-card { .bs-card {
overflow: hidden;
.arco-card-body { .arco-card-body {
padding: 10px; padding: 10px;
} }
&.mini {
.arco-card-body {
padding: 0;
}
}
&:hover { &:hover {
.cover { .cover {
background-size: 105%; background-size: 105%;

View File

@ -1,29 +1,36 @@
import { Card } from "@arco-design/web-react"; import { Card } from "@arco-design/web-react";
import { url } from "inspector"; import { MouseEventHandler } from "react";
import "./index.less"; import "./index.less";
const { Meta } = Card; const { Meta } = Card;
interface IProps { interface IProps {
imgUrl: string; imgUrl: string;
title: string; title: string;
desc: string; meta?: {
action: string; desc?: string;
action?: string;
};
styles?: {}; styles?: {};
onClick?: MouseEventHandler;
} }
function BsCard(props: IProps) { function BsCard(props: IProps) {
return ( const { imgUrl, title, meta, styles, ...rest } = props;
return meta ? (
<Card <Card
{...rest}
className="bs-card" className="bs-card"
hoverable hoverable
style={{ ...props.styles }} style={{ ...styles }}
cover={ cover={
<div <div
className="cover" className="cover"
style={{ backgroundImage: `url('${props.imgUrl}')` }} style={{
backgroundImage: `url('${imgUrl}')`,
}}
> >
<div className="mask"> <div className="mask">
<p>{props.title}</p> <p>{title}</p>
</div> </div>
</div> </div>
} }
@ -31,12 +38,25 @@ function BsCard(props: IProps) {
<Meta <Meta
description={ description={
<div className="bottom-des"> <div className="bottom-des">
<span className="bs-ellipsis">{props.desc}</span> <span className="bs-ellipsis">{meta?.desc}</span>
<a>{props.action}</a> <a>{meta?.action}</a>
</div> </div>
} }
/> />
</Card> </Card>
) : (
<Card {...rest} className="bs-card mini" hoverable style={{ ...styles }}>
<div
className="cover"
style={{
backgroundImage: `url('${imgUrl}')`,
}}
>
<div className="mask">
<p>{title}</p>
</div>
</div>
</Card>
); );
} }

View File

@ -1,17 +1,16 @@
.timescroll { .timescroll {
position: relative; position: relative;
width: 20px; width: 40px;
height: 200px;
&:hover {
.caret {
display: block;
}
}
.caret { .caret {
display: none; display: none;
position: absolute; position: absolute;
width: 20px;
height: 20px;
left: 50%;
transform: translateX(-50%);
right: 0; right: 0;
color: var(--color-fill-4); color: var(--color-text-3);
&.up { &.up {
top: 0; top: 0;
} }
@ -25,24 +24,35 @@
transform: translateX(-50%); transform: translateX(-50%);
top: 24px; top: 24px;
bottom: 24px; bottom: 24px;
width: 4px; width: 3px;
background: var(--color-fill-2); background: var(--color-fill-2);
&:hover {
.cursor.active {
opacity: 1;
}
}
.cursor { .cursor {
transition: opacity 0.25s ease;
position: absolute; position: absolute;
width: 8px; left: 50%;
height: 8px; transform: translateX(-50%);
border: 1px solid #333; width: 12px;
height: 12px;
border: 2px solid;
border-radius: 50%; border-radius: 50%;
&.active {
opacity: 0;
}
} }
.node { .node {
position: absolute; position: absolute;
width: 4px; width: 3px;
height: 4px; height: 3px;
border-radius: 50%; border-radius: 50%;
&.bingo { &.bingo {
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
background: var(--color-fill-4); background: var(--color-text-3);
&::before { &::before {
position: absolute; position: absolute;
content: attr(data-year); content: attr(data-year);
@ -50,7 +60,8 @@
transform: translateY(-50%); transform: translateY(-50%);
right: 10px; right: 10px;
font-size: 12px; font-size: 12px;
color: var(--color-fill-4); color: var(--color-text-3);
line-height: 1;
} }
} }
&.empty { &.empty {
@ -59,4 +70,10 @@
} }
} }
} }
&:hover {
.caret {
display: block;
}
}
} }

View File

@ -1,17 +1,59 @@
import { useState } from "react"; import { useEffect, useRef, useState } from "react";
import "./index.less"; import "./index.less";
function TimeScroll() { interface IProps {
const [cursor, setCursor] = useState({ className: string;
data: any;
}
function TimeScroll(props: IProps) {
const [cursorStatic, setCursorStatic] = useState({
top: 0, top: 0,
color: "var(--color-fill-4)",
}); });
const onMouseMove = (e: any) => { const [cursorActive, setCursorActive] = useState({
const diffY = e.screenY - e.clientY; top: 0,
console.log(e) color: "var(--color-border-3)",
setCursor({ top: diffY }); });
const [intervalPixel, setIntervalPixel] = useState<number>();
const cursorActiveRef = useRef<HTMLDivElement | null>(null);
const cursorStaticRef = useRef<HTMLDivElement | null>(null);
const orbitRef = useRef<HTMLDivElement | null>(null);
/**
* cursorStatic
*/
const onMouseDown = (ev: any) => {
const orbitClient = orbitRef.current!.getBoundingClientRect();
let mouseY = (ev || window.event).clientY; //鼠标按下的位置
if (mouseY > orbitClient.top && mouseY < orbitClient.bottom)
setCursorStatic((p) => ({ ...p, top: mouseY - orbitClient.top - 4 }));
}; };
/**
* coursorActive
*/
const onMouseMove = (ev: any) => {
const orbitClient = orbitRef.current!.getBoundingClientRect();
const mouseY = (ev || window.event).clientY;
if (mouseY > orbitClient.top && mouseY < orbitClient.bottom)
setCursorActive((p) => ({ ...p, top: mouseY - orbitClient.top - 4 }));
};
useEffect(() => {
if (props.data) {
const years = [...new Set(props.data.map((i: any) => i.year))];
const avg = orbitRef.current!.clientHeight / years.length;
setIntervalPixel(avg);
}
}, [props.data, orbitRef.current]);
return ( return (
<div className="timescroll" onMouseMove={onMouseMove}> <div
className={`timescroll ${props.className}`}
onMouseMove={onMouseMove}
onMouseDown={onMouseDown}
>
<svg <svg
className="caret up" className="caret up"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -19,24 +61,33 @@ function TimeScroll() {
> >
<path d="M8 20l8-10l8 10z" fill="currentColor"></path> <path d="M8 20l8-10l8 10z" fill="currentColor"></path>
</svg> </svg>
<div className="orbit"> <div className="orbit" ref={orbitRef}>
<span className="cursor" style={{ top: cursor.top }}></span> <span
ref={cursorStaticRef}
className="cursor static"
style={{ ...cursorStatic }}
></span>
<span
ref={cursorActiveRef}
className="cursor active"
style={{ ...cursorActive }}
></span>
{props.data.map((item: any, index: number) => {
return item.data.length > 0 ? (
<div <div
key={index}
className="node bingo" className="node bingo"
style={{ top: "20px" }} style={{ top: intervalPixel * index + "px" }}
data-year="2021" data-year={item.year}
></div> ></div>
<div className="node empty" style={{ top: "40px" }}></div> ) : (
<div <div
className="node bingo" key={index}
style={{ top: "60px" }} className="node empty"
data-year="2020" style={{ top: intervalPixel * index + "px" }}
></div>
<div
className="node bingo"
style={{ top: "80px" }}
data-year="2019"
></div> ></div>
);
})}
</div> </div>
<svg <svg
className="caret down" className="caret down"

View File

@ -1,11 +1,85 @@
.course { .course {
padding: 100px 0 40px 0; display: flex;
flex-direction: column;
padding: 100px 0 0 0;
height: 100vh;
.recommends { .recommends {
display: grid; display: grid;
grid-template-columns: 2fr 1fr 1fr; grid-template-columns: 2fr 1fr 1fr;
grid-column-gap: 20px; grid-column-gap: 20px;
} }
.action-bar {
padding: 40px 0 20px 0;
display: flex;
align-items: center;
justify-content: space-between;
.table-action {
line-height: 1;
> span {
transition: all 0.25s;
display: inline-block;
margin-left: 20px;
color: var(--color-text-2);
&:hover {
color: var(--color-text-1);
cursor: pointer;
}
svg {
width: 24px;
height: 24px;
}
&.active {
color: rgb(var(--primary-6));
}
}
}
}
.timeline { .timeline {
margin-top: 40px; flex: 1;
position: relative;
.thumbnail {
position: absolute;
top: 0;
left: 0;
right: 100px;
bottom: 0;
overflow: hidden;
> section {
margin-bottom: 40px;
.time {
padding-bottom: 10px;
color: var(--color-text-2);
}
.statistic {
padding-bottom: 10px;
color: var(--color-text-3);
}
.grid {
display: grid;
grid-column-gap: 10px;
grid-row-gap: 10px;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
.bs-card {
border-radius: 3px;
.cover {
height: 120px;
}
.mask {
p {
font-size: 13px;
}
}
}
}
}
}
.timescroll {
position: absolute;
right: 0;
top: 0;
bottom: 0;
}
} }
} }

View File

@ -1,44 +1,51 @@
import "./index.less"; import "./index.less";
import Player from "../../components/Player"; import Player from "../../components/Player";
import { useMount } from "../../hook"; import { useMount } from "../../hook";
import { Select, Message } from "@arco-design/web-react"; import { Select, Message, Space, Tooltip } from "@arco-design/web-react";
const Option = Select.Option; const Option = Select.Option;
const options = ["全部", "最新的"]; const options = ["全部", "最新的"];
import BsCard from "../../components/Card"; import BsCard from "../../components/Card";
import TimeScroll from "../../components/TimeScroll"; import TimeScroll from "../../components/TimeScroll";
import Tab20Regular from "@ricons/fluent/Tab20Regular";
import Table20Regular from "@ricons/fluent/Table20Regular";
import { useState } from "react";
import { recommendListDefault, courseTimeListDefault } from "./mock";
export default function Index() { export default function Index() {
useMount(() => {}); useMount(() => {});
const [actions, setActions] = useState([
{
key: "tab",
icon: Tab20Regular,
active: false,
tip: "单格排列",
},
{
key: "table",
icon: Table20Regular,
active: true,
tip: "缩略",
},
]);
const [recommendList, setRecommendList] = useState(recommendListDefault);
const [courseTimeList, setCourseTimeList] = useState(courseTimeListDefault);
const onClickActionItem = (action: any) => {
setActions((p) => p.map((a) => ({ ...a, active: a.key === action.key })));
};
return ( return (
<div className="container course"> <div className="container course">
<div className="recommends"> <div className="recommends">
<BsCard {recommendList.map((item, index) => (
imgUrl={ <BsCard {...item} key={index} />
"https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/a8c8cdb109cb051163646151a4a5083b.png~tplv-uwbnlip3yd-webp.webp" ))}
}
title={"这个非常OK啊"}
desc={"推荐内容推荐内容推荐内容推荐内容推荐内容推荐内容推荐内容"}
action={"开始学习"}
/>
<BsCard
imgUrl={
"https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/e278888093bef8910e829486fb45dd69.png~tplv-uwbnlip3yd-webp.webp"
}
title={"这个非常OK啊"}
desc={"推荐内容"}
action={"开始学习"}
/>
<BsCard
imgUrl={
"https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/8361eeb82904210b4f55fab888fe8416.png~tplv-uwbnlip3yd-webp.webp"
}
title={"这个非常OK啊"}
desc={"推荐内容"}
action={"开始学习"}
/>
</div> </div>
<div className="timeline">
<div className="action-bar">
<Select <Select
placeholder="排序规则" placeholder="排序规则"
style={{ width: 154 }} style={{ width: 154 }}
@ -55,7 +62,36 @@ export default function Index() {
</Option> </Option>
))} ))}
</Select> </Select>
<TimeScroll /> <div className="table-action">
{actions.map((action) => (
<Tooltip key={action.key} content={action.tip}>
<span
className={action.active ? "active" : ""}
onClick={() => onClickActionItem(action)}
>
<action.icon />
</span>
</Tooltip>
))}
</div>
</div>
<div className="timeline">
<div className="thumbnail">
{courseTimeList.map((item, index) => (
<section key={index}>
<div className="time">
{item.year}{item.month}
</div>
<div className="statistic">{item.data.length} </div>
<div className="grid">
{item.data.map((d: any) => (
<BsCard key={d.time} imgUrl={d.img} title={d.title} />
))}
</div>
</section>
))}
</div>
<TimeScroll className="timescroll" data={courseTimeList} />
</div> </div>
</div> </div>
); );

View File

@ -0,0 +1,83 @@
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 = [
{
title: "这个非常OK啊1",
time: "1661990400000",
img: "https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/a8c8cdb109cb051163646151a4a5083b.png~tplv-uwbnlip3yd-webp.webp",
},
{
title: "这个非常OK啊2",
time: "1630454400000",
img: "https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/a8c8cdb109cb051163646151a4a5083b.png~tplv-uwbnlip3yd-webp.webp",
},
{
title: "这个非常OK啊333",
time: "1625097600000",
img: "https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/a8c8cdb109cb051163646151a4a5083b.png~tplv-uwbnlip3yd-webp.webp",
},
{
title: "这个非常OK啊444",
time: "1625184000000",
img: "https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/a8c8cdb109cb051163646151a4a5083b.png~tplv-uwbnlip3yd-webp.webp",
},
{
title: "这个非常OK啊3",
time: "1598918400000",
img: "https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/a8c8cdb109cb051163646151a4a5083b.png~tplv-uwbnlip3yd-webp.webp",
},
];
const process = (before: any[]) => {
const after: any = {};
// 提取月份,塞入数据
before.forEach((item) => {
const year = dayjs(+item.time).year();
const month = dayjs(+item.time).month() + 1;
if (!(year in after)) after[year] = { [month]: [] };
if (!(month in after[year])) after[year][month] = [];
after[year][month].push(item);
});
// 年月为key倒叙排列
const compare = (key: string) => (a: any, b: any) => b[key] - a[key];
const ymArray = Object.keys(after)
.reverse()
.map((year) =>
Object.keys(after[year]).map((month) => ({
year,
month,
index: +`${year}.${+month > 10 ? month : "0" + month}`,
data: after[year][month],
}))
)
.flat()
.sort(compare("index"));
return ymArray;
};
export const courseTimeListDefault = process(courseTimeList);

View File

@ -178,10 +178,12 @@ importers:
apps/web-main: apps/web-main:
specifiers: specifiers:
'@arco-design/web-react': 2.45.0 '@arco-design/web-react': 2.45.0
'@ricons/fluent': 0.12.0
'@types/react': ^18.0.27 '@types/react': ^18.0.27
'@types/react-dom': ^18.0.10 '@types/react-dom': ^18.0.10
'@types/react-router-dom': 5.3.3 '@types/react-router-dom': 5.3.3
'@vitejs/plugin-react': ^3.1.0 '@vitejs/plugin-react': ^3.1.0
dayjs: 1.11.7
less: ^4.1.3 less: ^4.1.3
react: ^18.2.0 react: ^18.2.0
react-dom: ^18.2.0 react-dom: ^18.2.0
@ -191,6 +193,8 @@ importers:
vite-tsconfig-paths: 4.0.5 vite-tsconfig-paths: 4.0.5
dependencies: dependencies:
'@arco-design/web-react': registry.npmmirror.com/@arco-design/web-react/2.45.0_5ndqzdd6t4rivxsukjv3i3ak2q '@arco-design/web-react': registry.npmmirror.com/@arco-design/web-react/2.45.0_5ndqzdd6t4rivxsukjv3i3ak2q
'@ricons/fluent': registry.npmmirror.com/@ricons/fluent/0.12.0
dayjs: registry.npmmirror.com/dayjs/1.11.7
less: registry.npmmirror.com/less/4.1.3 less: registry.npmmirror.com/less/4.1.3
react: registry.npmmirror.com/react/18.2.0 react: registry.npmmirror.com/react/18.2.0
react-dom: registry.npmmirror.com/react-dom/18.2.0_react@18.2.0 react-dom: registry.npmmirror.com/react-dom/18.2.0_react@18.2.0
@ -3801,6 +3805,12 @@ packages:
engines: {node: '>=14'} engines: {node: '>=14'}
dev: false dev: false
registry.npmmirror.com/@ricons/fluent/0.12.0:
resolution: {integrity: sha512-q+mPtxwTCZBeNmIrnKQxHc08f4OvJOxaR1AiGbpJpTMAzm/b8ZdrL14wm5ArYJq+uDNpLynfBYi3CTWsHjRRgQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@ricons/fluent/-/fluent-0.12.0.tgz}
name: '@ricons/fluent'
version: 0.12.0
dev: false
registry.npmmirror.com/@rollup/plugin-commonjs/24.0.1_rollup@3.17.2: registry.npmmirror.com/@rollup/plugin-commonjs/24.0.1_rollup@3.17.2:
resolution: {integrity: sha512-15LsiWRZk4eOGqvrJyu3z3DaBu5BhXIMeWnijSRvd8irrrg9SHpQ1pH+BUK4H6Z9wL9yOxZJMTLU+Au86XHxow==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@rollup/plugin-commonjs/-/plugin-commonjs-24.0.1.tgz} resolution: {integrity: sha512-15LsiWRZk4eOGqvrJyu3z3DaBu5BhXIMeWnijSRvd8irrrg9SHpQ1pH+BUK4H6Z9wL9yOxZJMTLU+Au86XHxow==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@rollup/plugin-commonjs/-/plugin-commonjs-24.0.1.tgz}
id: registry.npmmirror.com/@rollup/plugin-commonjs/24.0.1 id: registry.npmmirror.com/@rollup/plugin-commonjs/24.0.1