temp: add pay-cneter vip info

This commit is contained in:
熊潇 2025-04-10 00:31:15 +08:00
parent 5f59158021
commit fe41c01f6a
7 changed files with 162 additions and 8 deletions

View File

@ -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
View File

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

View 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>
);
};

View File

@ -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} />;
}, },
}, },
{ {

View File

@ -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 ( return (
<div className='w-full flex flex-col items-center py-10 px-4 bg-gray-50 h-full scrollbar'> <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 (
<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>
); );
}; };

View File

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

View File

@ -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,
);
}
} }