generated from template/vite-react-template
temp: add pay-cneter vip info
This commit is contained in:
parent
5f59158021
commit
fe41c01f6a
@ -24,6 +24,7 @@
|
|||||||
"@ant-design/v5-patch-for-react-19": "^1.0.3",
|
"@ant-design/v5-patch-for-react-19": "^1.0.3",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
|
"@kevisual/load": "^0.0.6",
|
||||||
"@kevisual/router": "0.0.10",
|
"@kevisual/router": "0.0.10",
|
||||||
"@mui/material": "^7.0.1",
|
"@mui/material": "^7.0.1",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
|
15
pnpm-lock.yaml
generated
15
pnpm-lock.yaml
generated
@ -17,6 +17,9 @@ importers:
|
|||||||
'@emotion/styled':
|
'@emotion/styled':
|
||||||
specifier: ^11.14.0
|
specifier: ^11.14.0
|
||||||
version: 11.14.0(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.1.0))(@types/react@19.1.0)(react@19.1.0)
|
version: 11.14.0(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.1.0))(@types/react@19.1.0)(react@19.1.0)
|
||||||
|
'@kevisual/load':
|
||||||
|
specifier: ^0.0.6
|
||||||
|
version: 0.0.6
|
||||||
'@kevisual/router':
|
'@kevisual/router':
|
||||||
specifier: 0.0.10
|
specifier: 0.0.10
|
||||||
version: 0.0.10
|
version: 0.0.10
|
||||||
@ -531,6 +534,9 @@ packages:
|
|||||||
'@kevisual/cache@0.0.1':
|
'@kevisual/cache@0.0.1':
|
||||||
resolution: {integrity: sha512-yjQJ47NdE3smtJahA3UMcEEBU86uI3V93WnQZHTgFP1S1L8iD0Abct1cFWkuPIlsow8uBxbn4z4iN58KrsQlpA==}
|
resolution: {integrity: sha512-yjQJ47NdE3smtJahA3UMcEEBU86uI3V93WnQZHTgFP1S1L8iD0Abct1cFWkuPIlsow8uBxbn4z4iN58KrsQlpA==}
|
||||||
|
|
||||||
|
'@kevisual/load@0.0.6':
|
||||||
|
resolution: {integrity: sha512-+3YTFehRcZ1haGel5DKYMUwmi5i6f2psyaPZlfkKU/cOXgkpwoG9/BEqPCnPjicKqqnksEpixVRkyHJ+5bjLVA==}
|
||||||
|
|
||||||
'@kevisual/query-login@0.0.4':
|
'@kevisual/query-login@0.0.4':
|
||||||
resolution: {integrity: sha512-ibdSkMsoWYYvM9l5YqWbxVvNb+uTqLyfeS0wJqLumPyYFx3mSwFweI+isbtJQqpP/G3CywsXYrrbZbelSw124Q==}
|
resolution: {integrity: sha512-ibdSkMsoWYYvM9l5YqWbxVvNb+uTqLyfeS0wJqLumPyYFx3mSwFweI+isbtJQqpP/G3CywsXYrrbZbelSw124Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -1158,6 +1164,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
|
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
eventemitter3@5.0.1:
|
||||||
|
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
|
||||||
|
|
||||||
fdir@6.4.3:
|
fdir@6.4.3:
|
||||||
resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==}
|
resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -2450,6 +2459,10 @@ snapshots:
|
|||||||
- tslib
|
- tslib
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
|
'@kevisual/load@0.0.6':
|
||||||
|
dependencies:
|
||||||
|
eventemitter3: 5.0.1
|
||||||
|
|
||||||
'@kevisual/query-login@0.0.4(@kevisual/query@0.0.17(ws@8.18.1))(rollup@4.34.8)(typescript@5.8.3)':
|
'@kevisual/query-login@0.0.4(@kevisual/query@0.0.17(ws@8.18.1))(rollup@4.34.8)(typescript@5.8.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@kevisual/cache': 0.0.1(rollup@4.34.8)(typescript@5.8.3)
|
'@kevisual/cache': 0.0.1(rollup@4.34.8)(typescript@5.8.3)
|
||||||
@ -3103,6 +3116,8 @@ snapshots:
|
|||||||
|
|
||||||
event-target-shim@5.0.1: {}
|
event-target-shim@5.0.1: {}
|
||||||
|
|
||||||
|
eventemitter3@5.0.1: {}
|
||||||
|
|
||||||
fdir@6.4.3(picomatch@4.0.2):
|
fdir@6.4.3(picomatch@4.0.2):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
picomatch: 4.0.2
|
picomatch: 4.0.2
|
||||||
|
17
src/modules/layouts/index.tsx
Normal file
17
src/modules/layouts/index.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
import { toastLogin } from '@kevisual/components/toast/ToastLogin.tsx';
|
||||||
|
export const Layout = () => {
|
||||||
|
useEffect(() => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (!token) {
|
||||||
|
toastLogin();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full h-full'>
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -6,6 +6,28 @@ import dayjs from 'dayjs';
|
|||||||
import { Table, Button, Select, DatePicker, Input, Form } from 'antd';
|
import { Table, Button, Select, DatePicker, Input, Form } from 'antd';
|
||||||
import { ColumnType } from 'antd/es/table/interface';
|
import { ColumnType } from 'antd/es/table/interface';
|
||||||
|
|
||||||
|
type VipTargetProps = {
|
||||||
|
target: {
|
||||||
|
type: string; // vip,
|
||||||
|
level: string; // free,
|
||||||
|
month: number;
|
||||||
|
updateTime: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const VipTarget = ({ target }: VipTargetProps) => {
|
||||||
|
if (!target) return null;
|
||||||
|
|
||||||
|
if (target?.type === 'vip') {
|
||||||
|
return (
|
||||||
|
<div className='w-[200px] flex flex-col text-left'>
|
||||||
|
<div>level: {target?.level}</div>
|
||||||
|
<div>month: {target.month}</div>
|
||||||
|
<div>updateTime: {dayjs(target.updateTime).format('YYYY-MM-DD HH:mm:ss')}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <div>free</div>;
|
||||||
|
};
|
||||||
const defaultValues = {
|
const defaultValues = {
|
||||||
title: '',
|
title: '',
|
||||||
userId: '',
|
userId: '',
|
||||||
@ -165,7 +187,8 @@ export const List = () => {
|
|||||||
align: 'center',
|
align: 'center',
|
||||||
render: (_, record) => {
|
render: (_, record) => {
|
||||||
const target = record.data?.target;
|
const target = record.data?.target;
|
||||||
return JSON.stringify(target);
|
// return JSON.stringify(target);
|
||||||
|
return <VipTarget target={target} />;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import { queryApi } from '../store';
|
import { queryApi } from '../store';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { vipFeatureList } from '../constants';
|
import { vipFeatureList } from '../constants';
|
||||||
import { usePayModalStore, PayModal } from '../pay-modal/PayModal';
|
import { usePayModalStore, PayModal } from '../pay-modal/PayModal';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
import { Tooltip } from 'antd';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
interface VipStore {
|
interface VipStore {
|
||||||
vipList: any[];
|
vipList: any[];
|
||||||
setVipList: (vipList: any[]) => void;
|
setVipList: (vipList: any[]) => void;
|
||||||
@ -46,13 +48,12 @@ export const VipInfo = () => {
|
|||||||
const store = useVipStore();
|
const store = useVipStore();
|
||||||
const payModalStore = usePayModalStore(
|
const payModalStore = usePayModalStore(
|
||||||
useShallow((state) => {
|
useShallow((state) => {
|
||||||
return { setOpen: state.setOpen, setMoney: state.setMoney, setLevel: state.setLevel };
|
return { open: state.open, setOpen: state.setOpen, setMoney: state.setMoney, setLevel: state.setLevel };
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
store.init();
|
store.init();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const vipPlans = [
|
const vipPlans = [
|
||||||
{
|
{
|
||||||
type: '普通方案',
|
type: '普通方案',
|
||||||
@ -98,7 +99,7 @@ export const VipInfo = () => {
|
|||||||
toast.info('您当前会员等级为' + currentLevel + ',无法降级, 会直接覆盖。需要修改请联系管理员。');
|
toast.info('您当前会员等级为' + currentLevel + ',无法降级, 会直接覆盖。需要修改请联系管理员。');
|
||||||
} else if (clickLevelNumber === currentLevelNumber) {
|
} else if (clickLevelNumber === currentLevelNumber) {
|
||||||
} else if (clickLevelNumber > currentLevelNumber) {
|
} else if (clickLevelNumber > currentLevelNumber) {
|
||||||
toast.info('您当前会员等级为' + currentLevel + ',会直接覆盖原有会员等级。并重新计算会员时间。');
|
currentLevelNumber != 0 && toast.info('您当前会员等级为' + currentLevel + ',会直接覆盖原有会员等级。并重新计算会员时间。');
|
||||||
}
|
}
|
||||||
// 打开支付弹窗
|
// 打开支付弹窗
|
||||||
payModalStore.setOpen(true);
|
payModalStore.setOpen(true);
|
||||||
@ -106,8 +107,33 @@ export const VipInfo = () => {
|
|||||||
payModalStore.setLevel(clickLevel);
|
payModalStore.setLevel(clickLevel);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
const expiredCom = useMemo(() => {
|
||||||
|
const isVip = store.vipInfo && store.vipInfo?.level !== 'free';
|
||||||
|
if (!isVip) return null;
|
||||||
|
const expiredTime = dayjs(store.vipInfo?.endDate);
|
||||||
|
const now = dayjs();
|
||||||
|
const diff = expiredTime.diff(now, 'day');
|
||||||
|
if (diff > 0) {
|
||||||
|
return (
|
||||||
|
<Tooltip title={dayjs(store.vipInfo?.endDate).format('YYYY-MM-DD')}>
|
||||||
|
<div className=''>{diff}天</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <div className=''>已过期</div>;
|
||||||
|
}, [store.vipInfo?.level, store.vipInfo?.endDate]);
|
||||||
return (
|
return (
|
||||||
<div className='w-full flex flex-col items-center py-10 px-4 bg-gray-50 h-full scrollbar'>
|
<div className='w-full flex flex-col items-center pb-10 px-4 bg-gray-50 h-full scrollbar '>
|
||||||
|
<div className='w-full max-w-6xl flex justify-end mb-4 mt-4 gap-2'>
|
||||||
|
<div className='flex items-center gap-2 text-gray-800'>
|
||||||
|
<div className='text-gray-800'>过期时间:</div>
|
||||||
|
<div>{expiredCom}</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<span className='text-gray-800'>当前会员等级:</span>
|
||||||
|
<span className='text-blue-500'>{store.vipInfo?.level}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/* 封面部分 */}
|
{/* 封面部分 */}
|
||||||
<div className='w-full max-w-6xl bg-gradient-to-r from-blue-600 to-indigo-800 rounded-xl p-10 mb-16 text-white shadow-lg'>
|
<div className='w-full max-w-6xl bg-gradient-to-r from-blue-600 to-indigo-800 rounded-xl p-10 mb-16 text-white shadow-lg'>
|
||||||
<div className='max-w-3xl'>
|
<div className='max-w-3xl'>
|
||||||
@ -159,7 +185,7 @@ export const VipInfo = () => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<PayModal />
|
{payModalStore.open && <PayModal />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -4,6 +4,8 @@ import { generateQRCode } from '../../../uitls/qrcode';
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { queryApi } from '../store';
|
import { queryApi } from '../store';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
import { useVipStore } from '../pages/VipInfo';
|
||||||
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
|
||||||
interface PayModalStore {
|
interface PayModalStore {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -24,13 +26,69 @@ export const usePayModalStore = create<PayModalStore>((set) => ({
|
|||||||
|
|
||||||
export const PayModal = () => {
|
export const PayModal = () => {
|
||||||
const { open, setOpen, money, level } = usePayModalStore();
|
const { open, setOpen, money, level } = usePayModalStore();
|
||||||
|
const store = useVipStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
init: state.init,
|
||||||
|
})),
|
||||||
|
);
|
||||||
const [wxQrCode, setWxQrCode] = useState('');
|
const [wxQrCode, setWxQrCode] = useState('');
|
||||||
const [wxIframeURL, setWxIframeURL] = useState('');
|
const [wxIframeURL, setWxIframeURL] = useState('');
|
||||||
const [alipayIframeURL, setAlipayIframeURL] = useState('');
|
const [alipayIframeURL, setAlipayIframeURL] = useState('');
|
||||||
const [payMode, setPayMode] = useState<'wx' | 'alipay' | 'unset'>('unset');
|
const [payMode, setPayMode] = useState<'wx' | 'alipay' | 'unset'>('unset');
|
||||||
|
const [outTradeNo, setOutTradeNo] = useState('');
|
||||||
|
const [payStatus, setPayStatus] = useState<'wait-pay' | 'success' | 'fail'>('wait-pay');
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initQrCode(wxIframeURL);
|
initQrCode(wxIframeURL);
|
||||||
}, [wxIframeURL]);
|
}, [wxIframeURL]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (payStatus !== 'wait-pay' || !outTradeNo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let time = 0;
|
||||||
|
let load;
|
||||||
|
|
||||||
|
const checkPayStatus = async () => {
|
||||||
|
if (time > 60 * 3) {
|
||||||
|
setPayStatus('fail');
|
||||||
|
toast.dismiss(load);
|
||||||
|
toast.error('支付超时,请刷新后重试');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await queryApi.vipCheckPayStatus({ out_trade_no: outTradeNo });
|
||||||
|
time++;
|
||||||
|
if (res.code === 200) {
|
||||||
|
const status = res.data.status;
|
||||||
|
if (status === 'TRADE_SUCCESS') {
|
||||||
|
setPayStatus('success');
|
||||||
|
toast.dismiss(load);
|
||||||
|
toast.success('支付成功');
|
||||||
|
setTimeout(() => {
|
||||||
|
store.init();
|
||||||
|
}, 1000);
|
||||||
|
setOpen(false);
|
||||||
|
return;
|
||||||
|
} else if (status === 'TRADE_CLOSED') {
|
||||||
|
setPayStatus('fail');
|
||||||
|
setOpen(false);
|
||||||
|
toast.dismiss(load);
|
||||||
|
toast.error('支付失败, 请刷新后重试');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
checkPayStatus();
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
let timer = setTimeout(() => {
|
||||||
|
checkPayStatus();
|
||||||
|
load = toast.loading('查询支付状态...');
|
||||||
|
}, 10000);
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
toast.dismiss(load);
|
||||||
|
};
|
||||||
|
}, [payStatus, outTradeNo]);
|
||||||
const initQrCode = async (url: string) => {
|
const initQrCode = async (url: string) => {
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
const wxQrCode = await generateQRCode(url);
|
const wxQrCode = await generateQRCode(url);
|
||||||
@ -41,8 +99,12 @@ export const PayModal = () => {
|
|||||||
level,
|
level,
|
||||||
money: money * 100,
|
money: money * 100,
|
||||||
payMode,
|
payMode,
|
||||||
|
out_trade_no: outTradeNo,
|
||||||
});
|
});
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
|
const outTradeNo = res.data.out_trade_no;
|
||||||
|
setOutTradeNo(outTradeNo);
|
||||||
|
setPayStatus('wait-pay');
|
||||||
if (payMode === 'wx') {
|
if (payMode === 'wx') {
|
||||||
const form = res.data.url;
|
const form = res.data.url;
|
||||||
const wxQrCode = await generateQRCode(form);
|
const wxQrCode = await generateQRCode(form);
|
||||||
@ -57,7 +119,7 @@ export const PayModal = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={open} onCancel={() => setOpen(false)} maskClosable={false} footer={null} style={{}}>
|
<Modal open={open} onCancel={() => setOpen(false)} maskClosable={false} footer={null} style={{}} destroyOnClose>
|
||||||
<div className='px-4 py-4 flex flex-col gap-4 select-none justify-center items-center'>
|
<div className='px-4 py-4 flex flex-col gap-4 select-none justify-center items-center'>
|
||||||
<h2 className='text-2xl font-bold mb-4'>支付确认</h2>
|
<h2 className='text-2xl font-bold mb-4'>支付确认</h2>
|
||||||
<p className='text-lg'>请确认支付金额:{money} 元</p>
|
<p className='text-lg'>请确认支付金额:{money} 元</p>
|
||||||
|
@ -117,4 +117,14 @@ export class QueryApi extends BaseQuery {
|
|||||||
dataOpts,
|
dataOpts,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
async vipCheckPayStatus(data?: { out_trade_no: string }, dataOpts?: any) {
|
||||||
|
return this.query.post(
|
||||||
|
{
|
||||||
|
path: 'vip',
|
||||||
|
key: 'check-pay-status',
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
dataOpts,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user