This commit is contained in:
2026-03-25 14:59:06 +08:00
commit ae315100b4
92 changed files with 9285 additions and 0 deletions

View 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;

View 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);
}
}
}
}
}
}

View 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}&nbsp;
<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";