feat: usage filter & export & dashbord (#3538)

* feat: usage filter & export & dashbord

* adjust ui

* fix tmb scroll

* fix code & selecte all

* merge
This commit is contained in:
heheer
2025-01-23 10:54:30 +08:00
committed by archer
parent 12c6ecb987
commit e4b85ffada
22 changed files with 1112 additions and 275 deletions

View File

@@ -1,4 +1,4 @@
import React, { useState, useMemo, useRef } from 'react';
import React, { useState, useMemo, useRef, useEffect } from 'react';
import { Box, Card, Flex, useTheme, useOutsideClick, Button } from '@chakra-ui/react';
import { addDays, format } from 'date-fns';
import { type DateRange, DayPicker } from 'react-day-picker';
@@ -14,12 +14,14 @@ const DateRangePicker = ({
defaultDate = {
from: addDays(new Date(), -30),
to: new Date()
}
},
dateRange
}: {
onChange?: (date: DateRange) => void;
onSuccess?: (date: DateRange) => void;
position?: 'bottom' | 'top';
defaultDate?: DateRange;
dateRange?: DateRange;
}) => {
const { t } = useTranslation();
const theme = useTheme();
@@ -27,6 +29,12 @@ const DateRangePicker = ({
const [range, setRange] = useState<DateRange | undefined>(defaultDate);
const [showSelected, setShowSelected] = useState(false);
useEffect(() => {
if (dateRange) {
setRange(dateRange);
}
}, [dateRange]);
const formatSelected = useMemo(() => {
if (range?.from && range.to) {
return `${format(range.from, 'y-MM-dd')} ~ ${format(range.to, 'y-MM-dd')}`;
@@ -49,7 +57,7 @@ const DateRangePicker = ({
py={1}
borderRadius={'sm'}
cursor={'pointer'}
bg={'myGray.100'}
bg={'myGray.50'}
fontSize={'sm'}
onClick={() => setShowSelected(true)}
>

View File

@@ -1,7 +1,7 @@
import {
Box,
Button,
ButtonProps,
Checkbox,
Flex,
Menu,
MenuButton,
@@ -10,11 +10,12 @@ import {
MenuList,
useDisclosure
} from '@chakra-ui/react';
import React, { useRef } from 'react';
import { useTranslation } from 'next-i18next';
import React, { useMemo, useRef } from 'react';
import MyTag from '../Tag/index';
import MyIcon from '../Icon';
import MyAvatar from '../Avatar';
import { useTranslation } from 'next-i18next';
import { useScrollPagination } from '../../../hooks/useScrollPagination';
export type SelectProps<T = any> = {
value: T[];
@@ -25,22 +26,31 @@ export type SelectProps<T = any> = {
value: T;
}[];
maxH?: number;
itemWrap?: boolean;
onSelect: (val: T[]) => void;
closeable?: boolean;
showCheckedIcon?: boolean;
ScrollData?: ReturnType<typeof useScrollPagination>['ScrollData'];
isSelectAll?: boolean;
setIsSelectAll?: React.Dispatch<React.SetStateAction<boolean>>;
} & Omit<ButtonProps, 'onSelect'>;
const MultipleSelect = <T = any,>({
value = [],
placeholder,
list = [],
width = '100%',
maxH = 400,
onSelect,
closeable = false,
showCheckedIcon = true,
itemWrap = true,
ScrollData,
isSelectAll,
setIsSelectAll,
...props
}: SelectProps<T>) => {
const { t } = useTranslation();
const ref = useRef<HTMLButtonElement>(null);
const { t } = useTranslation();
const { isOpen, onOpen, onClose } = useDisclosure();
const menuItemStyles: MenuItemProps = {
borderRadius: 'sm',
@@ -63,6 +73,71 @@ const MultipleSelect = <T = any,>({
}
};
const onSelectAll = () => {
if (!setIsSelectAll) {
onSelect(value.length === list.length ? [] : list.map((item) => item.value));
return;
}
if (isSelectAll) {
onSelect([]);
}
setIsSelectAll((state) => !state);
};
const ListRender = useMemo(() => {
return (
<>
{list.map((item, i) => (
<MenuItem
key={i}
{...menuItemStyles}
{...((isSelectAll && !value.includes(item.value)) ||
(!isSelectAll && value.includes(item.value))
? {
color: 'primary.600'
}
: {
color: 'myGray.900'
})}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
onclickItem(item.value);
}}
whiteSpace={'pre-wrap'}
fontSize={'sm'}
gap={2}
>
{!showCheckedIcon && (
<Checkbox
isChecked={
(isSelectAll && !value.includes(item.value)) ||
(!isSelectAll && value.includes(item.value))
}
/>
)}
{item.icon && <MyAvatar src={item.icon} w={'1rem'} borderRadius={'0'} />}
<Box flex={'1 0 0'}>{item.label}</Box>
{showCheckedIcon && (
<Box w={'0.8rem'} lineHeight={1}>
{(isSelectAll && !value.includes(item.value)) ||
(!isSelectAll && value.includes(item.value) && (
<MyIcon name={'price/right'} w={'1rem'} />
))}
</Box>
)}
</MenuItem>
))}
</>
);
}, [value, list, isSelectAll]);
const isAllSelected = useMemo(
() => (isSelectAll && value.length === 0) || (!isSelectAll && value.length === list.length),
[isSelectAll, value, list]
);
return (
<Box>
<Menu
@@ -75,12 +150,10 @@ const MultipleSelect = <T = any,>({
closeOnSelect={false}
>
<MenuButton
as={Box}
as={Flex}
alignItems={'center'}
ref={ref}
width={width}
minH={'40px'}
px={3}
py={2}
borderRadius={'md'}
border={'base'}
userSelect={'none'}
@@ -88,6 +161,9 @@ const MultipleSelect = <T = any,>({
_active={{
transform: 'none'
}}
_hover={{
borderColor: 'primary.300'
}}
{...props}
{...(isOpen
? {
@@ -102,82 +178,94 @@ const MultipleSelect = <T = any,>({
{placeholder}
</Box>
) : (
<Flex alignItems={'center'} gap={2} flexWrap={'wrap'}>
{value.map((item, i) => {
const listItem = list.find((i) => i.value === item);
if (!listItem) return null;
return (
<MyTag className="tag-icon" key={i} colorSchema="blue" type={'borderFill'}>
{listItem.label}
{closeable && (
<MyIcon
name={'common/closeLight'}
ml={1}
w="0.8rem"
cursor={'pointer'}
_hover={{
color: 'red.500'
}}
onClick={(e) => {
console.log(111);
e.stopPropagation();
onclickItem(item);
}}
/>
)}
</MyTag>
);
})}
<Flex alignItems={'center'} gap={2}>
<Flex
alignItems={'center'}
gap={2}
flexWrap={itemWrap ? 'wrap' : 'nowrap'}
overflow={'hidden'}
flex={1}
>
{isAllSelected ? (
<Box fontSize={'mini'} color={'myGray.900'}>
{t('common:common.All')}
</Box>
) : (
(isSelectAll
? list.filter((item) => !value.includes(item.value))
: list.filter((item) => value.includes(item.value))
).map((item, i) => (
<MyTag
className="tag-icon"
key={i}
bg={'primary.100'}
color={'primary.700'}
type={'fill'}
borderRadius={'full'}
px={2}
py={0.5}
flexShrink={0}
>
{item.label}
{closeable && (
<MyIcon
name={'common/closeLight'}
ml={1}
w="0.8rem"
cursor={'pointer'}
_hover={{
color: 'red.500'
}}
onClick={(e) => {
e.stopPropagation();
onclickItem(item.value);
}}
/>
)}
</MyTag>
))
)}
</Flex>
<MyIcon name={'core/chat/chevronDown'} color={'myGray.600'} w={4} h={4} />
</Flex>
)}
</MenuButton>
<MenuList
className={props.className}
minW={(() => {
const w = ref.current?.clientWidth;
if (w) {
return `${w}px !important`;
}
return Array.isArray(width)
? width.map((item) => `${item} !important`)
: `${width} !important`;
})()}
w={'auto'}
px={'6px'}
py={'6px'}
border={'1px solid #fff'}
boxShadow={
'0px 2px 4px rgba(161, 167, 179, 0.25), 0px 0px 1px rgba(121, 141, 159, 0.25);'
'0px 4px 10px 0px rgba(19, 51, 107, 0.10), 0px 0px 1px 0px rgba(19, 51, 107, 0.10);'
}
zIndex={99}
maxH={'40vh'}
overflowY={'auto'}
>
{list.map((item, i) => (
<MenuItem
key={i}
{...menuItemStyles}
{...(value.includes(item.value)
? {
color: 'primary.600'
}
: {
color: 'myGray.900'
})}
onClick={() => onclickItem(item.value)}
whiteSpace={'pre-wrap'}
fontSize={'sm'}
gap={2}
>
{item.icon && <MyAvatar src={item.icon} w={'1rem'} borderRadius={'0'} />}
<Box flex={'1 0 0'}>{item.label}</Box>
<MenuItem
{...menuItemStyles}
color={isAllSelected ? 'primary.600' : 'myGray.900'}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
onSelectAll();
}}
whiteSpace={'pre-wrap'}
fontSize={'sm'}
gap={2}
mb={1}
>
{!showCheckedIcon && <Checkbox isChecked={isAllSelected} />}
<Box flex={'1 0 0'}>{t('common:common.All')}</Box>
{showCheckedIcon && (
<Box w={'0.8rem'} lineHeight={1}>
{value.includes(item.value) && <MyIcon name={'price/right'} w={'1rem'} />}
{isAllSelected && <MyIcon name={'price/right'} w={'1rem'} />}
</Box>
</MenuItem>
))}
)}
</MenuItem>
{ScrollData ? <ScrollData>{ListRender}</ScrollData> : ListRender}
</MenuList>
</Menu>
</Box>

View File

@@ -66,7 +66,7 @@ const MyTag = ({ children, colorSchema = 'blue', type = 'fill', showDot, ...prop
}, [colorSchema]);
return (
<Box
<Flex
display={'inline-flex'}
px={2.5}
lineHeight={1}
@@ -83,7 +83,7 @@ const MyTag = ({ children, colorSchema = 'blue', type = 'fill', showDot, ...prop
>
{showDot && <Box w={1.5} h={1.5} borderRadius={'md'} bg={theme.color} mr={1.5}></Box>}
{children}
</Box>
</Flex>
);
};