invite url
This commit is contained in:
@@ -35,6 +35,7 @@ const queryClient = new QueryClient({
|
||||
|
||||
function App({ Component, pageProps }: AppProps) {
|
||||
const router = useRouter();
|
||||
const { hiId } = router.query as { hiId?: string };
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const [scripts, setScripts] = useState<FeConfigsType['scripts']>([]);
|
||||
@@ -50,6 +51,10 @@ function App({ Component, pageProps }: AppProps) {
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
hiId && localStorage.setItem('inviterId', hiId);
|
||||
}, [hiId]);
|
||||
|
||||
useEffect(() => {
|
||||
const lang = getLangStore() || 'zh';
|
||||
i18n?.changeLanguage?.(lang);
|
||||
|
||||
148
client/src/pages/account/components/Promotion.tsx
Normal file
148
client/src/pages/account/components/Promotion.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Grid,
|
||||
Box,
|
||||
Flex,
|
||||
BoxProps,
|
||||
useTheme,
|
||||
Button,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getPromotionInitData, getPromotionRecords } from '@/api/user';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import { useCopyData } from '@/utils/tools';
|
||||
import { usePagination } from '@/hooks/usePagination';
|
||||
import { PromotionRecordType } from '@/api/response/user';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const Promotion = () => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { copyData } = useCopyData();
|
||||
const { userInfo } = useUserStore();
|
||||
const { Loading } = useLoading();
|
||||
|
||||
const {
|
||||
data: promotionRecords,
|
||||
isLoading,
|
||||
total,
|
||||
pageSize,
|
||||
Pagination
|
||||
} = usePagination<PromotionRecordType>({
|
||||
api: getPromotionRecords
|
||||
});
|
||||
|
||||
const { data: { invitedAmount = 0, earningsAmount = 0 } = {} } = useQuery(
|
||||
['getPromotionInitData'],
|
||||
getPromotionInitData
|
||||
);
|
||||
|
||||
const statisticsStyles: BoxProps = {
|
||||
p: [4, 5],
|
||||
border: theme.borders.base,
|
||||
textAlign: 'center',
|
||||
fontSize: ['md', 'xl'],
|
||||
borderRadius: 'md'
|
||||
};
|
||||
const titleStyles: BoxProps = {
|
||||
mt: 2,
|
||||
fontSize: ['lg', '28px'],
|
||||
fontWeight: 'bold'
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} py={[0, 5]} px={5} h={'100%'} position={'relative'}>
|
||||
<Grid gridTemplateColumns={['1fr 1fr', 'repeat(2,1fr)', 'repeat(4,1fr)']} gridGap={5}>
|
||||
<Box {...statisticsStyles}>
|
||||
<Box>{t('user.Amount of inviter')}</Box>
|
||||
<Box {...titleStyles}>{invitedAmount}</Box>
|
||||
</Box>
|
||||
<Box {...statisticsStyles}>
|
||||
<Box>{t('user.Amount of earnings')}</Box>
|
||||
<Box {...titleStyles}>{earningsAmount}</Box>
|
||||
</Box>
|
||||
<Box {...statisticsStyles}>
|
||||
<Flex alignItems={'center'} justifyContent={'center'}>
|
||||
<Box>{t('user.Promotion Rate')}</Box>
|
||||
<MyTooltip label={t('user.Promotion rate tip')}>
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Box {...titleStyles}>{userInfo?.promotionRate || 15}%</Box>
|
||||
</Box>
|
||||
<Box {...statisticsStyles}>
|
||||
<Flex alignItems={'center'} justifyContent={'center'}>
|
||||
<Box>{t('user.Invite Url')}</Box>
|
||||
<MyTooltip label={t('user.Invite url tip')}>
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Button
|
||||
mt={4}
|
||||
variant={'base'}
|
||||
fontSize={'sm'}
|
||||
onClick={() => {
|
||||
copyData(`${location.origin}/?hiId=${userInfo?._id}`);
|
||||
}}
|
||||
>
|
||||
{t('user.Copy invite url')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Box mt={5}>
|
||||
<TableContainer position={'relative'} overflow={'hidden'} minH={'100px'}>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>时间</Th>
|
||||
<Th>类型</Th>
|
||||
<Th>金额</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody fontSize={'sm'}>
|
||||
{promotionRecords.map((item) => (
|
||||
<Tr key={item._id}>
|
||||
<Td>
|
||||
{item.createTime ? dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss') : '-'}
|
||||
</Td>
|
||||
<Td>{t(`user.promotion.${item.type}`)}</Td>
|
||||
<Td>{item.amount}</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{!isLoading && promotionRecords.length === 0 && (
|
||||
<Flex mt={'10vh'} flexDirection={'column'} alignItems={'center'}>
|
||||
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
|
||||
<Box mt={2} color={'myGray.500'}>
|
||||
无邀请记录~
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
{total > pageSize && (
|
||||
<Flex mt={4} justifyContent={'flex-end'}>
|
||||
<Pagination />
|
||||
</Flex>
|
||||
)}
|
||||
<Loading loading={isLoading} fixed={false} />
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default Promotion;
|
||||
@@ -12,7 +12,11 @@ import Tabs from '@/components/Tabs';
|
||||
import UserInfo from './components/Info';
|
||||
import { serviceSideProps } from '@/utils/i18n';
|
||||
import { feConfigs } from '@/store/static';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const Promotion = dynamic(() => import('./components/Promotion'), {
|
||||
ssr: false
|
||||
});
|
||||
const BillTable = dynamic(() => import('./components/BillTable'), {
|
||||
ssr: false
|
||||
});
|
||||
@@ -25,6 +29,7 @@ const InformTable = dynamic(() => import('./components/InformTable'), {
|
||||
|
||||
enum TabEnum {
|
||||
'info' = 'info',
|
||||
'promotion' = 'promotion',
|
||||
'bill' = 'bill',
|
||||
'pay' = 'pay',
|
||||
'inform' = 'inform',
|
||||
@@ -32,40 +37,42 @@ enum TabEnum {
|
||||
}
|
||||
|
||||
const Account = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
|
||||
const { t } = useTranslation();
|
||||
const tabList = useRef([
|
||||
{
|
||||
icon: 'meLight',
|
||||
label: 'user.Personal Information',
|
||||
id: TabEnum.info,
|
||||
Component: <BillTable />
|
||||
label: t('user.Personal Information'),
|
||||
id: TabEnum.info
|
||||
},
|
||||
|
||||
{
|
||||
icon: 'billRecordLight',
|
||||
label: 'user.Usage Record',
|
||||
id: TabEnum.bill,
|
||||
Component: <BillTable />
|
||||
label: t('user.Usage Record'),
|
||||
id: TabEnum.bill
|
||||
},
|
||||
...(feConfigs?.show_userDetail
|
||||
? [
|
||||
{
|
||||
icon: 'promotionLight',
|
||||
label: t('user.Promotion Record'),
|
||||
id: TabEnum.promotion
|
||||
},
|
||||
{
|
||||
icon: 'payRecordLight',
|
||||
label: 'user.Recharge Record',
|
||||
id: TabEnum.pay,
|
||||
Component: <PayRecordTable />
|
||||
label: t('user.Recharge Record'),
|
||||
id: TabEnum.pay
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
icon: 'informLight',
|
||||
label: 'user.Notice',
|
||||
id: TabEnum.inform,
|
||||
Component: <InformTable />
|
||||
label: t('user.Notice'),
|
||||
id: TabEnum.inform
|
||||
},
|
||||
{
|
||||
icon: 'loginoutLight',
|
||||
label: 'user.Sign Out',
|
||||
id: TabEnum.loginout,
|
||||
Component: () => <></>
|
||||
label: t('user.Sign Out'),
|
||||
id: TabEnum.loginout
|
||||
}
|
||||
]);
|
||||
|
||||
@@ -122,7 +129,6 @@ const Account = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
|
||||
<Box mb={3}>
|
||||
<Tabs
|
||||
m={'auto'}
|
||||
w={'90%'}
|
||||
size={isPc ? 'md' : 'sm'}
|
||||
list={tabList.current.map((item) => ({
|
||||
id: item.id,
|
||||
@@ -136,6 +142,7 @@ const Account = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
|
||||
|
||||
<Box flex={'1 0 0'} h={'100%'} pb={[4, 0]}>
|
||||
{currentTab === TabEnum.info && <UserInfo />}
|
||||
{currentTab === TabEnum.promotion && <Promotion />}
|
||||
{currentTab === TabEnum.bill && <BillTable />}
|
||||
{currentTab === TabEnum.pay && <PayRecordTable />}
|
||||
{currentTab === TabEnum.inform && <InformTable />}
|
||||
|
||||
@@ -24,7 +24,7 @@ type GithubUserType = {
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { code } = req.query as { code: string };
|
||||
const { code, inviterId } = req.query as { code: string; inviterId?: string };
|
||||
|
||||
const { data: gitAccessToken } = await axios.post<string>(
|
||||
`https://github.com/login/oauth/access_token?client_id=${global.feConfigs.gitLoginKey}&client_secret=${global.systemEnv.gitLoginSecret}&code=${code}`
|
||||
@@ -51,7 +51,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
} catch (err: any) {
|
||||
if (err?.code === 500) {
|
||||
jsonRes(res, {
|
||||
data: await registerUser({ username, avatar: avatar_url, res })
|
||||
data: await registerUser({ username, avatar: avatar_url, res, inviterId })
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -88,17 +88,21 @@ export async function loginByUsername({
|
||||
export async function registerUser({
|
||||
username,
|
||||
avatar,
|
||||
inviterId,
|
||||
res
|
||||
}: {
|
||||
username: string;
|
||||
avatar?: string;
|
||||
inviterId?: string;
|
||||
res: NextApiResponse;
|
||||
}) {
|
||||
const response = await User.create({
|
||||
username,
|
||||
avatar,
|
||||
password: nanoid()
|
||||
password: nanoid(),
|
||||
inviterId
|
||||
});
|
||||
console.log(response, '-=-=-=');
|
||||
|
||||
// 根据 id 获取用户信息
|
||||
const user = await User.findById(response._id);
|
||||
|
||||
51
client/src/pages/api/user/promotion/getPromotionData.ts
Normal file
51
client/src/pages/api/user/promotion/getPromotionData.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, User, promotionRecord } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
const invitedAmount = await User.countDocuments({
|
||||
inviterId: userId
|
||||
});
|
||||
|
||||
// 计算累计合
|
||||
const countHistory: { totalAmount: number }[] = await promotionRecord.aggregate([
|
||||
{
|
||||
$match: {
|
||||
userId: new mongoose.Types.ObjectId(userId),
|
||||
amount: { $gt: 0 }
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null, // 分组条件,这里使用 null 表示不分组
|
||||
totalAmount: { $sum: '$amount' } // 计算 amount 字段的总和
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: false, // 排除 _id 字段
|
||||
totalAmount: true // 只返回 totalAmount 字段
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
jsonRes(res, {
|
||||
data: {
|
||||
invitedAmount,
|
||||
historyAmount: countHistory[0]?.totalAmount || 0
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
44
client/src/pages/api/user/promotion/getPromotions.ts
Normal file
44
client/src/pages/api/user/promotion/getPromotions.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, promotionRecord } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
let { pageNum = 1, pageSize = 10 } = req.body as {
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
};
|
||||
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
const data = await promotionRecord
|
||||
.find(
|
||||
{
|
||||
userId
|
||||
},
|
||||
'_id createTime type amount'
|
||||
)
|
||||
.sort({ _id: -1 })
|
||||
.skip((pageNum - 1) * pageSize)
|
||||
.limit(pageSize);
|
||||
|
||||
jsonRes(res, {
|
||||
data: {
|
||||
pageNum,
|
||||
pageSize,
|
||||
data,
|
||||
total: await promotionRecord.countDocuments({
|
||||
userId
|
||||
})
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,6 @@ interface RegisterType {
|
||||
}
|
||||
|
||||
const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
|
||||
const { inviterId = '' } = useRouter().query as { inviterId: string };
|
||||
const { toast } = useToast();
|
||||
const {
|
||||
register,
|
||||
@@ -58,7 +57,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
|
||||
username,
|
||||
code,
|
||||
password,
|
||||
inviterId: inviterId || localStorage.getItem('inviterId') || ''
|
||||
inviterId: localStorage.getItem('inviterId') || ''
|
||||
})
|
||||
);
|
||||
toast({
|
||||
@@ -81,7 +80,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
|
||||
}
|
||||
setRequesting(false);
|
||||
},
|
||||
[inviterId, loginSuccess, toast]
|
||||
[loginSuccess, toast]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -44,7 +44,10 @@ const provider = ({ code }: { code: string }) => {
|
||||
try {
|
||||
const res = await (async () => {
|
||||
if (loginStore.provider === 'git') {
|
||||
return gitLogin(code);
|
||||
return gitLogin({
|
||||
code,
|
||||
inviterId: localStorage.getItem('inviterId') || ''
|
||||
});
|
||||
}
|
||||
return null;
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user