From fe41c01f6a6e9138aa436473ac9bbd2aa85c4ee7 Mon Sep 17 00:00:00 2001 From: xion Date: Thu, 10 Apr 2025 00:31:15 +0800 Subject: [PATCH] temp: add pay-cneter vip info --- package.json | 1 + pnpm-lock.yaml | 15 +++++++ src/modules/layouts/index.tsx | 17 ++++++++ src/pages/trade/pages/List.tsx | 25 ++++++++++- src/pages/vip/pages/VipInfo.tsx | 38 ++++++++++++++--- src/pages/vip/pay-modal/PayModal.tsx | 64 +++++++++++++++++++++++++++- src/pages/vip/query.ts | 10 +++++ 7 files changed, 162 insertions(+), 8 deletions(-) create mode 100644 src/modules/layouts/index.tsx 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 ( -
+
+
+
+
过期时间:
+
{expiredCom}
+
+
+ 当前会员等级: + {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, + ); + } }