feat(project): init
This commit is contained in:
224
components/tree-select/tree.tsx
Normal file
224
components/tree-select/tree.tsx
Normal file
@@ -0,0 +1,224 @@
|
||||
import React, { useEffect, useMemo, useState } from "react"
|
||||
import {
|
||||
Popover,
|
||||
Text,
|
||||
UnstyledButton,
|
||||
Input,
|
||||
Checkbox,
|
||||
useTree,
|
||||
getTreeExpandedState,
|
||||
RenderTreeNodePayload,
|
||||
Flex,
|
||||
TreeNodeData,
|
||||
Combobox,
|
||||
CloseButton,
|
||||
Stack,
|
||||
TextInput,
|
||||
InputProps,
|
||||
} from "@mantine/core"
|
||||
import { useDisclosure } from "@mantine/hooks"
|
||||
import { IconChevronUp, IconSearch } from "@tabler/icons-react"
|
||||
import { VirtualizedTree } from "./VirtualizedTree"
|
||||
import {
|
||||
filterTreeData_dfs,
|
||||
findLabel,
|
||||
getStateFromDataByName,
|
||||
} from "./libs/util"
|
||||
|
||||
interface VirtualTreeSelectProps extends InputProps {
|
||||
data: TreeNodeData[]
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
placeholder?: string
|
||||
setCurrCityCode: (code: string) => void
|
||||
}
|
||||
|
||||
export function VirtualTreeSelect({
|
||||
data,
|
||||
value,
|
||||
onChange,
|
||||
setCurrCityCode,
|
||||
...props
|
||||
}: VirtualTreeSelectProps) {
|
||||
const [opened, { toggle }] = useDisclosure(false)
|
||||
const [searchName, setSearchName] = useState<string>("")
|
||||
|
||||
const filteredTreeData = useMemo(() => {
|
||||
return filterTreeData_dfs(data, searchName)
|
||||
}, [searchName, data])
|
||||
|
||||
const tree = useTree({
|
||||
initialExpandedState: getTreeExpandedState(filteredTreeData, []),
|
||||
initialCheckedState: [],
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (filteredTreeData?.length && searchName) {
|
||||
let state = getStateFromDataByName(filteredTreeData)
|
||||
tree.setExpandedState(state)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [filteredTreeData, searchName])
|
||||
|
||||
useEffect(() => {
|
||||
setCurrCityCode(tree.selectedState[0])
|
||||
let label = findLabel(data, tree.selectedState[0])
|
||||
onChange(label.path || "")
|
||||
}, [data, onChange, setCurrCityCode, tree.selectedState])
|
||||
|
||||
const ChevronIcon = ({
|
||||
hasChildren,
|
||||
expanded,
|
||||
}: {
|
||||
hasChildren: boolean
|
||||
expanded: boolean
|
||||
}) => {
|
||||
if (hasChildren) {
|
||||
return (
|
||||
<IconChevronUp
|
||||
size={16}
|
||||
stroke={2.5}
|
||||
style={{
|
||||
transform: expanded ? "rotate(180deg)" : "rotate(90deg)",
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const Leaf = React.memo(
|
||||
({
|
||||
node,
|
||||
expanded,
|
||||
elementProps,
|
||||
selected,
|
||||
tree,
|
||||
}: RenderTreeNodePayload) => {
|
||||
return (
|
||||
<Flex
|
||||
{...elementProps}
|
||||
onClick={() => {}}
|
||||
gap={12}
|
||||
px={16}
|
||||
justify={"space-between"}
|
||||
flex={1}
|
||||
align="stretch"
|
||||
bdrs={4}
|
||||
pl={(node.nodeProps?.level || 1) * 16}
|
||||
style={{
|
||||
overflowX: "hidden",
|
||||
cursor: "auto",
|
||||
height: 36,
|
||||
}}>
|
||||
<Flex align={"center"} gap={12} flex={1} onClick={() => {}}>
|
||||
<Checkbox.Indicator
|
||||
checked={selected}
|
||||
size="xs"
|
||||
radius="sm"
|
||||
onClick={() => {
|
||||
if (!selected) {
|
||||
tree.select(node.value)
|
||||
} else {
|
||||
tree.select("")
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
lh={"22px"}
|
||||
size="15px"
|
||||
title={node.label as string}
|
||||
style={{
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}>
|
||||
{node.label}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex gap={24} align={"center"}>
|
||||
<UnstyledButton
|
||||
h={16}
|
||||
style={{
|
||||
display: "flex",
|
||||
}}
|
||||
onClick={() => tree.toggleExpanded(node.value)}>
|
||||
<ChevronIcon
|
||||
hasChildren={!!(node.children && node.children.length)}
|
||||
expanded={expanded}
|
||||
/>
|
||||
</UnstyledButton>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<Popover
|
||||
opened={opened}
|
||||
onChange={toggle}
|
||||
width="target"
|
||||
position="bottom-start"
|
||||
shadow="md"
|
||||
keepMounted={false} // 性能优化:关闭时销毁 DOM
|
||||
>
|
||||
<Popover.Target>
|
||||
<Input
|
||||
component="button"
|
||||
pointer
|
||||
onClick={toggle}
|
||||
rightSection={<Combobox.Chevron />}
|
||||
// style={{ width: 250 }}
|
||||
{...props}>
|
||||
{value || "请选择..."}
|
||||
</Input>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown p={0}>
|
||||
<TextInput
|
||||
m={"xs"}
|
||||
radius={"md"}
|
||||
placeholder="搜索省、市、区名称"
|
||||
value={searchName}
|
||||
onChange={(e) => {
|
||||
setSearchName(e.currentTarget.value)
|
||||
}}
|
||||
rightSection={
|
||||
searchName ? (
|
||||
<CloseButton
|
||||
size="1rem"
|
||||
aria-label="Clear input"
|
||||
onClick={() => {
|
||||
setSearchName("")
|
||||
}}
|
||||
style={{
|
||||
display: searchName ? undefined : "none",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<IconSearch size="1rem" />
|
||||
)
|
||||
}
|
||||
/>
|
||||
{filteredTreeData.length === 0 ? (
|
||||
<Stack flex={1} h={"250px"} justify="center" align="center">
|
||||
<Text c="dimmed">暂无数据</Text>
|
||||
</Stack>
|
||||
) : (
|
||||
<>
|
||||
{/* 虚拟列表容器:必须有固定高度 */}
|
||||
<VirtualizedTree
|
||||
height={"450px"}
|
||||
tree={tree}
|
||||
data={filteredTreeData}
|
||||
levelOffset={30}
|
||||
renderNode={(payload) => <Leaf {...payload} />}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user