From 0c05add8b2f479b2ebcdc795f7dc8f8945e179a3 Mon Sep 17 00:00:00 2001 From: heheer Date: Thu, 23 Jan 2025 10:54:30 +0800 Subject: [PATCH] feat: usage filter & export & dashbord (#3538) * feat: usage filter & export & dashbord * adjust ui * fix tmb scroll * fix code & selecte all * merge --- packages/global/support/wallet/usage/api.d.ts | 17 + .../global/support/wallet/usage/constants.ts | 14 +- .../global/support/wallet/usage/type.d.ts | 3 + .../common/DateRangePicker/index.tsx | 14 +- .../common/MySelect/MultipleSelect.tsx | 224 +++++++---- packages/web/components/common/Tag/index.tsx | 4 +- packages/web/i18n/en/account_usage.json | 17 +- packages/web/i18n/en/user.json | 7 +- packages/web/i18n/zh-CN/account_usage.json | 19 +- packages/web/i18n/zh-CN/user.json | 7 +- packages/web/i18n/zh-Hant/account_usage.json | 15 +- packages/web/i18n/zh-Hant/user.json | 7 +- pnpm-lock.yaml | 199 +++++++++- projects/app/package.json | 3 +- .../account/bill/components/BillTable.tsx | 2 + .../account/usage/components/ExportModal.tsx | 88 +++++ .../usage/{ => components}/UsageDetail.tsx | 0 .../account/usage/components/UsageForm.tsx | 164 ++++++++ .../account/usage/components/UsageTable.tsx | 188 +++++++++ .../app/src/pages/account/usage/index.tsx | 372 +++++++++++------- .../nodes/NodePluginIO/InputTypeConfig.tsx | 2 + .../app/src/web/support/wallet/usage/api.ts | 23 +- 22 files changed, 1114 insertions(+), 275 deletions(-) create mode 100644 projects/app/src/pages/account/usage/components/ExportModal.tsx rename projects/app/src/pages/account/usage/{ => components}/UsageDetail.tsx (100%) create mode 100644 projects/app/src/pages/account/usage/components/UsageForm.tsx create mode 100644 projects/app/src/pages/account/usage/components/UsageTable.tsx diff --git a/packages/global/support/wallet/usage/api.d.ts b/packages/global/support/wallet/usage/api.d.ts index 9c6825b22..ee7002417 100644 --- a/packages/global/support/wallet/usage/api.d.ts +++ b/packages/global/support/wallet/usage/api.d.ts @@ -6,6 +6,23 @@ export type CreateTrainingUsageProps = { datasetId: string; }; +export type GetTotalPointsProps = { + dateStart: Date; + dateEnd: Date; + teamMemberIds: string[]; + sources: UsageSourceEnum[]; + unit: 'day' | 'week' | 'month'; +}; + +export type GetUsageProps = { + dateStart: Date; + dateEnd: Date; + sources?: UsageSourceEnum[]; + teamMemberIds?: string[]; + projectName?: string; + isSelectAllTmb?: boolean; +}; + export type ConcatUsageProps = UsageListItemCountType & { teamId: string; tmbId: string; diff --git a/packages/global/support/wallet/usage/constants.ts b/packages/global/support/wallet/usage/constants.ts index dfbff7f4d..b20bc8a6d 100644 --- a/packages/global/support/wallet/usage/constants.ts +++ b/packages/global/support/wallet/usage/constants.ts @@ -18,30 +18,30 @@ export const UsageSourceMap = { label: i18nT('common:core.chat.logs.online') }, [UsageSourceEnum.api]: { - label: 'Api' + label: 'API' }, [UsageSourceEnum.shareLink]: { label: i18nT('common:core.chat.logs.free_login') }, [UsageSourceEnum.training]: { - label: 'dataset.Training Name' + label: i18nT('common:dataset.Training Name') }, [UsageSourceEnum.cronJob]: { label: i18nT('common:cron_job_run_app') }, [UsageSourceEnum.feishu]: { - label: i18nT('user:usage.feishu') + label: i18nT('account_usage:feishu') }, [UsageSourceEnum.official_account]: { - label: i18nT('user:usage.official_account') + label: i18nT('account_usage:official_account') }, [UsageSourceEnum.share]: { - label: i18nT('user:usage.share') + label: i18nT('account_usage:share') }, [UsageSourceEnum.wecom]: { - label: i18nT('user:usage.wecom') + label: i18nT('account_usage:wecom') }, [UsageSourceEnum.dingtalk]: { - label: i18nT('user:usage.dingtalk') + label: i18nT('account_usage:dingtalk') } }; diff --git a/packages/global/support/wallet/usage/type.d.ts b/packages/global/support/wallet/usage/type.d.ts index 076c589b5..f34feb580 100644 --- a/packages/global/support/wallet/usage/type.d.ts +++ b/packages/global/support/wallet/usage/type.d.ts @@ -1,3 +1,4 @@ +import { SourceMemberType } from '../../../support/user/type'; import { CreateUsageProps } from './api'; import { UsageSourceEnum } from './constants'; @@ -10,6 +11,7 @@ export type UsageListItemCountType = { // deprecated tokens?: number; }; + export type UsageListItemType = UsageListItemCountType & { moduleName: string; amount: number; @@ -28,4 +30,5 @@ export type UsageItemType = { source: UsageSchemaType['source']; totalPoints: number; list: UsageSchemaType['list']; + sourceMember: SourceMemberType; }; diff --git a/packages/web/components/common/DateRangePicker/index.tsx b/packages/web/components/common/DateRangePicker/index.tsx index 30fe8c989..fe57e8408 100644 --- a/packages/web/components/common/DateRangePicker/index.tsx +++ b/packages/web/components/common/DateRangePicker/index.tsx @@ -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(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)} > diff --git a/packages/web/components/common/MySelect/MultipleSelect.tsx b/packages/web/components/common/MySelect/MultipleSelect.tsx index 2aec556ac..987f947c5 100644 --- a/packages/web/components/common/MySelect/MultipleSelect.tsx +++ b/packages/web/components/common/MySelect/MultipleSelect.tsx @@ -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 = { value: T[]; @@ -25,22 +26,31 @@ export type SelectProps = { value: T; }[]; maxH?: number; + itemWrap?: boolean; onSelect: (val: T[]) => void; closeable?: boolean; + showCheckedIcon?: boolean; + ScrollData?: ReturnType['ScrollData']; + isSelectAll?: boolean; + setIsSelectAll?: React.Dispatch>; } & Omit; const MultipleSelect = ({ value = [], placeholder, list = [], - width = '100%', maxH = 400, onSelect, closeable = false, + showCheckedIcon = true, + itemWrap = true, + ScrollData, + isSelectAll, + setIsSelectAll, ...props }: SelectProps) => { - const { t } = useTranslation(); const ref = useRef(null); + const { t } = useTranslation(); const { isOpen, onOpen, onClose } = useDisclosure(); const menuItemStyles: MenuItemProps = { borderRadius: 'sm', @@ -63,6 +73,71 @@ const MultipleSelect = ({ } }; + 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) => ( + { + e.stopPropagation(); + e.preventDefault(); + onclickItem(item.value); + }} + whiteSpace={'pre-wrap'} + fontSize={'sm'} + gap={2} + > + {!showCheckedIcon && ( + + )} + {item.icon && } + {item.label} + {showCheckedIcon && ( + + {(isSelectAll && !value.includes(item.value)) || + (!isSelectAll && value.includes(item.value) && ( + + ))} + + )} + + ))} + + ); + }, [value, list, isSelectAll]); + + const isAllSelected = useMemo( + () => (isSelectAll && value.length === 0) || (!isSelectAll && value.length === list.length), + [isSelectAll, value, list] + ); + return ( ({ closeOnSelect={false} > ({ _active={{ transform: 'none' }} + _hover={{ + borderColor: 'primary.300' + }} {...props} {...(isOpen ? { @@ -102,82 +178,94 @@ const MultipleSelect = ({ {placeholder} ) : ( - - {value.map((item, i) => { - const listItem = list.find((i) => i.value === item); - if (!listItem) return null; - - return ( - - {listItem.label} - {closeable && ( - { - console.log(111); - e.stopPropagation(); - onclickItem(item); - }} - /> - )} - - ); - })} + + + {isAllSelected ? ( + + {t('common:common.All')} + + ) : ( + (isSelectAll + ? list.filter((item) => !value.includes(item.value)) + : list.filter((item) => value.includes(item.value)) + ).map((item, i) => ( + + {item.label} + {closeable && ( + { + e.stopPropagation(); + onclickItem(item.value); + }} + /> + )} + + )) + )} + + )} { - 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) => ( - onclickItem(item.value)} - whiteSpace={'pre-wrap'} - fontSize={'sm'} - gap={2} - > - {item.icon && } - {item.label} + { + e.stopPropagation(); + e.preventDefault(); + onSelectAll(); + }} + whiteSpace={'pre-wrap'} + fontSize={'sm'} + gap={2} + mb={1} + > + {!showCheckedIcon && } + {t('common:common.All')} + {showCheckedIcon && ( - {value.includes(item.value) && } + {isAllSelected && } - - ))} + )} + + + {ScrollData ? {ListRender} : ListRender} diff --git a/packages/web/components/common/Tag/index.tsx b/packages/web/components/common/Tag/index.tsx index 9a714d369..7236f424b 100644 --- a/packages/web/components/common/Tag/index.tsx +++ b/packages/web/components/common/Tag/index.tsx @@ -66,7 +66,7 @@ const MyTag = ({ children, colorSchema = 'blue', type = 'fill', showDot, ...prop }, [colorSchema]); return ( - {showDot && } {children} - + ); }; diff --git a/packages/web/i18n/en/account_usage.json b/packages/web/i18n/en/account_usage.json index 28b0969a5..1dc6c32ca 100644 --- a/packages/web/i18n/en/account_usage.json +++ b/packages/web/i18n/en/account_usage.json @@ -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" } diff --git a/packages/web/i18n/en/user.json b/packages/web/i18n/en/user.json index 091cffd3c..99501b944 100644 --- a/packages/web/i18n/en/user.json +++ b/packages/web/i18n/en/user.json @@ -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": "" } diff --git a/packages/web/i18n/zh-CN/account_usage.json b/packages/web/i18n/zh-CN/account_usage.json index 7a33417d2..45cfc2fb5 100644 --- a/packages/web/i18n/zh-CN/account_usage.json +++ b/packages/web/i18n/zh-CN/account_usage.json @@ -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": "企业微信" } diff --git a/packages/web/i18n/zh-CN/user.json b/packages/web/i18n/zh-CN/user.json index e0ddec790..9a9734423 100644 --- a/packages/web/i18n/zh-CN/user.json +++ b/packages/web/i18n/zh-CN/user.json @@ -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": "可写权限" } diff --git a/packages/web/i18n/zh-Hant/account_usage.json b/packages/web/i18n/zh-Hant/account_usage.json index 7fd26b94a..2da7a68e6 100644 --- a/packages/web/i18n/zh-Hant/account_usage.json +++ b/packages/web/i18n/zh-Hant/account_usage.json @@ -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": "企業微信" } diff --git a/packages/web/i18n/zh-Hant/user.json b/packages/web/i18n/zh-Hant/user.json index 049178369..36c096c71 100644 --- a/packages/web/i18n/zh-Hant/user.json +++ b/packages/web/i18n/zh-Hant/user.json @@ -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": "可寫入權限" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c9a4b8c37..859fc598b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,7 +22,7 @@ importers: version: 13.3.0 next-i18next: specifier: 15.3.0 - version: 15.3.0(i18next@23.11.5)(next@14.2.5(@babel/core@7.24.9)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 15.3.0(i18next@23.11.5)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) prettier: specifier: 3.2.4 version: 3.2.4 @@ -67,7 +67,7 @@ importers: version: 4.0.2 next: specifier: 14.2.5 - version: 14.2.5(@babel/core@7.24.9)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8) + version: 14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8) openai: specifier: 4.61.0 version: 4.61.0(encoding@0.1.13) @@ -210,7 +210,7 @@ importers: version: 1.4.5-lts.1 next: specifier: 14.2.5 - version: 14.2.5(@babel/core@7.24.9)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8) + version: 14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8) nextjs-cors: specifier: ^2.2.0 version: 2.2.0(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8)) @@ -295,7 +295,7 @@ importers: version: 2.1.1(@chakra-ui/system@2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1))(react@18.3.1) '@chakra-ui/next-js': specifier: 2.1.5 - version: 2.1.5(@chakra-ui/react@2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.5(@babel/core@7.24.9)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react@18.3.1) + version: 2.1.5(@chakra-ui/react@2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react@18.3.1) '@chakra-ui/react': specifier: 2.8.1 version: 2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -358,7 +358,7 @@ importers: version: 4.17.21 next-i18next: specifier: 15.3.0 - version: 15.3.0(i18next@23.11.5)(next@14.2.5(@babel/core@7.24.9)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 15.3.0(i18next@23.11.5)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) papaparse: specifier: ^5.4.1 version: 5.4.1 @@ -558,6 +558,9 @@ importers: reactflow: specifier: ^11.7.4 version: 11.11.4(@types/react@18.3.1)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + recharts: + specifier: ^2.15.0 + version: 2.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rehype-external-links: specifier: ^3.0.0 version: 3.0.0 @@ -3198,8 +3201,8 @@ packages: '@tanstack/react-query@4.36.1': resolution: {integrity: sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==} peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: 18.3.1 + react-dom: 18.3.1 react-native: '*' peerDependenciesMeta: react-dom: @@ -4330,6 +4333,10 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + co@4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} @@ -4760,6 +4767,9 @@ packages: supports-color: optional: true + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + decode-named-character-reference@1.0.2: resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} @@ -4902,6 +4912,9 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dom-serializer@1.4.1: resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} @@ -5215,6 +5228,9 @@ packages: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} @@ -5272,6 +5288,10 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-equals@5.2.0: + resolution: {integrity: sha512-3VpaQYf+CDFdRQfgsb+3vY7XaKjM35WCMoQTTE8h4S/eUkHzyJFOOA/gATYgoLejy4FBrEQD/sXe5Auk4cW/AQ==} + engines: {node: '>=6.0.0'} + fast-fifo@1.3.2: resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} @@ -7828,6 +7848,12 @@ packages: '@types/react': optional: true + react-smooth@4.0.4: + resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-style-singleton@2.2.1: resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} engines: {node: '>=10'} @@ -7849,6 +7875,12 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -7878,6 +7910,16 @@ packages: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} + recharts-scale@0.4.5: + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} + + recharts@2.15.0: + resolution: {integrity: sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw==} + engines: {node: '>=14'} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + redux@4.2.1: resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} @@ -8978,6 +9020,9 @@ packages: vfile@6.0.2: resolution: {integrity: sha512-zND7NlS8rJYb/sPqkb13ZvbbUoExdbi4w3SfRrMq6R3FvnLQmmfpajJNITuuYm6AZ5uao9vy4BAos3EXBPf2rg==} + victory-vendor@36.9.2: + resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + vite-node@1.6.0: resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==} engines: {node: ^18.0.0 || >=20.0.0} @@ -10492,6 +10537,14 @@ snapshots: next: 14.2.5(@babel/core@7.24.9)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8) react: 18.3.1 + '@chakra-ui/next-js@2.1.5(@chakra-ui/react@2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react@18.3.1)': + dependencies: + '@chakra-ui/react': 2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@emotion/cache': 11.11.0 + '@emotion/react': 11.11.1(@types/react@18.3.1)(react@18.3.1) + next: 14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8) + react: 18.3.1 + '@chakra-ui/number-input@2.1.1(@chakra-ui/system@2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1))(react@18.3.1)': dependencies: '@chakra-ui/counter': 2.1.0(react@18.3.1) @@ -13231,7 +13284,7 @@ snapshots: axios@1.7.7: dependencies: - follow-redirects: 1.15.9(debug@4.3.7) + follow-redirects: 1.15.9 form-data: 4.0.1 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -13637,6 +13690,8 @@ snapshots: clone@1.0.4: {} + clsx@2.1.1: {} + co@4.6.0: {} collapse-white-space@1.0.6: {} @@ -14078,6 +14133,8 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js-light@2.5.1: {} + decode-named-character-reference@1.0.2: dependencies: character-entities: 2.0.2 @@ -14234,6 +14291,11 @@ snapshots: dependencies: esutils: 2.0.3 + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.25.7 + csstype: 3.1.3 + dom-serializer@1.4.1: dependencies: domelementtype: 2.3.0 @@ -14731,6 +14793,8 @@ snapshots: event-target-shim@5.0.1: {} + eventemitter3@4.0.7: {} + eventemitter3@5.0.1: {} events@3.3.0: {} @@ -14837,6 +14901,8 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-equals@5.2.0: {} + fast-fifo@1.3.2: {} fast-glob@3.3.2: @@ -15012,6 +15078,8 @@ snapshots: follow-redirects@1.15.6: {} + follow-redirects@1.15.9: {} + follow-redirects@1.15.9(debug@4.3.4): optionalDependencies: debug: 4.3.4 @@ -17359,6 +17427,18 @@ snapshots: react: 18.3.1 react-i18next: 14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next-i18next@15.3.0(i18next@23.11.5)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.24.8 + '@types/hoist-non-react-statics': 3.3.5 + core-js: 3.37.1 + hoist-non-react-statics: 3.3.2 + i18next: 23.11.5 + i18next-fs-backend: 2.3.1 + next: 14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8) + react: 18.3.1 + react-i18next: 14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next@14.2.5(@babel/core@7.24.9)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8): dependencies: '@next/env': 14.2.5 @@ -17385,10 +17465,36 @@ snapshots: - '@babel/core' - babel-plugin-macros + next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8): + dependencies: + '@next/env': 14.2.5 + '@swc/helpers': 0.5.5 + busboy: 1.6.0 + caniuse-lite: 1.0.30001669 + graceful-fs: 4.2.11 + postcss: 8.4.31 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + styled-jsx: 5.1.1(react@18.3.1) + optionalDependencies: + '@next/swc-darwin-arm64': 14.2.5 + '@next/swc-darwin-x64': 14.2.5 + '@next/swc-linux-arm64-gnu': 14.2.5 + '@next/swc-linux-arm64-musl': 14.2.5 + '@next/swc-linux-x64-gnu': 14.2.5 + '@next/swc-linux-x64-musl': 14.2.5 + '@next/swc-win32-arm64-msvc': 14.2.5 + '@next/swc-win32-ia32-msvc': 14.2.5 + '@next/swc-win32-x64-msvc': 14.2.5 + sass: 1.77.8 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + nextjs-cors@2.2.0(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8)): dependencies: cors: 2.8.5 - next: 14.2.5(@babel/core@7.24.9)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8) + next: 14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8) nextjs-node-loader@1.1.5(webpack@5.92.1): dependencies: @@ -18097,6 +18203,14 @@ snapshots: optionalDependencies: '@types/react': 18.3.1 + react-smooth@4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + fast-equals: 5.2.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-style-singleton@2.2.1(@types/react@18.3.1)(react@18.3.1): dependencies: get-nonce: 1.0.1 @@ -18124,6 +18238,15 @@ snapshots: transitivePeerDependencies: - '@types/react' + react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.25.7 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react@18.3.1: dependencies: loose-envify: 1.4.0 @@ -18172,6 +18295,23 @@ snapshots: real-require@0.2.0: {} + recharts-scale@0.4.5: + dependencies: + decimal.js-light: 2.5.1 + + recharts@2.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + clsx: 2.1.1 + eventemitter3: 4.0.7 + lodash: 4.17.21 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-is: 18.3.1 + react-smooth: 4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + recharts-scale: 0.4.5 + tiny-invariant: 1.3.3 + victory-vendor: 36.9.2 + redux@4.2.1: dependencies: '@babel/runtime': 7.24.8 @@ -18791,6 +18931,11 @@ snapshots: '@babel/core': 7.24.9 babel-plugin-macros: 3.1.0 + styled-jsx@5.1.1(react@18.3.1): + dependencies: + client-only: 0.0.1 + react: 18.3.1 + stylis@4.2.0: {} stylis@4.3.2: {} @@ -18992,6 +19137,25 @@ snapshots: ts-dedent@2.2.0: {} + ts-jest@29.2.2(@babel/core@7.24.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.9))(jest@29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.3)))(typescript@5.5.3): + dependencies: + bs-logger: 0.2.6 + ejs: 3.1.10 + fast-json-stable-stringify: 2.1.0 + jest: 29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.3)) + jest-util: 29.7.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.6.3 + typescript: 5.5.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.24.9 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.24.9) + ts-jest@29.2.2(@babel/core@7.24.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.9))(jest@29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.3)))(typescript@5.5.3): dependencies: bs-logger: 0.2.6 @@ -19377,6 +19541,23 @@ snapshots: unist-util-stringify-position: 4.0.0 vfile-message: 4.0.2 + victory-vendor@36.9.2: + dependencies: + '@types/d3-array': 3.2.1 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.8 + '@types/d3-shape': 3.1.6 + '@types/d3-time': 3.0.3 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + vite-node@1.6.0(@types/node@22.7.8)(sass@1.77.8)(terser@5.31.3): dependencies: cac: 6.7.14 diff --git a/projects/app/package.json b/projects/app/package.json index 86370fcda..95dff330e 100644 --- a/projects/app/package.json +++ b/projects/app/package.json @@ -21,8 +21,8 @@ "@fastgpt/global": "workspace:*", "@fastgpt/plugins": "workspace:*", "@fastgpt/service": "workspace:*", - "@fastgpt/web": "workspace:*", "@fastgpt/templates": "workspace:*", + "@fastgpt/web": "workspace:*", "@fortaine/fetch-event-source": "^3.0.6", "@node-rs/jieba": "1.10.0", "@tanstack/react-query": "^4.24.10", @@ -60,6 +60,7 @@ "react-syntax-highlighter": "^15.5.0", "react-textarea-autosize": "^8.5.4", "reactflow": "^11.7.4", + "recharts": "^2.15.0", "rehype-external-links": "^3.0.0", "rehype-katex": "^7.0.0", "remark-breaks": "^4.0.0", diff --git a/projects/app/src/pages/account/bill/components/BillTable.tsx b/projects/app/src/pages/account/bill/components/BillTable.tsx index 228de85cb..5e993c1a3 100644 --- a/projects/app/src/pages/account/bill/components/BillTable.tsx +++ b/projects/app/src/pages/account/bill/components/BillTable.tsx @@ -32,6 +32,7 @@ import MySelect from '@fastgpt/web/components/common/MySelect'; import MyModal from '@fastgpt/web/components/common/MyModal'; import { usePagination } from '@fastgpt/web/hooks/usePagination'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; + const BillTable = () => { const { t } = useTranslation(); const { toast } = useToast(); @@ -177,6 +178,7 @@ export default BillTable; function BillDetailModal({ bill, onClose }: { bill: BillSchemaType; onClose: () => void }) { const { t } = useTranslation(); + return ( void; + params: ExportModalParams; + memberTotal: number; + total: number; +}) => { + const { t } = useTranslation(); + + const { + teamMemberIds, + teamMemberNames, + isSelectAllTmb, + sources, + dateStart, + dateEnd, + projectName + } = params; + + const { runAsync: exportUsage, loading } = useRequest2( + async () => { + const searchParams = new URLSearchParams(); + searchParams.set('dateStart', dateStart.toISOString()); + searchParams.set('dateEnd', dateEnd.toISOString()); + sources.forEach((source) => searchParams.append('sources', source.toString())); + teamMemberIds.forEach((tmbId) => searchParams.append('teamMemberIds', tmbId)); + searchParams.set('isSelectAllTmb', isSelectAllTmb.toString()); + searchParams.set('projectName', projectName); + + await downloadFetch({ + url: `/api/proApi/support/wallet/usage/exportUsage?${searchParams}`, + filename: `usage.csv` + }); + }, + { + successToast: t('account_usage:start_export') + } + ); + + return ( + + + {t('account_usage:current_filter_conditions')} + + {`${t('common:user.Time')}: ${formatTime2YMD(dateStart)} ~ ${formatTime2YMD(dateEnd)}`} + + {`${t('common:user.team.Member')}(${memberTotal}): ${teamMemberNames.join(', ')}`} + + {`${t('common:user.type')}: ${sources.map((item) => t(UsageSourceMap[item].label as any)).join(', ')}`} + + {`${t('common:user.Application Name')}: ${projectName}`} + {t('account_usage:confirm_export', { total })} + + + + + + + ); +}; + +export default ExportModal; diff --git a/projects/app/src/pages/account/usage/UsageDetail.tsx b/projects/app/src/pages/account/usage/components/UsageDetail.tsx similarity index 100% rename from projects/app/src/pages/account/usage/UsageDetail.tsx rename to projects/app/src/pages/account/usage/components/UsageDetail.tsx diff --git a/projects/app/src/pages/account/usage/components/UsageForm.tsx b/projects/app/src/pages/account/usage/components/UsageForm.tsx new file mode 100644 index 000000000..baa69fa5c --- /dev/null +++ b/projects/app/src/pages/account/usage/components/UsageForm.tsx @@ -0,0 +1,164 @@ +import { getTotalPoints } from '@/web/support/wallet/usage/api'; +import { Box, Flex } from '@chakra-ui/react'; +import { formatNumber } from '@fastgpt/global/common/math/tools'; +import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants'; +import { DateRangeType } from '@fastgpt/web/components/common/DateRangePicker'; +import MyBox from '@fastgpt/web/components/common/MyBox'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { addDays } from 'date-fns'; +import { useTranslation } from 'next-i18next'; +import React, { useEffect, useMemo } from 'react'; +import { + ResponsiveContainer, + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + TooltipProps +} from 'recharts'; +import { NameType, ValueType } from 'recharts/types/component/DefaultTooltipContent'; +import { UnitType } from '../index'; + +export type usageFormType = { + date: string; + totalPoints: number; +}; + +const CustomTooltip = ({ active, payload }: TooltipProps) => { + const data = payload?.[0]?.payload as usageFormType; + const { t } = useTranslation(); + if (active && data) { + return ( + + + {data.date} + + + {`${formatNumber(data.totalPoints)} ${t('account_usage:points')}`} + + + ); + } + return null; +}; + +const UsageForm = ({ + dateRange, + selectTmbIds, + usageSources, + unit, + Tabs, + Selectors +}: { + dateRange: DateRangeType; + selectTmbIds: string[]; + usageSources: UsageSourceEnum[]; + unit: UnitType; + Tabs: React.ReactNode; + Selectors: React.ReactNode; +}) => { + const { t } = useTranslation(); + + const { + run: getTotalPointsData, + data: totalPoints, + loading: totalPointsLoading + } = useRequest2( + () => + getTotalPoints({ + dateStart: dateRange.from || new Date(), + dateEnd: addDays(dateRange.to || new Date(), 1), + teamMemberIds: selectTmbIds, + sources: usageSources, + unit + }), + { + manual: true + } + ); + + const totalUsage = useMemo(() => { + return totalPoints?.reduce((acc, curr) => acc + curr.totalPoints, 0); + }, [totalPoints]); + + useEffect(() => { + if (selectTmbIds.length === 0 || usageSources.length === 0) return; + getTotalPointsData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [usageSources, selectTmbIds.length, dateRange, unit]); + + return ( + <> + {Tabs} + {Selectors} + + + {`${t('account_usage:total_usage')}:`} + + {`${formatNumber(totalUsage || 0)} ${t('account_usage:points')}`} + + + + {t('account_usage:points')} + + + + + + { + const { width } = props; + if (width < 500) { + return [width * 0.2, width * 0.4, width * 0.6, width * 0.8]; + } else { + return [ + width * 0.125, + width * 0.25, + width * 0.375, + width * 0.5, + width * 0.625, + width * 0.75, + width * 0.875 + ]; + } + }} + /> + } /> + + + + + + ); +}; + +export default React.memo(UsageForm); diff --git a/projects/app/src/pages/account/usage/components/UsageTable.tsx b/projects/app/src/pages/account/usage/components/UsageTable.tsx new file mode 100644 index 000000000..5a0b788f7 --- /dev/null +++ b/projects/app/src/pages/account/usage/components/UsageTable.tsx @@ -0,0 +1,188 @@ +import { + Box, + Button, + Flex, + Table, + TableContainer, + Tbody, + Td, + Th, + Thead, + Tr +} from '@chakra-ui/react'; +import { formatNumber } from '@fastgpt/global/common/math/tools'; +import { UsageSourceEnum, UsageSourceMap } from '@fastgpt/global/support/wallet/usage/constants'; +import { UsageItemType } from '@fastgpt/global/support/wallet/usage/type'; +import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; +import MyBox from '@fastgpt/web/components/common/MyBox'; +import dayjs from 'dayjs'; +import { useTranslation } from 'next-i18next'; +import { useEffect, useState } from 'react'; +import Avatar from '@fastgpt/web/components/common/Avatar'; +import { usePagination } from '@fastgpt/web/hooks/usePagination'; +import { getUserUsages } from '@/web/support/wallet/usage/api'; +import { useSystem } from '@fastgpt/web/hooks/useSystem'; +import { DateRangeType } from '@fastgpt/web/components/common/DateRangePicker'; +import { addDays } from 'date-fns'; +import { ExportModalParams } from './ExportModal'; +import dynamic from 'next/dynamic'; +import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type'; +import { useToast } from '@fastgpt/web/hooks/useToast'; + +const UsageDetail = dynamic(() => import('./UsageDetail')); +const ExportModal = dynamic(() => import('./ExportModal')); + +const UsageTableList = ({ + dateRange, + selectTmbIds, + usageSources, + projectName, + members, + memberTotal, + isSelectAllTmb, + Tabs, + Selectors +}: { + dateRange: DateRangeType; + selectTmbIds: string[]; + usageSources: UsageSourceEnum[]; + projectName: string; + members: TeamMemberItemType[]; + memberTotal: number; + isSelectAllTmb: boolean; + Tabs: React.ReactNode; + Selectors: React.ReactNode; +}) => { + const { t } = useTranslation(); + const { isPc } = useSystem(); + const { toast } = useToast(); + + const { + data: usages, + isLoading, + Pagination, + getData, + total + } = usePagination(getUserUsages, { + pageSize: isPc ? 20 : 10, + params: { + dateStart: dateRange.from || new Date(), + dateEnd: addDays(dateRange.to || new Date(), 1), + sources: usageSources, + teamMemberIds: selectTmbIds, + isSelectAllTmb, + projectName + }, + defaultRequest: false + }); + + const [usageDetail, setUsageDetail] = useState(); + const [currentParams, setCurrentParams] = useState(null); + + useEffect(() => { + if ((!isSelectAllTmb && selectTmbIds.length === 0) || usageSources.length === 0) return; + getData(1); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [usageSources, selectTmbIds.length, projectName, dateRange, isSelectAllTmb]); + + return ( + <> + {Tabs} + + {Selectors} + + + + + + + + + + + + + + + + + + {usages.map((item) => ( + + + + + + + + + ))} + +
{t('common:user.Time')}{t('account_usage:member')}{t('account_usage:user_type')}{t('account_usage:project_name')}{t('account_usage:total_points')}
{dayjs(item.time).format('YYYY/MM/DD HH:mm:ss')} + + + {item.sourceMember.name} + + {t(UsageSourceMap[item.source]?.label as any) || '-'}{t(item.appName as any) || '-'}{formatNumber(item.totalPoints) || 0} + +
+ {!isLoading && usages.length === 0 && ( + + )} +
+
+ + + + + {!!usageDetail && ( + setUsageDetail(undefined)} /> + )} + + {!!currentParams && ( + setCurrentParams(null)} + params={currentParams} + memberTotal={isSelectAllTmb ? memberTotal - selectTmbIds.length : selectTmbIds.length} + total={total} + /> + )} + + ); +}; + +export default UsageTableList; diff --git a/projects/app/src/pages/account/usage/index.tsx b/projects/app/src/pages/account/usage/index.tsx index 8db661730..17f4b5c55 100644 --- a/projects/app/src/pages/account/usage/index.tsx +++ b/projects/app/src/pages/account/usage/index.tsx @@ -1,76 +1,66 @@ import React, { useEffect, useMemo, useState } from 'react'; -import { - Table, - Thead, - Tbody, - Tr, - Th, - Td, - TableContainer, - Flex, - Box, - Button -} from '@chakra-ui/react'; +import { Flex, Box } from '@chakra-ui/react'; import { UsageSourceEnum, UsageSourceMap } from '@fastgpt/global/support/wallet/usage/constants'; -import { getUserUsages } from '@/web/support/wallet/usage/api'; -import type { UsageItemType } from '@fastgpt/global/support/wallet/usage/type'; -import { usePagination } from '@fastgpt/web/hooks/usePagination'; -import { useLoading } from '@fastgpt/web/hooks/useLoading'; -import dayjs from 'dayjs'; import DateRangePicker, { type DateRangeType } from '@fastgpt/web/components/common/DateRangePicker'; -import { addDays } from 'date-fns'; -import dynamic from 'next/dynamic'; +import { addDays, startOfMonth, startOfWeek } from 'date-fns'; import { useTranslation } from 'next-i18next'; import { useUserStore } from '@/web/support/user/useUserStore'; import Avatar from '@fastgpt/web/components/common/Avatar'; -import MySelect from '@fastgpt/web/components/common/MySelect'; -import { formatNumber } from '@fastgpt/global/common/math/tools'; -import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; -import { useSystem } from '@fastgpt/web/hooks/useSystem'; import AccountContainer from '../components/AccountContainer'; import { serviceSideProps } from '@fastgpt/web/common/system/nextjs'; import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; import { getTeamMembers } from '@/web/support/user/team/api'; +import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs'; +import MultipleSelect from '@fastgpt/web/components/common/MySelect/MultipleSelect'; +import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; +import UsageForm from './components/UsageForm'; +import UsageTableList from './components/UsageTable'; +import MySelect from '@fastgpt/web/components/common/MySelect'; +import { useRouter } from 'next/router'; -const UsageDetail = dynamic(() => import('./UsageDetail')); +export enum UsageTabEnum { + detail = 'detail', + dashboard = 'dashboard' +} + +export type UnitType = 'day' | 'week' | 'month'; const UsageTable = () => { const { t } = useTranslation(); - const { Loading } = useLoading(); + const { userInfo } = useUserStore(); + const router = useRouter(); + const { usageTab = UsageTabEnum.detail } = router.query as { usageTab: `${UsageTabEnum}` }; + const { data: members, ScrollData, total: memberTotal } = useScrollPagination(getTeamMembers, {}); const [dateRange, setDateRange] = useState({ from: addDays(new Date(), -7), to: new Date() }); - const [usageSource, setUsageSource] = useState(''); - const { isPc } = useSystem(); - const { userInfo } = useUserStore(); - const [usageDetail, setUsageDetail] = useState(); + const [selectTmbIds, setSelectTmbIds] = useState([]); + const [usageSources, setUsageSources] = useState( + Object.values(UsageSourceEnum) + ); + const [isSelectAllTmb, setIsSelectAllTmb] = useState(true); + const [unit, setUnit] = useState('day'); + const [projectName, setProjectName] = useState(''); + const [inputValue, setInputValue] = useState(''); const sourceList = useMemo( () => - [ - { label: t('account_usage:all'), value: '' }, - ...Object.entries(UsageSourceMap).map(([key, value]) => ({ - label: t(value.label as any), - value: key - })) - ] as { - label: never; - value: UsageSourceEnum | ''; - }[], + Object.entries(UsageSourceMap).map(([key, value]) => ({ + label: t(value.label as any), + value: key + })), [t] ); - const [selectTmbId, setSelectTmbId] = useState(userInfo?.team?.tmbId); - const { data: members, ScrollData } = useScrollPagination(getTeamMembers, {}); const tmbList = useMemo( () => members.map((item) => ({ label: ( - - + + {item.memberName} ), @@ -79,122 +69,198 @@ const UsageTable = () => { [members] ); - const { - data: usages, - isLoading, - Pagination, - getData - } = usePagination(getUserUsages, { - pageSize: isPc ? 20 : 10, - params: { - dateStart: dateRange.from || new Date(), - dateEnd: addDays(dateRange.to || new Date(), 1), - source: usageSource as UsageSourceEnum, - teamMemberId: selectTmbId ?? '' - }, - defaultRequest: false - }); + const Tabs = useMemo( + () => ( + { + router.replace({ + query: { + ...router.query, + usageTab: e + } + }); + }} + /> + ), + [router, t, usageTab] + ); + + const Selectors = useMemo( + () => ( + + + + {t('common:user.Time')} + + + {usageTab === UsageTabEnum.dashboard && ( + { + if (!dateRange.from) return dateRange; + + switch (val) { + case 'week': + setDateRange({ + from: startOfWeek(dateRange.from, { weekStartsOn: 1 }), + to: dateRange.to + }); + break; + case 'month': + setDateRange({ + from: startOfMonth(dateRange.from), + to: dateRange.to + }); + break; + default: + break; + } + + setUnit(val as 'day' | 'week' | 'month'); + }} + /> + )} + + {tmbList.length > 1 && userInfo?.team?.permission.hasManagePer && ( + + + {t('account_usage:member')} + + + + list={tmbList} + value={selectTmbIds} + onSelect={(val) => { + setSelectTmbIds(val as string[]); + }} + itemWrap={false} + height={'32px'} + bg={'myGray.50'} + w={'160px'} + showCheckedIcon={false} + ScrollData={ScrollData} + isSelectAll={isSelectAllTmb} + setIsSelectAll={setIsSelectAllTmb} + /> + + + )} + + + {t('common:user.type')} + + + + list={sourceList} + value={usageSources} + onSelect={(val) => setUsageSources(val as UsageSourceEnum[])} + itemWrap={false} + height={'32px'} + bg={'myGray.50'} + w={'160px'} + showCheckedIcon={false} + /> + + + {usageTab === UsageTabEnum.detail && ( + + + {t('common:user.Application Name')} + + setInputValue(e.target.value)} + /> + + )} + + ), + [ + dateRange, + selectTmbIds, + sourceList, + t, + tmbList, + unit, + usageSources, + usageTab, + inputValue, + isSelectAllTmb + ] + ); useEffect(() => { - getData(1); - }, [usageSource, selectTmbId]); + const timer = setTimeout(() => { + setProjectName(inputValue); + }, 300); + + return () => clearTimeout(timer); + }, [inputValue]); return ( - - - {tmbList.length > 1 && userInfo?.team?.permission.hasManagePer && ( - - - {t('account_usage:member')} - - - - )} - - - getData(1)} - /> - - - - - - - - {/* */} - - - - - - - - - {usages.map((item) => ( - - {/* */} - - - - - - - ))} - -
{t('account_usage:user.team.Member Name')}{t('account_usage:user_type')} - - list={sourceList} - value={usageSource} - size={'sm'} - onchange={(e) => { - setUsageSource(e); - }} - w={'130px'} - > - {t('account_usage:project_name')}{t('account_usage:total_points')}
{item.memberName}{dayjs(item.time).format('YYYY/MM/DD HH:mm:ss')}{t(UsageSourceMap[item.source]?.label as any) || '-'}{t(item.appName as any) || '-'}{formatNumber(item.totalPoints) || 0} - -
- {!isLoading && usages.length === 0 && ( - - )} -
- - - {!!usageDetail && ( - setUsageDetail(undefined)} /> + + {usageTab === UsageTabEnum.detail && ( + )} -
+ {usageTab === UsageTabEnum.dashboard && ( + + )} +
); }; diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/InputTypeConfig.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/InputTypeConfig.tsx index 9d5894778..0098f7b05 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/InputTypeConfig.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/InputTypeConfig.tsx @@ -359,6 +359,8 @@ const InputTypeConfig = ({ list={valueTypeSelectList} bg={'myGray.50'} + minH={'40px'} + py={2} value={selectValueTypeList || []} onSelect={(e) => { setValue('customInputConfig.selectValueTypeList', e); diff --git a/projects/app/src/web/support/wallet/usage/api.ts b/projects/app/src/web/support/wallet/usage/api.ts index c4ce4171a..6fccc9bdb 100644 --- a/projects/app/src/web/support/wallet/usage/api.ts +++ b/projects/app/src/web/support/wallet/usage/api.ts @@ -1,17 +1,20 @@ import { POST } from '@/web/common/api/request'; -import { CreateTrainingUsageProps } from '@fastgpt/global/support/wallet/usage/api.d'; -import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants'; +import { + CreateTrainingUsageProps, + GetTotalPointsProps, + GetUsageProps +} from '@fastgpt/global/support/wallet/usage/api.d'; import type { UsageItemType } from '@fastgpt/global/support/wallet/usage/type'; import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type'; -export const getUserUsages = ( - data: PaginationProps<{ - dateStart: Date; - dateEnd: Date; - source?: UsageSourceEnum; - teamMemberId: string; - }> -) => POST>(`/proApi/support/wallet/usage/getUsage`, data); +export const getUserUsages = (data: PaginationProps) => + POST>(`/proApi/support/wallet/usage/getUsage`, data); + +export const getTotalPoints = (data: GetTotalPointsProps) => + POST<{ totalPoints: number; date: string }[]>( + `/proApi/support/wallet/usage/getTotalPoints`, + data + ); export const postCreateTrainingUsage = (data: CreateTrainingUsageProps) => POST(`/support/wallet/usage/createTrainingUsage`, data);