✨
This commit is contained in:
20
src/components/TreeSelector/Props.ts
Normal file
20
src/components/TreeSelector/Props.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
interface TreeItem {
|
||||
id: number;
|
||||
parentId: number;
|
||||
name: string;
|
||||
logoUrl: string | null;
|
||||
}
|
||||
|
||||
export type TreeItemWithUnknown = Omit<TreeItem & unknown, "children">;
|
||||
|
||||
interface TreeActions {
|
||||
onChecked: (item: Omit<TreeItemWithChildren, "children">) => void;
|
||||
}
|
||||
|
||||
export type TreeItemWithChildren = TreeItemWithUnknown & {
|
||||
children: Array<TreeItemWithChildren>;
|
||||
};
|
||||
|
||||
export type TreeProps = {
|
||||
items: Array<TreeItemWithChildren>;
|
||||
} & TreeActions;
|
||||
89
src/components/TreeSelector/index.module.css
Normal file
89
src/components/TreeSelector/index.module.css
Normal file
@@ -0,0 +1,89 @@
|
||||
.toggler {
|
||||
border: none;
|
||||
color: white;
|
||||
background-color: rgba(21, 144, 251, 0.8);
|
||||
padding: 5px 10px;
|
||||
font-size: 1em;
|
||||
|
||||
&:hover {
|
||||
color: white;
|
||||
background-color: rgba(19, 105, 210, 0.8);
|
||||
}
|
||||
i {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.show {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background-color: white;
|
||||
box-shadow: 0 0 5px var(--border-color);
|
||||
overflow-y: auto;
|
||||
visibility: visible;
|
||||
height: 100vh;
|
||||
transition: all 0.1s ease-in-out;
|
||||
|
||||
&.hidden {
|
||||
height: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.path {
|
||||
padding: 5px 10px;
|
||||
border-top: solid var(--border-color) 1px;
|
||||
border-bottom: solid var(--border-color) 1px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 99;
|
||||
background-color: white;
|
||||
|
||||
ol {
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
|
||||
a {
|
||||
padding: 5px 10px;
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
li {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
a {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 2em;
|
||||
|
||||
&.pager {
|
||||
flex: 2;
|
||||
font-size: 4em;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
&.pager:hover {
|
||||
color: var(--hover-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
147
src/components/TreeSelector/index.tsx
Normal file
147
src/components/TreeSelector/index.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import { useState, useRef, useLayoutEffect } from "react";
|
||||
import type {
|
||||
TreeProps,
|
||||
TreeItemWithChildren,
|
||||
TreeItemWithUnknown,
|
||||
} from "./Props";
|
||||
import styles from "./index.module.css";
|
||||
|
||||
export function TreeSelector(props: TreeProps) {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const togglerRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const defaultItem: TreeItemWithUnknown = {
|
||||
id: 0,
|
||||
logoUrl: "",
|
||||
name: "全部分类",
|
||||
parentId: 0,
|
||||
};
|
||||
const [treePath, setTreePath] = useState<Array<TreeItemWithUnknown>>([
|
||||
defaultItem,
|
||||
]);
|
||||
const [checkedItem, setCheckedItem] =
|
||||
useState<TreeItemWithUnknown>(defaultItem);
|
||||
const [show, toggleShow] = useState<boolean>(false);
|
||||
const withoutChildren = props.items.map(({ children: _, ...rest }) => rest);
|
||||
const [list, setList] = useState<Array<TreeItemWithUnknown>>(withoutChildren);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (contentRef.current && togglerRef.current && show) {
|
||||
const top =
|
||||
togglerRef.current.offsetTop + togglerRef.current.clientHeight;
|
||||
const left = contentRef.current.offsetLeft;
|
||||
contentRef.current.style.position = "absolute";
|
||||
contentRef.current.style.top = `${top}px`;
|
||||
contentRef.current.style.left = `$${left}px`;
|
||||
}
|
||||
}, [show]);
|
||||
|
||||
function pathClick(item: TreeItemWithUnknown) {
|
||||
if (treePath.indexOf(item) != treePath.length - 1) {
|
||||
setTreePath((state) => state.slice(0, state.indexOf(item) + 1));
|
||||
setList(getChildren(item, props.items));
|
||||
} else if (item.id == 0) {
|
||||
listClick(item);
|
||||
}
|
||||
}
|
||||
|
||||
function listClick(item: TreeItemWithUnknown) {
|
||||
setCheckedItem(item);
|
||||
toggleShow(false);
|
||||
props.onChecked(item);
|
||||
}
|
||||
|
||||
function prevClick(item: TreeItemWithUnknown) {
|
||||
if (item.parentId == 0) return;
|
||||
setTreePath((state) => state.slice(0, state.length - 1));
|
||||
setList(getSiblings(item.parentId, props.items));
|
||||
}
|
||||
|
||||
function nextClick(item: TreeItemWithUnknown) {
|
||||
if (treePath.indexOf(item) != treePath.length - 1) {
|
||||
setTreePath((state) => [...state, item]);
|
||||
const children = getChildren(item, props.items);
|
||||
setList(children || []);
|
||||
}
|
||||
}
|
||||
|
||||
function getSiblings(
|
||||
id: number,
|
||||
items: Array<TreeItemWithChildren>,
|
||||
): Array<TreeItemWithUnknown> {
|
||||
for (const i of items) {
|
||||
if (i.id == id) return items;
|
||||
if (i.children) {
|
||||
const siblings = getSiblings(id, i.children);
|
||||
if (siblings.length) return siblings;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function getChildren(
|
||||
item: TreeItemWithUnknown,
|
||||
items: Array<TreeItemWithChildren>,
|
||||
): Array<TreeItemWithUnknown> {
|
||||
if (item.id == 0) return items;
|
||||
for (const i of items) {
|
||||
if (i.id == item.id) {
|
||||
return i.children;
|
||||
}
|
||||
if (i.children) {
|
||||
const children = getChildren(item, i.children);
|
||||
if (children.length) return children;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
ref={togglerRef}
|
||||
className={styles.toggler}
|
||||
onClick={() => toggleShow(!show)}
|
||||
>
|
||||
{checkedItem.name}
|
||||
<i className={`ss ssi-expand ${show ? styles.show : ""}`}></i>
|
||||
</button>
|
||||
<div
|
||||
ref={contentRef}
|
||||
className={`${styles.content} ${show ? "" : styles.hidden}`}
|
||||
>
|
||||
<div className={styles.path}>
|
||||
<ol>
|
||||
{treePath.map((t) => {
|
||||
return (
|
||||
<li key={t.id}>
|
||||
<a onClick={() => pathClick(t)}>{t.name}</a>
|
||||
<span>{">"}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</div>
|
||||
<div className={styles.list}>
|
||||
<ul>
|
||||
{list.map((l) => {
|
||||
return (
|
||||
<li key={l.id}>
|
||||
<a className={styles.pager} onClick={() => prevClick(l)}>
|
||||
{"<"}
|
||||
</a>
|
||||
<a onClick={() => listClick(l)}>{l.name}</a>
|
||||
<a className={styles.pager} onClick={() => nextClick(l)}>
|
||||
{">"}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { type TreeItemWithUnknown } from "./Props";
|
||||
Reference in New Issue
Block a user