generated from template/vite-react-template
	temp: add pay-cneter vip info
This commit is contained in:
		@@ -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",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										15
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							@@ -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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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 { 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 = {
 | 
			
		||||
  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 <VipTarget target={target} />;
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -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 (
 | 
			
		||||
    <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='max-w-3xl'>
 | 
			
		||||
@@ -159,7 +185,7 @@ export const VipInfo = () => {
 | 
			
		||||
          );
 | 
			
		||||
        })}
 | 
			
		||||
      </div>
 | 
			
		||||
      <PayModal />
 | 
			
		||||
      {payModalStore.open && <PayModal />}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -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<PayModalStore>((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 (
 | 
			
		||||
    <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'>
 | 
			
		||||
        <h2 className='text-2xl font-bold mb-4'>支付确认</h2>
 | 
			
		||||
        <p className='text-lg'>请确认支付金额:{money} 元</p>
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user