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

View File

@@ -3,8 +3,14 @@
"all": "all",
"app_name": "Application name",
"billing_module": "Deduction module",
"confirm_export": "A total of {{total}} pieces of data were filtered out. Are you sure to export?",
"current_filter_conditions": "Current filter conditions",
"dashboard": "Dashboard",
"details": "Details",
"dingtalk": "DingTalk",
"duration_seconds": "Duration (seconds)",
"export_confirm": "Export confirmation",
"feishu": "Feishu",
"generation_time": "Generation time",
"input_token_length": "input tokens",
"member": "member",
@@ -12,14 +18,21 @@
"module_name": "module name",
"month": "moon",
"no_usage_records": "No usage record yet",
"official_account": "Official Account",
"order_number": "Order number",
"output_token_length": "output tokens",
"points": "Points",
"project_name": "Project name",
"select_member_and_source_first": "Please select members and types first",
"share": "Share Link",
"source": "source",
"start_export": "Export started",
"text_length": "text length",
"token_length": "token length",
"total_points": "AI points consumption",
"total_points_consumed": "AI points consumption",
"usage_detail": "Usage details",
"user_type": "type"
"total_usage": "Total Usage",
"usage_detail": "Details",
"user_type": "type",
"wecom": "WeCom"
}

View File

@@ -112,10 +112,5 @@
"team.org.org": "Organization",
"team.manage_collaborators": "Manage Collaborators",
"team.no_collaborators": "No Collaborators",
"team.write_role_member": "",
"usage.feishu": "Feishu",
"usage.dingtalk": "DingTalk",
"usage.official_account": "Official Account",
"usage.share": "Share Link",
"usage.wecom": "WeCom"
"team.write_role_member": ""
}

View File

@@ -3,8 +3,18 @@
"all": "所有",
"app_name": "应用名",
"billing_module": "扣费模块",
"confirm_export": "共筛选出 {{total}} 条数据,是否确认导出?",
"current_filter_conditions": "当前筛选条件:",
"dashboard": "仪表盘",
"details": "详情",
"dingtalk": "钉钉",
"duration_seconds": "时长(秒)",
"every_day": "每天",
"every_month": "每月",
"every_week": "每周",
"export_confirm": "导出确认",
"export_success": "导出成功",
"feishu": "飞书",
"generation_time": "生成时间",
"input_token_length": "输入 tokens",
"member": "成员",
@@ -12,14 +22,21 @@
"module_name": "模块名",
"month": "月",
"no_usage_records": "暂无使用记录",
"official_account": "公众号",
"order_number": "订单号",
"output_token_length": "输出 tokens",
"points": "积分",
"project_name": "项目名",
"select_member_and_source_first": "请先选中成员和类型",
"share": "分享链接",
"source": "来源",
"start_export": "已开始导出",
"text_length": "文本长度",
"token_length": "token 长度",
"total_points": "AI 积分消耗",
"total_points_consumed": "AI 积分消耗",
"total_usage": "总消耗",
"usage_detail": "使用详情",
"user_type": "类型"
"user_type": "类型",
"wecom": "企业微信"
}

View File

@@ -112,10 +112,5 @@
"team.org.org": "部门",
"team.manage_collaborators": "管理协作者",
"team.no_collaborators": "暂无协作者",
"team.write_role_member": "可写权限",
"usage.feishu": "飞书",
"usage.dingtalk": "钉钉",
"usage.official_account": "公众号",
"usage.share": "分享链接",
"usage.wecom": "企业微信"
"team.write_role_member": "可写权限"
}

View File

@@ -3,8 +3,14 @@
"all": "所有",
"app_name": "應用程式名",
"billing_module": "扣費模組",
"confirm_export": "共篩選出 {{total}} 條數據,是否確認導出?",
"current_filter_conditions": "當前篩選條件:",
"dashboard": "儀表板",
"details": "詳情",
"dingtalk": "釘釘",
"duration_seconds": "時長(秒)",
"export_confirm": "導出確認",
"feishu": "飛書",
"generation_time": "生成時間",
"input_token_length": "輸入 tokens",
"member": "成員",
@@ -12,14 +18,21 @@
"module_name": "模組名",
"month": "月",
"no_usage_records": "暫無使用紀錄",
"official_account": "公眾號",
"order_number": "訂單編號",
"output_token_length": "輸出 tokens",
"points": "積分",
"project_name": "專案名",
"select_member_and_source_first": "請先選取成員和類型",
"share": "分享連結",
"source": "來源",
"start_export": "已開始匯出",
"text_length": "文字長度",
"token_length": "token 長度",
"total_points": "AI 積分消耗",
"total_points_consumed": "AI 積分消耗",
"total_usage": "總消耗",
"usage_detail": "使用詳情",
"user_type": "類型"
"user_type": "類型",
"wecom": "企業微信"
}

View File

@@ -112,10 +112,5 @@
"team.org.org": "組織",
"team.manage_collaborators": "管理協作者",
"team.no_collaborators": "目前沒有協作者",
"team.write_role_member": "可寫入權限",
"usage.feishu": "飛書",
"usage.dingtalk": "釘釘",
"usage.official_account": "公眾號",
"usage.share": "分享連結",
"usage.wecom": "企業微信"
"team.write_role_member": "可寫入權限"
}