diff --git a/package.json b/package.json
index 1a943fc..1b36ec1 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
"@ant-design/v5-patch-for-react-19": "^1.0.3",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
+ "@kevisual/load": "^0.0.6",
"@kevisual/router": "0.0.10",
"@mui/material": "^7.0.1",
"@types/qrcode": "^1.5.5",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1464ed8..aadb4de 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -17,6 +17,9 @@ importers:
'@emotion/styled':
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)
+ '@kevisual/load':
+ specifier: ^0.0.6
+ version: 0.0.6
'@kevisual/router':
specifier: 0.0.10
version: 0.0.10
@@ -531,6 +534,9 @@ packages:
'@kevisual/cache@0.0.1':
resolution: {integrity: sha512-yjQJ47NdE3smtJahA3UMcEEBU86uI3V93WnQZHTgFP1S1L8iD0Abct1cFWkuPIlsow8uBxbn4z4iN58KrsQlpA==}
+ '@kevisual/load@0.0.6':
+ resolution: {integrity: sha512-+3YTFehRcZ1haGel5DKYMUwmi5i6f2psyaPZlfkKU/cOXgkpwoG9/BEqPCnPjicKqqnksEpixVRkyHJ+5bjLVA==}
+
'@kevisual/query-login@0.0.4':
resolution: {integrity: sha512-ibdSkMsoWYYvM9l5YqWbxVvNb+uTqLyfeS0wJqLumPyYFx3mSwFweI+isbtJQqpP/G3CywsXYrrbZbelSw124Q==}
peerDependencies:
@@ -1158,6 +1164,9 @@ packages:
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
engines: {node: '>=6'}
+ eventemitter3@5.0.1:
+ resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
+
fdir@6.4.3:
resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==}
peerDependencies:
@@ -2450,6 +2459,10 @@ snapshots:
- tslib
- 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)':
dependencies:
'@kevisual/cache': 0.0.1(rollup@4.34.8)(typescript@5.8.3)
@@ -3103,6 +3116,8 @@ snapshots:
event-target-shim@5.0.1: {}
+ eventemitter3@5.0.1: {}
+
fdir@6.4.3(picomatch@4.0.2):
optionalDependencies:
picomatch: 4.0.2
diff --git a/src/modules/layouts/index.tsx b/src/modules/layouts/index.tsx
new file mode 100644
index 0000000..39c42da
--- /dev/null
+++ b/src/modules/layouts/index.tsx
@@ -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 (
+
+
+
+ );
+};
diff --git a/src/pages/trade/pages/List.tsx b/src/pages/trade/pages/List.tsx
index 70295b4..3bd0e0a 100644
--- a/src/pages/trade/pages/List.tsx
+++ b/src/pages/trade/pages/List.tsx
@@ -6,6 +6,28 @@ import dayjs from 'dayjs';
import { Table, Button, Select, DatePicker, Input, Form } from 'antd';
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 (
+
+
level: {target?.level}
+
month: {target.month}
+
updateTime: {dayjs(target.updateTime).format('YYYY-MM-DD HH:mm:ss')}
+
+ );
+ }
+ return free
;
+};
const defaultValues = {
title: '',
userId: '',
@@ -165,7 +187,8 @@ export const List = () => {
align: 'center',
render: (_, record) => {
const target = record.data?.target;
- return JSON.stringify(target);
+ // return JSON.stringify(target);
+ return ;
},
},
{
diff --git a/src/pages/vip/pages/VipInfo.tsx b/src/pages/vip/pages/VipInfo.tsx
index c07de9e..37c51dc 100644
--- a/src/pages/vip/pages/VipInfo.tsx
+++ b/src/pages/vip/pages/VipInfo.tsx
@@ -1,10 +1,12 @@
-import { useEffect } from 'react';
+import { useEffect, useMemo } from 'react';
import { queryApi } from '../store';
import { create } from 'zustand';
import { toast } from 'react-toastify';
import { vipFeatureList } from '../constants';
import { usePayModalStore, PayModal } from '../pay-modal/PayModal';
import { useShallow } from 'zustand/react/shallow';
+import { Tooltip } from 'antd';
+import dayjs from 'dayjs';
interface VipStore {
vipList: any[];
setVipList: (vipList: any[]) => void;
@@ -46,13 +48,12 @@ export const VipInfo = () => {
const store = useVipStore();
const payModalStore = usePayModalStore(
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(() => {
store.init();
}, []);
-
const vipPlans = [
{
type: '普通方案',
@@ -98,7 +99,7 @@ export const VipInfo = () => {
toast.info('您当前会员等级为' + currentLevel + ',无法降级, 会直接覆盖。需要修改请联系管理员。');
} else if (clickLevelNumber === currentLevelNumber) {
} else if (clickLevelNumber > currentLevelNumber) {
- toast.info('您当前会员等级为' + currentLevel + ',会直接覆盖原有会员等级。并重新计算会员时间。');
+ currentLevelNumber != 0 && toast.info('您当前会员等级为' + currentLevel + ',会直接覆盖原有会员等级。并重新计算会员时间。');
}
// 打开支付弹窗
payModalStore.setOpen(true);
@@ -106,8 +107,33 @@ export const VipInfo = () => {
payModalStore.setLevel(clickLevel);
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 (
+
+ {diff}天
+
+ );
+ }
+ return 已过期
;
+ }, [store.vipInfo?.level, store.vipInfo?.endDate]);
return (
-
+
+
+
+
+ 当前会员等级:
+ {store.vipInfo?.level}
+
+
{/* 封面部分 */}
@@ -159,7 +185,7 @@ export const VipInfo = () => {
);
})}
-
+ {payModalStore.open &&
}
);
};
diff --git a/src/pages/vip/pay-modal/PayModal.tsx b/src/pages/vip/pay-modal/PayModal.tsx
index a7818dc..0e68e9f 100644
--- a/src/pages/vip/pay-modal/PayModal.tsx
+++ b/src/pages/vip/pay-modal/PayModal.tsx
@@ -4,6 +4,8 @@ import { generateQRCode } from '../../../uitls/qrcode';
import { useEffect, useState } from 'react';
import { queryApi } from '../store';
import { toast } from 'react-toastify';
+import { useVipStore } from '../pages/VipInfo';
+import { useShallow } from 'zustand/react/shallow';
interface PayModalStore {
open: boolean;
@@ -24,13 +26,69 @@ export const usePayModalStore = create
((set) => ({
export const PayModal = () => {
const { open, setOpen, money, level } = usePayModalStore();
+ const store = useVipStore(
+ useShallow((state) => ({
+ init: state.init,
+ })),
+ );
const [wxQrCode, setWxQrCode] = useState('');
const [wxIframeURL, setWxIframeURL] = useState('');
const [alipayIframeURL, setAlipayIframeURL] = useState('');
const [payMode, setPayMode] = useState<'wx' | 'alipay' | 'unset'>('unset');
+ const [outTradeNo, setOutTradeNo] = useState('');
+ const [payStatus, setPayStatus] = useState<'wait-pay' | 'success' | 'fail'>('wait-pay');
useEffect(() => {
initQrCode(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) => {
if (!url) return;
const wxQrCode = await generateQRCode(url);
@@ -41,8 +99,12 @@ export const PayModal = () => {
level,
money: money * 100,
payMode,
+ out_trade_no: outTradeNo,
});
if (res.code === 200) {
+ const outTradeNo = res.data.out_trade_no;
+ setOutTradeNo(outTradeNo);
+ setPayStatus('wait-pay');
if (payMode === 'wx') {
const form = res.data.url;
const wxQrCode = await generateQRCode(form);
@@ -57,7 +119,7 @@ export const PayModal = () => {
};
return (
- setOpen(false)} maskClosable={false} footer={null} style={{}}>
+ setOpen(false)} maskClosable={false} footer={null} style={{}} destroyOnClose>
支付确认
请确认支付金额:{money} 元
diff --git a/src/pages/vip/query.ts b/src/pages/vip/query.ts
index 6a4518d..4bbde59 100644
--- a/src/pages/vip/query.ts
+++ b/src/pages/vip/query.ts
@@ -117,4 +117,14 @@ export class QueryApi extends BaseQuery {
dataOpts,
);
}
+ async vipCheckPayStatus(data?: { out_trade_no: string }, dataOpts?: any) {
+ return this.query.post(
+ {
+ path: 'vip',
+ key: 'check-pay-status',
+ data,
+ },
+ dataOpts,
+ );
+ }
}