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

@@ -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",

View File

@@ -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 (
<MyModal
isOpen={true}

View File

@@ -0,0 +1,88 @@
import { downloadFetch } from '@/web/common/system/utils';
import { Button, Flex, ModalBody, ModalFooter } from '@chakra-ui/react';
import { formatTime2YMD } from '@fastgpt/global/common/string/time';
import { UsageSourceEnum, UsageSourceMap } from '@fastgpt/global/support/wallet/usage/constants';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
export type ExportModalParams = {
dateStart: Date;
dateEnd: Date;
sources: UsageSourceEnum[];
teamMemberIds: string[];
teamMemberNames: string[];
isSelectAllTmb: boolean;
projectName: string;
};
const ExportModal = ({
onClose,
params,
memberTotal,
total
}: {
onClose: () => 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 (
<MyModal title={t('account_usage:export_confirm')} iconSrc="export" iconColor={'primary.600'}>
<ModalBody>
<Flex mb={4}>{t('account_usage:current_filter_conditions')}</Flex>
<Flex>
{`${t('common:user.Time')}: ${formatTime2YMD(dateStart)} ~ ${formatTime2YMD(dateEnd)}`}
</Flex>
<Flex>{`${t('common:user.team.Member')}(${memberTotal}): ${teamMemberNames.join(', ')}`}</Flex>
<Flex>
{`${t('common:user.type')}: ${sources.map((item) => t(UsageSourceMap[item].label as any)).join(', ')}`}
</Flex>
<Flex>{`${t('common:user.Application Name')}: ${projectName}`}</Flex>
<Flex mt={4}>{t('account_usage:confirm_export', { total })}</Flex>
</ModalBody>
<ModalFooter gap={2}>
<Button variant={'whiteBase'} onClick={onClose}>
{t('common:common.Cancel')}
</Button>
<Button onClick={exportUsage} isLoading={loading}>
{t('common:Export')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default ExportModal;

View File

@@ -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<ValueType, NameType>) => {
const data = payload?.[0]?.payload as usageFormType;
const { t } = useTranslation();
if (active && data) {
return (
<Box
bg={'white'}
p={3}
borderRadius={'md'}
border={'0.5px solid'}
borderColor={'myGray.200'}
boxShadow={
'0px 24px 48px -12px rgba(19, 51, 107, 0.20), 0px 0px 1px 0px rgba(19, 51, 107, 0.20)'
}
>
<Box fontSize={'mini'} color={'myGray.600'} mb={3}>
{data.date}
</Box>
<Box fontSize={'14px'} color={'myGray.900'} fontWeight={'medium'}>
{`${formatNumber(data.totalPoints)} ${t('account_usage:points')}`}
</Box>
</Box>
);
}
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 (
<>
<Box>{Tabs}</Box>
<Box>{Selectors}</Box>
<MyBox isLoading={totalPointsLoading}>
<Flex fontSize={'20px'} fontWeight={'medium'} my={6}>
<Box color={'black'}>{`${t('account_usage:total_usage')}:`}</Box>
<Box color={'primary.600'} ml={2}>
{`${formatNumber(totalUsage || 0)} ${t('account_usage:points')}`}
</Box>
</Flex>
<Flex mb={4} fontSize={'mini'} color={'myGray.500'} fontWeight={'medium'}>
{t('account_usage:points')}
</Flex>
<ResponsiveContainer width="100%" height={424}>
<LineChart data={totalPoints} margin={{ top: 10, right: 30, left: -12, bottom: 0 }}>
<XAxis
dataKey="date"
padding={{ left: 40, right: 40 }}
tickMargin={10}
tickSize={0}
tick={{ fontSize: '12px', color: '#667085', fontWeight: '500' }}
/>
<YAxis
axisLine={false}
tickSize={0}
tickMargin={12}
tick={{ fontSize: '12px', color: '#667085', fontWeight: '500' }}
/>
<CartesianGrid
strokeDasharray="3 3"
verticalCoordinatesGenerator={(props) => {
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
];
}
}}
/>
<Tooltip content={<CustomTooltip />} />
<Line
type="monotone"
dataKey="totalPoints"
stroke="#5E8FFF"
strokeWidth={1.5}
dot={false}
/>
</LineChart>
</ResponsiveContainer>
</MyBox>
</>
);
};
export default React.memo(UsageForm);

View File

@@ -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<UsageItemType>();
const [currentParams, setCurrentParams] = useState<ExportModalParams | null>(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 (
<>
<Box>{Tabs}</Box>
<Flex flexDir={['column', 'row']} w={'100%'} alignItems={['flex-end', 'center']}>
<Box>{Selectors}</Box>
<Box flex={'1'} />
<Button
size={'md'}
onClick={() => {
if ((selectTmbIds.length === 0 && !isSelectAllTmb) || usageSources.length === 0) {
return toast({
status: 'warning',
title: t('account_usage:select_member_and_source_first')
});
}
setCurrentParams({
dateStart: dateRange.from || new Date(),
dateEnd: addDays(dateRange.to || new Date(), 1),
sources: usageSources,
teamMemberIds: selectTmbIds,
teamMemberNames: members
.filter((item) =>
isSelectAllTmb
? !selectTmbIds.includes(item.tmbId)
: selectTmbIds.includes(item.tmbId)
)
.map((item) => item.memberName),
isSelectAllTmb,
projectName
});
}}
>
{t('common:Export')}
</Button>
</Flex>
<MyBox position={'relative'} overflowY={'auto'} mt={3} flex={1} isLoading={isLoading}>
<TableContainer>
<Table>
<Thead>
<Tr>
<Th>{t('common:user.Time')}</Th>
<Th>{t('account_usage:member')}</Th>
<Th>{t('account_usage:user_type')}</Th>
<Th>{t('account_usage:project_name')}</Th>
<Th>{t('account_usage:total_points')}</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody fontSize={'sm'}>
{usages.map((item) => (
<Tr key={item.id}>
<Td>{dayjs(item.time).format('YYYY/MM/DD HH:mm:ss')}</Td>
<Td>
<Flex alignItems={'center'} color={'myGray.500'}>
<Avatar src={item.sourceMember.avatar} w={'20px'} mr={1} rounded={'full'} />
{item.sourceMember.name}
</Flex>
</Td>
<Td>{t(UsageSourceMap[item.source]?.label as any) || '-'}</Td>
<Td>{t(item.appName as any) || '-'}</Td>
<Td>{formatNumber(item.totalPoints) || 0}</Td>
<Td>
<Button
size={'sm'}
variant={'whitePrimary'}
onClick={() => setUsageDetail(item)}
>
{t('account_usage:details')}
</Button>
</Td>
</Tr>
))}
</Tbody>
</Table>
{!isLoading && usages.length === 0 && (
<EmptyTip text={t('account_usage:no_usage_records')}></EmptyTip>
)}
</TableContainer>
</MyBox>
<Flex mt={3} justifyContent={'center'}>
<Pagination />
</Flex>
{!!usageDetail && (
<UsageDetail usage={usageDetail} onClose={() => setUsageDetail(undefined)} />
)}
{!!currentParams && (
<ExportModal
onClose={() => setCurrentParams(null)}
params={currentParams}
memberTotal={isSelectAllTmb ? memberTotal - selectTmbIds.length : selectTmbIds.length}
total={total}
/>
)}
</>
);
};
export default UsageTableList;

View File

@@ -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<DateRangeType>({
from: addDays(new Date(), -7),
to: new Date()
});
const [usageSource, setUsageSource] = useState<UsageSourceEnum | ''>('');
const { isPc } = useSystem();
const { userInfo } = useUserStore();
const [usageDetail, setUsageDetail] = useState<UsageItemType>();
const [selectTmbIds, setSelectTmbIds] = useState<string[]>([]);
const [usageSources, setUsageSources] = useState<UsageSourceEnum[]>(
Object.values(UsageSourceEnum)
);
const [isSelectAllTmb, setIsSelectAllTmb] = useState<boolean>(true);
const [unit, setUnit] = useState<UnitType>('day');
const [projectName, setProjectName] = useState<string>('');
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: (
<Flex alignItems={'center'}>
<Avatar src={item.avatar} w={'16px'} mr={1} />
<Flex alignItems={'center'} color={'myGray.500'}>
<Avatar src={item.avatar} w={'20px'} mr={1} rounded={'full'} />
{item.memberName}
</Flex>
),
@@ -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(
() => (
<FillRowTabs
list={[
{ label: t('account_usage:usage_detail'), value: 'detail' },
{ label: t('account_usage:dashboard'), value: 'dashboard' }
]}
px={'1rem'}
value={usageTab}
onChange={(e) => {
router.replace({
query: {
...router.query,
usageTab: e
}
});
}}
/>
),
[router, t, usageTab]
);
const Selectors = useMemo(
() => (
<Flex mt={4}>
<Flex alignItems={'center'}>
<Box fontSize={'mini'} fontWeight={'medium'} color={'myGray.900'} mr={4}>
{t('common:user.Time')}
</Box>
<DateRangePicker
defaultDate={dateRange}
dateRange={dateRange}
position="bottom"
onChange={setDateRange}
/>
{usageTab === UsageTabEnum.dashboard && (
<MySelect
bg={'myGray.50'}
minH={'32px'}
height={'32px'}
fontSize={'mini'}
ml={1}
list={[
{ label: t('account_usage:every_day'), value: 'day' },
{ label: t('account_usage:every_week'), value: 'week' },
{ label: t('account_usage:every_month'), value: 'month' }
]}
value={unit}
onchange={(val) => {
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');
}}
/>
)}
</Flex>
{tmbList.length > 1 && userInfo?.team?.permission.hasManagePer && (
<Flex alignItems={'center'} ml={6}>
<Box fontSize={'mini'} fontWeight={'medium'} color={'myGray.900'} mr={4}>
{t('account_usage:member')}
</Box>
<Box>
<MultipleSelect<string>
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}
/>
</Box>
</Flex>
)}
<Flex alignItems={'center'} ml={6}>
<Box fontSize={'mini'} fontWeight={'medium'} color={'myGray.900'} mr={4}>
{t('common:user.type')}
</Box>
<Box>
<MultipleSelect<string>
list={sourceList}
value={usageSources}
onSelect={(val) => setUsageSources(val as UsageSourceEnum[])}
itemWrap={false}
height={'32px'}
bg={'myGray.50'}
w={'160px'}
showCheckedIcon={false}
/>
</Box>
</Flex>
{usageTab === UsageTabEnum.detail && (
<Flex alignItems={'center'} ml={6}>
<Box
fontSize={'mini'}
fontWeight={'medium'}
color={'myGray.900'}
mr={4}
whiteSpace={'nowrap'}
>
{t('common:user.Application Name')}
</Box>
<SearchInput
placeholder={t('common:user.Application Name')}
w={'160px'}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
</Flex>
)}
</Flex>
),
[
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 (
<AccountContainer>
<Flex flexDirection={'column'} py={[0, 5]} h={'100%'} position={'relative'}>
<Flex
flexDir={['column', 'row']}
gap={2}
w={'100%'}
px={[3, 8]}
alignItems={['flex-end', 'center']}
>
{tmbList.length > 1 && userInfo?.team?.permission.hasManagePer && (
<Flex alignItems={'center'}>
<Box mr={2} flexShrink={0}>
{t('account_usage:member')}
</Box>
<MySelect
size={'sm'}
minW={'100px'}
ScrollData={ScrollData}
list={tmbList}
value={selectTmbId}
onchange={setSelectTmbId}
/>
</Flex>
)}
<Box flex={'1'} />
<Flex alignItems={'center'} gap={3}>
<DateRangePicker
defaultDate={dateRange}
position="bottom"
onChange={setDateRange}
onSuccess={() => getData(1)}
/>
<Pagination />
</Flex>
</Flex>
<TableContainer
mt={2}
px={[3, 8]}
position={'relative'}
flex={'1 0 0'}
h={0}
overflowY={'auto'}
>
<Table>
<Thead>
<Tr>
{/* <Th>{t('account_usage:user.team.Member Name')}</Th> */}
<Th>{t('account_usage:user_type')}</Th>
<Th>
<MySelect<UsageSourceEnum | ''>
list={sourceList}
value={usageSource}
size={'sm'}
onchange={(e) => {
setUsageSource(e);
}}
w={'130px'}
></MySelect>
</Th>
<Th>{t('account_usage:project_name')}</Th>
<Th>{t('account_usage:total_points')}</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody fontSize={'sm'}>
{usages.map((item) => (
<Tr key={item.id}>
{/* <Td>{item.memberName}</Td> */}
<Td>{dayjs(item.time).format('YYYY/MM/DD HH:mm:ss')}</Td>
<Td>{t(UsageSourceMap[item.source]?.label as any) || '-'}</Td>
<Td>{t(item.appName as any) || '-'}</Td>
<Td>{formatNumber(item.totalPoints) || 0}</Td>
<Td>
<Button
size={'sm'}
variant={'whitePrimary'}
onClick={() => setUsageDetail(item)}
>
{t('account_usage:details')}
</Button>
</Td>
</Tr>
))}
</Tbody>
</Table>
{!isLoading && usages.length === 0 && (
<EmptyTip text={t('account_usage:no_usage_records')}></EmptyTip>
)}
</TableContainer>
<Loading loading={isLoading} fixed={false} />
{!!usageDetail && (
<UsageDetail usage={usageDetail} onClose={() => setUsageDetail(undefined)} />
<Box
px={[3, 8]}
pt={[0, 8]}
pb={[0, 4]}
h={'full'}
overflow={'hidden'}
display={'flex'}
flexDirection={'column'}
>
{usageTab === UsageTabEnum.detail && (
<UsageTableList
dateRange={dateRange}
selectTmbIds={selectTmbIds}
usageSources={usageSources}
projectName={projectName}
members={members}
memberTotal={memberTotal}
isSelectAllTmb={isSelectAllTmb}
Tabs={Tabs}
Selectors={Selectors}
/>
)}
</Flex>
{usageTab === UsageTabEnum.dashboard && (
<UsageForm
dateRange={dateRange}
selectTmbIds={selectTmbIds}
usageSources={usageSources}
unit={unit}
Tabs={Tabs}
Selectors={Selectors}
/>
)}
</Box>
</AccountContainer>
);
};

View File

@@ -359,6 +359,8 @@ const InputTypeConfig = ({
<MultipleSelect<WorkflowIOValueTypeEnum>
list={valueTypeSelectList}
bg={'myGray.50'}
minH={'40px'}
py={2}
value={selectValueTypeList || []}
onSelect={(e) => {
setValue('customInputConfig.selectValueTypeList', e);

View File

@@ -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<PaginationResponse<UsageItemType>>(`/proApi/support/wallet/usage/getUsage`, data);
export const getUserUsages = (data: PaginationProps<GetUsageProps>) =>
POST<PaginationResponse<UsageItemType>>(`/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<string>(`/support/wallet/usage/createTrainingUsage`, data);