mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-21 03:35:36 +00:00
feat: 增加充值功能
This commit is contained in:
@@ -28,6 +28,7 @@
|
||||
"immer": "^9.0.19",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"mongoose": "^6.10.0",
|
||||
"nanoid": "^4.0.1",
|
||||
"next": "13.1.6",
|
||||
"nodemailer": "^6.9.1",
|
||||
"nprogress": "^0.2.0",
|
||||
@@ -43,7 +44,6 @@
|
||||
"sass": "^1.58.3",
|
||||
"sharp": "^0.31.3",
|
||||
"tunnel": "^0.0.6",
|
||||
"uuid": "^9.0.0",
|
||||
"zustand": "^4.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
19
pnpm-lock.yaml
generated
19
pnpm-lock.yaml
generated
@@ -32,6 +32,7 @@ specifiers:
|
||||
jsonwebtoken: ^9.0.0
|
||||
lint-staged: ^13.1.2
|
||||
mongoose: ^6.10.0
|
||||
nanoid: ^4.0.1
|
||||
next: 13.1.6
|
||||
nodemailer: ^6.9.1
|
||||
nprogress: ^0.2.0
|
||||
@@ -49,7 +50,6 @@ specifiers:
|
||||
sharp: ^0.31.3
|
||||
tunnel: ^0.0.6
|
||||
typescript: 4.9.5
|
||||
uuid: ^9.0.0
|
||||
zustand: ^4.3.5
|
||||
|
||||
dependencies:
|
||||
@@ -70,6 +70,7 @@ dependencies:
|
||||
immer: registry.npmmirror.com/immer/9.0.19
|
||||
jsonwebtoken: registry.npmmirror.com/jsonwebtoken/9.0.0
|
||||
mongoose: registry.npmmirror.com/mongoose/6.10.0
|
||||
nanoid: registry.npmmirror.com/nanoid/4.0.1
|
||||
next: registry.npmmirror.com/next/13.1.6_wiv434v7erz4aedd5whhdwmpv4
|
||||
nodemailer: registry.npmmirror.com/nodemailer/6.9.1
|
||||
nprogress: registry.npmmirror.com/nprogress/0.2.0
|
||||
@@ -85,7 +86,6 @@ dependencies:
|
||||
sass: registry.npmmirror.com/sass/1.58.3
|
||||
sharp: registry.npmmirror.com/sharp/0.31.3
|
||||
tunnel: registry.npmmirror.com/tunnel/0.0.6
|
||||
uuid: registry.npmmirror.com/uuid/9.0.0
|
||||
zustand: registry.npmmirror.com/zustand/4.3.5_immer@9.0.19+react@18.2.0
|
||||
|
||||
devDependencies:
|
||||
@@ -8496,6 +8496,14 @@ packages:
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/nanoid/4.0.1:
|
||||
resolution: {integrity: sha512-udKGtCCUafD3nQtJg9wBhRP3KMbPglUsgV5JVsXhvyBs/oefqb4sqMEhKBBgqZncYowu58p1prsZQBYvAj/Gww==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/nanoid/-/nanoid-4.0.1.tgz}
|
||||
name: nanoid
|
||||
version: 4.0.1
|
||||
engines: {node: ^14 || ^16 || >=18}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/napi-build-utils/1.0.2:
|
||||
resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz}
|
||||
name: napi-build-utils
|
||||
@@ -10335,13 +10343,6 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
registry.npmmirror.com/uuid/9.0.0:
|
||||
resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/uuid/-/uuid-9.0.0.tgz}
|
||||
name: uuid
|
||||
version: 9.0.0
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/uvu/0.5.6:
|
||||
resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/uvu/-/uvu-0.5.6.tgz}
|
||||
name: uvu
|
||||
|
BIN
public/imgs/wxcode.jpg
Normal file
BIN
public/imgs/wxcode.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 320 KiB |
1
public/qrcode.min.js
vendored
Normal file
1
public/qrcode.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -55,3 +55,12 @@ export const getUserBills = (data: RequestPaging) =>
|
||||
...res,
|
||||
data: res.data.map((bill) => adaptBill(bill))
|
||||
}));
|
||||
|
||||
export const getPayCode = (amount: number) =>
|
||||
GET<{
|
||||
codeUrl: string;
|
||||
orderId: string;
|
||||
}>(`/user/getPayCode?amount=${amount}`);
|
||||
|
||||
export const checkPayResult = (orderId: string) =>
|
||||
GET<number>(`/user/checkPayResult?orderId=${orderId}`);
|
||||
|
1
src/components/Icon/icons/pay.svg
Normal file
1
src/components/Icon/icons/pay.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1679410564438" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2824" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M693.095316 281.760857l-131.632817 223.935003 103.718481 0 0 49.478312-120.846571 0 0 68.193688 120.846571 0 0 50.115659-120.846571 0 0 99.276514-62.164435 0L482.169975 673.483519 356.88022 673.483519l0-50.115659 125.289755 0 0-68.193688L356.88022 555.174172l0-49.478312 106.893053 0-130.364204-223.935003 70.099647 0c60.895822 111.230417 97.898433 181.748475 111.012698 211.562689l1.268612 0c4.441967-12.262847 16.596562-37.002611 36.474732-74.219292l74.536749-137.343396L693.095316 281.760857 693.095316 281.760857zM693.095316 281.760857" p-id="2825"></path><path d="M784.470674 621.448522c-15.061578 0-27.247797 12.187435-27.247797 27.247797s12.187435 27.247797 27.247797 27.247797l71.98128 0c-61.204765 128.843816-192.338895 217.986027-344.464118 217.986027-210.6687 0-381.478892-170.782216-381.478892-381.475243 0-210.696675 170.810191-381.465512 381.478892-381.465512 192.121175 0 350.635679 142.189179 377.137878 326.968701l55.08064 0C917.333181 242.953241 734.255278 76.493794 511.987837 76.493794 271.197197 76.493794 76.012135 271.688586 76.012135 512.456117c0 240.762665 195.185062 435.972053 435.975702 435.972053 164.236031 0 307.128238-90.894915 381.475243-225.064956l0 61.57574c0 15.061578 12.187435 27.247797 27.276989 27.247797 15.004412 0 27.247797-12.187435 27.247797-27.247797L947.987865 648.697535c0-3.297419 0-27.247797-27.247797-27.247797L784.470674 621.449738 784.470674 621.448522zM784.470674 621.448522" p-id="2826"></path></svg>
|
After Width: | Height: | Size: 1.7 KiB |
@@ -7,7 +7,8 @@ const map = {
|
||||
model: require('./icons/model.svg').default,
|
||||
share: require('./icons/share.svg').default,
|
||||
home: require('./icons/home.svg').default,
|
||||
menu: require('./icons/menu.svg').default
|
||||
menu: require('./icons/menu.svg').default,
|
||||
pay: require('./icons/pay.svg').default
|
||||
};
|
||||
|
||||
export type IconName = keyof typeof map;
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { getTokenLogin } from '@/api/user';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
@@ -19,7 +18,7 @@ const Auth = ({ children }: { children: JSX.Element }) => {
|
||||
position: 'top',
|
||||
status: 'warning'
|
||||
});
|
||||
const { userInfo, setUserInfo } = useUserStore();
|
||||
const { userInfo, initUserInfo } = useUserStore();
|
||||
const { setLoading } = useGlobalStore();
|
||||
|
||||
useQuery(
|
||||
@@ -29,15 +28,10 @@ const Auth = ({ children }: { children: JSX.Element }) => {
|
||||
return setLoading(false);
|
||||
} else {
|
||||
setLoading(true);
|
||||
return getTokenLogin();
|
||||
return initUserInfo();
|
||||
}
|
||||
},
|
||||
{
|
||||
onSuccess(user) {
|
||||
if (user) {
|
||||
setUserInfo(user);
|
||||
}
|
||||
},
|
||||
onError(error) {
|
||||
console.log('error->', error);
|
||||
router.push('/login');
|
||||
|
@@ -24,7 +24,7 @@ export const ModelList: ModelConstantsData[] = [
|
||||
trainName: 'turbo',
|
||||
maxToken: 4000,
|
||||
maxTemperature: 2,
|
||||
price: 2
|
||||
price: 5
|
||||
},
|
||||
{
|
||||
serviceCompany: 'openai',
|
||||
@@ -33,7 +33,7 @@ export const ModelList: ModelConstantsData[] = [
|
||||
trainName: 'davinci',
|
||||
maxToken: 4000,
|
||||
maxTemperature: 2,
|
||||
price: 20
|
||||
price: 50
|
||||
}
|
||||
];
|
||||
|
||||
|
@@ -39,6 +39,7 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<Script src="/iconfont.js" strategy="afterInteractive"></Script>
|
||||
<Script src="/qrcode.min.js" strategy="afterInteractive"></Script>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ChakraProvider theme={theme}>
|
||||
<ColorModeScript initialColorMode={theme.config.initialColorMode} />
|
||||
|
63
src/pages/api/user/checkPayResult.ts
Normal file
63
src/pages/api/user/checkPayResult.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import axios from 'axios';
|
||||
import { connectToDatabase, User, Pay } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { authorization } = req.headers;
|
||||
let { orderId } = req.query as { orderId: string };
|
||||
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
const { data } = await axios.get(
|
||||
`https://sif268.laf.dev/wechat-order-query?order_number=${orderId}&api_key=${process.env.WXPAYCODE}`
|
||||
);
|
||||
|
||||
if (data.trade_state === 'SUCCESS') {
|
||||
await connectToDatabase();
|
||||
|
||||
// 重复记录校验
|
||||
const count = await Pay.count({
|
||||
orderId
|
||||
});
|
||||
|
||||
if (count > 0) {
|
||||
throw new Error('订单重复,请刷新');
|
||||
}
|
||||
|
||||
// 计算实际充值。把分转成数据库的值
|
||||
const price = data.amount.total * 0.01 * 100000;
|
||||
let payId;
|
||||
try {
|
||||
// 充值记录 +1
|
||||
const payRecord = await Pay.create({
|
||||
userId,
|
||||
price,
|
||||
orderId
|
||||
});
|
||||
payId = payRecord._id;
|
||||
// 充钱
|
||||
await User.findByIdAndUpdate(userId, {
|
||||
$inc: { balance: price }
|
||||
});
|
||||
} catch (error) {
|
||||
payId && Pay.findByIdAndDelete(payId);
|
||||
}
|
||||
|
||||
jsonRes(res, {
|
||||
data: 'success'
|
||||
});
|
||||
} else {
|
||||
throw new Error(data.trade_state_desc);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
45
src/pages/api/user/getPayCode.ts
Normal file
45
src/pages/api/user/getPayCode.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import axios from 'axios';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 20);
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { authorization } = req.headers;
|
||||
let { amount = 0 } = req.query as { amount: string };
|
||||
amount = +amount;
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error('缺少登录凭证');
|
||||
}
|
||||
await authToken(authorization);
|
||||
|
||||
const id = nanoid();
|
||||
|
||||
const response = await axios({
|
||||
url: 'https://sif268.laf.dev/wechat-pay',
|
||||
method: 'POST',
|
||||
data: {
|
||||
trade_order_number: id,
|
||||
amount: amount * 100,
|
||||
api_key: process.env.WXPAYCODE
|
||||
}
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
data: {
|
||||
orderId: id,
|
||||
codeUrl: response.data?.code_url
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
@@ -32,6 +32,7 @@ import { useCopyData } from '@/utils/tools';
|
||||
import Markdown from '@/components/Markdown';
|
||||
import { shareHint } from '@/constants/common';
|
||||
import { getChatSiteId } from '@/api/chat';
|
||||
import Image from 'next/image';
|
||||
|
||||
const SlideBar = ({
|
||||
name,
|
||||
@@ -53,6 +54,7 @@ const SlideBar = ({
|
||||
const { chatHistory, removeChatHistoryByWindowId } = useChatStore();
|
||||
const [hasReady, setHasReady] = useState(false);
|
||||
const { isOpen: isOpenShare, onOpen: onOpenShare, onClose: onCloseShare } = useDisclosure();
|
||||
const { isOpen: isOpenWx, onOpen: onOpenWx, onClose: onCloseWx } = useDisclosure();
|
||||
|
||||
const { isSuccess } = useQuery(['init'], getMyModels, {
|
||||
cacheTime: 5 * 60 * 1000
|
||||
@@ -113,7 +115,13 @@ const SlideBar = ({
|
||||
</>
|
||||
);
|
||||
|
||||
const RenderButton = ({ onClick, children }: { onClick: () => void; children: JSX.Element }) => (
|
||||
const RenderButton = ({
|
||||
onClick,
|
||||
children
|
||||
}: {
|
||||
onClick: () => void;
|
||||
children: JSX.Element | string;
|
||||
}) => (
|
||||
<Box px={3} mb={3}>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
@@ -229,8 +237,17 @@ const SlideBar = ({
|
||||
分享
|
||||
</>
|
||||
</RenderButton>
|
||||
<RenderButton onClick={() => router.push('/number/setting')}>
|
||||
<>
|
||||
<MyIcon name="pay" fill={'white'} w={'16px'} h={'16px'} mr={4} />
|
||||
充值
|
||||
</>
|
||||
</RenderButton>
|
||||
|
||||
<Box textAlign={'end'} mr={4}>
|
||||
<Flex alignItems={'center'} mr={4}>
|
||||
<Box flex={1}>
|
||||
<RenderButton onClick={onOpenWx}>交流群</RenderButton>
|
||||
</Box>
|
||||
<IconButton
|
||||
icon={colorMode === 'light' ? <MoonIcon /> : <SunIcon />}
|
||||
aria-label={''}
|
||||
@@ -242,7 +259,7 @@ const SlideBar = ({
|
||||
}}
|
||||
onClick={toggleColorMode}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
{/* 分享提示modal */}
|
||||
<Modal isOpen={isOpenShare} onClose={onCloseShare}>
|
||||
@@ -287,6 +304,35 @@ const SlideBar = ({
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{/* wx 联系 */}
|
||||
<Modal isOpen={isOpenWx} onClose={onCloseWx}>
|
||||
<ModalOverlay />
|
||||
<ModalContent color={useColorModeValue('blackAlpha.700', 'white')}>
|
||||
<ModalHeader>wx交流群</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody textAlign={'center'}>
|
||||
<Image
|
||||
style={{ margin: 'auto' }}
|
||||
src={'/imgs/wxcode.jpg'}
|
||||
width={200}
|
||||
height={200}
|
||||
alt=""
|
||||
/>
|
||||
<Box mt={2}>
|
||||
微信号:{' '}
|
||||
<Box as={'span'} userSelect={'all'}>
|
||||
YNyiqi
|
||||
</Box>
|
||||
</Box>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant={'outline'} onClick={onCloseWx}>
|
||||
关闭
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import React, { Dispatch, useState, useCallback } from 'react';
|
||||
import React, { Dispatch, useState, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
@@ -12,12 +12,14 @@ import {
|
||||
Button,
|
||||
useToast,
|
||||
Input,
|
||||
Select
|
||||
Select,
|
||||
Box
|
||||
} from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { postCreateModel } from '@/api/model';
|
||||
import type { ModelSchema } from '@/types/mongoSchema';
|
||||
import { ModelList } from '@/constants/model';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
|
||||
interface CreateFormType {
|
||||
name: string;
|
||||
@@ -37,6 +39,7 @@ const CreateModel = ({
|
||||
position: 'top'
|
||||
});
|
||||
const {
|
||||
getValues,
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors }
|
||||
@@ -105,6 +108,12 @@ const CreateModel = ({
|
||||
{!!errors.serviceModelName && errors.serviceModelName.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<Box mt={3} textAlign={'center'} fontSize={'sm'} color={'blackAlpha.600'}>
|
||||
{formatPrice(
|
||||
ModelList.find((item) => item.model === getValues('serviceModelName'))?.price || 0
|
||||
) * 1000}
|
||||
元/1000字(包括上下文和标点符号)
|
||||
</Box>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
|
138
src/pages/number/components/PayModal.tsx
Normal file
138
src/pages/number/components/PayModal.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
Button,
|
||||
Input,
|
||||
Box,
|
||||
Grid
|
||||
} from '@chakra-ui/react';
|
||||
import { getPayCode, checkPayResult } from '@/api/user';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useUserStore } from '@/store/user';
|
||||
|
||||
const PayModal = ({ onClose }: { onClose: () => void }) => {
|
||||
const { toast } = useToast();
|
||||
const { initUserInfo } = useUserStore();
|
||||
const [inputVal, setInputVal] = useState<number | ''>('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [orderId, setOrderId] = useState('');
|
||||
|
||||
const handleClickPay = useCallback(async () => {
|
||||
if (!inputVal || inputVal <= 0 || isNaN(+inputVal)) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
// 获取支付二维码
|
||||
const res = await getPayCode(inputVal);
|
||||
new QRCode(document.getElementById('payQRCode'), {
|
||||
text: res.codeUrl,
|
||||
width: 128,
|
||||
height: 128,
|
||||
colorDark: '#000000',
|
||||
colorLight: '#ffffff',
|
||||
correctLevel: QRCode.CorrectLevel.H
|
||||
});
|
||||
setOrderId(res.orderId);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '出现了一些意外...',
|
||||
status: 'error'
|
||||
});
|
||||
console.log(error);
|
||||
}
|
||||
setLoading(false);
|
||||
}, [inputVal, toast]);
|
||||
|
||||
useQuery(
|
||||
[orderId],
|
||||
() => {
|
||||
if (!orderId) return null;
|
||||
return checkPayResult(orderId);
|
||||
},
|
||||
{
|
||||
refetchInterval: 2000,
|
||||
onSuccess(res) {
|
||||
if (!res) return;
|
||||
onClose();
|
||||
initUserInfo();
|
||||
toast({
|
||||
title: '充值成功',
|
||||
status: 'success'
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
isOpen={true}
|
||||
onClose={() => {
|
||||
if (orderId) return;
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>充值</ModalHeader>
|
||||
{!orderId && <ModalCloseButton />}
|
||||
|
||||
<ModalBody>
|
||||
{!orderId && (
|
||||
<>
|
||||
<Grid gridTemplateColumns={'repeat(4,1fr)'} gridGap={5} mb={4}>
|
||||
{[5, 10, 20, 50].map((item) => (
|
||||
<Button
|
||||
key={item}
|
||||
variant={item === inputVal ? 'solid' : 'outline'}
|
||||
onClick={() => setInputVal(item)}
|
||||
>
|
||||
{item}元
|
||||
</Button>
|
||||
))}
|
||||
</Grid>
|
||||
<Box>
|
||||
<Input
|
||||
value={inputVal}
|
||||
type={'number'}
|
||||
step={1}
|
||||
placeholder={'其他金额,请取整数'}
|
||||
onChange={(e) => {
|
||||
setInputVal(Math.floor(+e.target.value));
|
||||
}}
|
||||
></Input>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
{/* 付费二维码 */}
|
||||
<Box textAlign={'center'}>
|
||||
{orderId && <Box mb={3}>请微信扫码支付: {inputVal}元,请勿关闭页面</Box>}
|
||||
<Box id={'payQRCode'} display={'inline-block'}></Box>
|
||||
</Box>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
{!orderId && (
|
||||
<>
|
||||
<Button colorScheme={'gray'} onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button ml={3} isLoading={loading} onClick={handleClickPay}>
|
||||
获取充值二维码
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PayModal;
|
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Box,
|
||||
@@ -25,13 +25,18 @@ import { useUserStore } from '@/store/user';
|
||||
import { UserType } from '@/types/user';
|
||||
import { usePaging } from '@/hooks/usePaging';
|
||||
import type { UserBillType } from '@/types/user';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const PayModal = dynamic(() => import('./components/PayModal'));
|
||||
|
||||
const NumberSetting = () => {
|
||||
const { userInfo, updateUserInfo } = useUserStore();
|
||||
const { userInfo, updateUserInfo, initUserInfo } = useUserStore();
|
||||
const { setLoading } = useGlobalStore();
|
||||
const { register, handleSubmit, control } = useForm<UserUpdateParams>({
|
||||
defaultValues: userInfo as UserType
|
||||
});
|
||||
const [showPay, setShowPay] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const {
|
||||
fields: accounts,
|
||||
@@ -43,9 +48,9 @@ const NumberSetting = () => {
|
||||
});
|
||||
const { setPageNum, data: bills } = usePaging<UserBillType>({
|
||||
api: getUserBills,
|
||||
pageSize: 20
|
||||
pageSize: 30
|
||||
});
|
||||
console.log(bills);
|
||||
|
||||
const onclickSave = useCallback(
|
||||
async (data: UserUpdateParams) => {
|
||||
setLoading(true);
|
||||
@@ -62,6 +67,8 @@ const NumberSetting = () => {
|
||||
[setLoading, toast, updateUserInfo]
|
||||
);
|
||||
|
||||
useQuery(['init'], initUserInfo);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card px={6} py={4}>
|
||||
@@ -80,9 +87,9 @@ const NumberSetting = () => {
|
||||
<Box>
|
||||
<strong>{userInfo?.balance}</strong> 元
|
||||
</Box>
|
||||
{/* <Button size={'sm'} w={'80px'} ml={5}>
|
||||
<Button size={'sm'} w={'80px'} ml={5} onClick={() => setShowPay(true)}>
|
||||
充值
|
||||
</Button> */}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Card>
|
||||
@@ -181,6 +188,7 @@ const NumberSetting = () => {
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Card>
|
||||
{showPay && <PayModal onClose={() => setShowPay(false)} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -36,6 +36,6 @@ export const pushBill = async ({
|
||||
$inc: { balance: -price }
|
||||
});
|
||||
} catch (error) {
|
||||
Bill.findByIdAndDelete(billId);
|
||||
billId && Bill.findByIdAndDelete(billId);
|
||||
}
|
||||
};
|
||||
|
23
src/service/models/pay.ts
Normal file
23
src/service/models/pay.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Schema, model, models } from 'mongoose';
|
||||
|
||||
const PaySchema = new Schema({
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'user',
|
||||
required: true
|
||||
},
|
||||
time: {
|
||||
type: Number,
|
||||
default: () => Date.now()
|
||||
},
|
||||
price: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
orderId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
export const Pay = models['pay'] || model('pay', PaySchema);
|
@@ -16,7 +16,7 @@ const UserSchema = new Schema({
|
||||
},
|
||||
balance: {
|
||||
type: Number,
|
||||
default: 0
|
||||
default: 0.5
|
||||
},
|
||||
accounts: [
|
||||
{
|
||||
|
@@ -31,3 +31,4 @@ export * from './models/model';
|
||||
export * from './models/user';
|
||||
export * from './models/training';
|
||||
export * from './models/bill';
|
||||
export * from './models/pay';
|
||||
|
@@ -22,8 +22,12 @@ export const generateToken = (userId: string) => {
|
||||
};
|
||||
|
||||
/* 校验 token */
|
||||
export const authToken = (token: string): Promise<string> => {
|
||||
export const authToken = (token?: string): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!token) {
|
||||
reject('缺少登录凭证');
|
||||
return;
|
||||
}
|
||||
const key = process.env.TOKEN_KEY as string;
|
||||
|
||||
jwt.verify(token, key, function (err, decoded: any) {
|
||||
|
@@ -6,9 +6,11 @@ import type { ModelSchema } from '@/types/mongoSchema';
|
||||
import { setToken } from '@/utils/user';
|
||||
import { getMyModels } from '@/api/model';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
import { getTokenLogin } from '@/api/user';
|
||||
|
||||
type State = {
|
||||
userInfo: UserType | null;
|
||||
initUserInfo: () => Promise<null>;
|
||||
setUserInfo: (user: UserType, token?: string) => void;
|
||||
updateUserInfo: (user: UserUpdateParams) => void;
|
||||
myModels: ModelSchema[];
|
||||
@@ -20,6 +22,11 @@ export const useUserStore = create<State>()(
|
||||
devtools(
|
||||
immer((set, get) => ({
|
||||
userInfo: null,
|
||||
async initUserInfo() {
|
||||
const res = await getTokenLogin();
|
||||
get().setUserInfo(res);
|
||||
return null;
|
||||
},
|
||||
setUserInfo(user: UserType, token?: string) {
|
||||
set((state) => {
|
||||
state.userInfo = {
|
||||
|
1
src/types/index.d.ts
vendored
1
src/types/index.d.ts
vendored
@@ -2,6 +2,7 @@ import type { Mongoose } from 'mongoose';
|
||||
|
||||
declare global {
|
||||
var mongodb: Mongoose | string | null;
|
||||
var QRCode: any;
|
||||
}
|
||||
|
||||
export type PagingData<T> = {
|
||||
|
1
src/types/user.d.ts
vendored
1
src/types/user.d.ts
vendored
@@ -14,6 +14,7 @@ export interface UserType {
|
||||
}
|
||||
|
||||
export interface UserUpdateParams {
|
||||
balance?: number;
|
||||
accounts?: {
|
||||
type: string;
|
||||
value: string;
|
||||
|
Reference in New Issue
Block a user