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:
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
164
projects/app/src/pages/account/usage/components/UsageForm.tsx
Normal file
164
projects/app/src/pages/account/usage/components/UsageForm.tsx
Normal 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);
|
||||
188
projects/app/src/pages/account/usage/components/UsageTable.tsx
Normal file
188
projects/app/src/pages/account/usage/components/UsageTable.tsx
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user