generated from template/vite-react-template
	init
This commit is contained in:
		@@ -21,10 +21,12 @@
 | 
			
		||||
  "author": "abearxiong <xiongxiao@xiongxiao.me>",
 | 
			
		||||
  "license": "MIT",
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@ant-design/v5-patch-for-react-19": "^1.0.3",
 | 
			
		||||
    "@emotion/react": "^11.14.0",
 | 
			
		||||
    "@emotion/styled": "^11.14.0",
 | 
			
		||||
    "@kevisual/router": "0.0.10",
 | 
			
		||||
    "@mui/material": "^7.0.1",
 | 
			
		||||
    "antd": "^5.24.6",
 | 
			
		||||
    "clsx": "^2.1.1",
 | 
			
		||||
    "dayjs": "^1.11.13",
 | 
			
		||||
    "lodash-es": "^4.17.21",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										910
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										910
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1 +1,6 @@
 | 
			
		||||
@import "tailwindcss";
 | 
			
		||||
@import '@kevisual/components/theme/wind-theme.css';
 | 
			
		||||
 | 
			
		||||
.ant-modal {
 | 
			
		||||
  z-index: 10;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								src/main.tsx
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								src/main.tsx
									
									
									
									
									
								
							@@ -1,10 +1,18 @@
 | 
			
		||||
import { createRoot } from 'react-dom/client';
 | 
			
		||||
import { App, AppRoute } from './pages/App.tsx';
 | 
			
		||||
import { CustomThemeProvider } from '@kevisual/components/theme/index.tsx';
 | 
			
		||||
 | 
			
		||||
console.log('cu',)
 | 
			
		||||
import { ConfigProvider } from 'antd';
 | 
			
		||||
import { ToastContainer } from 'react-toastify';
 | 
			
		||||
// import 'react-toastify/dist/ReactToastify.css';
 | 
			
		||||
createRoot(document.getElementById('root')!).render(
 | 
			
		||||
  <CustomThemeProvider>
 | 
			
		||||
  <ConfigProvider
 | 
			
		||||
    theme={{
 | 
			
		||||
      components: {
 | 
			
		||||
        Modal: {
 | 
			
		||||
          // zIndex: 1000,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    }}>
 | 
			
		||||
    <AppRoute />
 | 
			
		||||
  </CustomThemeProvider>,
 | 
			
		||||
    <ToastContainer />
 | 
			
		||||
  </ConfigProvider>,
 | 
			
		||||
);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,9 @@
 | 
			
		||||
import { basename } from '../modules/basename';
 | 
			
		||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
 | 
			
		||||
console.log('basename', basename);
 | 
			
		||||
import { App as AppDemo } from './app-demo';
 | 
			
		||||
import { App as AppVip } from './vip';
 | 
			
		||||
import '@ant-design/v5-patch-for-react-19';
 | 
			
		||||
 | 
			
		||||
export const App = () => {
 | 
			
		||||
  return <div className='bg-slate-200 w-full h-full border'>123</div>;
 | 
			
		||||
};
 | 
			
		||||
@@ -10,8 +12,7 @@ export const AppRoute = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Router>
 | 
			
		||||
      <Routes>
 | 
			
		||||
        <Route path='/' element={<Navigate to='/app-demo/list' />} />
 | 
			
		||||
        <Route path='/app-demo/*' element={<AppDemo />} />
 | 
			
		||||
        <Route path='*' element={<AppVip />} />
 | 
			
		||||
      </Routes>
 | 
			
		||||
    </Router>
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										66
									
								
								src/pages/vip/constants.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/pages/vip/constants.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,66 @@
 | 
			
		||||
export const vipFeatureList = [
 | 
			
		||||
  {
 | 
			
		||||
    level: 'free',
 | 
			
		||||
    levelNumber: 0,
 | 
			
		||||
    title: '免费',
 | 
			
		||||
    description: '满足简单的部署应用需求',
 | 
			
		||||
    features: [
 | 
			
		||||
      {
 | 
			
		||||
        title: '部署10个以内的前端应用',
 | 
			
		||||
        description: '可以部署10个以内的应用,包括网页应用、小程序、H5等',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        title: '资源存储不超过50MB',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        title: '应用下载',
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    level: 'love',
 | 
			
		||||
    levelNumber: 1,
 | 
			
		||||
    title: '支持会员',
 | 
			
		||||
    description: '支持一下',
 | 
			
		||||
    features: [
 | 
			
		||||
      {
 | 
			
		||||
        title: '部署30个以内的前端应用',
 | 
			
		||||
        description: '可以部署30个以内的应用,包括网页应用、小程序、H5等',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        title: '资源存储不超过200MB',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        title: '专属支持',
 | 
			
		||||
        description: '专属支持,包括专属客服、专属技术支持等',
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    level: 'vip',
 | 
			
		||||
    levelNumber: 5,
 | 
			
		||||
    title: '高级会员',
 | 
			
		||||
    description: '应用定制和专属支持',
 | 
			
		||||
    features: [
 | 
			
		||||
      {
 | 
			
		||||
        title: '无限部署应用',
 | 
			
		||||
        description: '可以部署无限个应用,包括网页应用、小程序、H5等',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        title: '资源存储不超过500MB',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        title: '优先支持',
 | 
			
		||||
        description: '专属支持,包括专属客服、专属技术支持等',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        title: '专属域名',
 | 
			
		||||
        description: '可以申请专属域名,并使用专属域名进行访问',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        title: '私有化部署',
 | 
			
		||||
        description: '可以私有化部署,代理运维。',
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
							
								
								
									
										12
									
								
								src/pages/vip/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/pages/vip/index.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
import { Routes, Route } from 'react-router';
 | 
			
		||||
import { List } from './pages/List';
 | 
			
		||||
import { VipInfo } from './pages/VipInfo';
 | 
			
		||||
 | 
			
		||||
export const App = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Routes>
 | 
			
		||||
      <Route index element={<VipInfo />} />
 | 
			
		||||
      <Route path='/list' element={<List />} />
 | 
			
		||||
    </Routes>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										239
									
								
								src/pages/vip/pages/List.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										239
									
								
								src/pages/vip/pages/List.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,239 @@
 | 
			
		||||
import { useEffect } from 'react';
 | 
			
		||||
import { useDemoStore } from '../store';
 | 
			
		||||
import { useShallow } from 'zustand/react/shallow';
 | 
			
		||||
import { Modal, Space } from 'antd';
 | 
			
		||||
import { vipLevel, VipCategory } from '../query.ts';
 | 
			
		||||
import dayjs from 'dayjs';
 | 
			
		||||
import { Table, Button, Select, DatePicker, Input, Form } from 'antd';
 | 
			
		||||
import { ColumnType } from 'antd/es/table/interface';
 | 
			
		||||
 | 
			
		||||
const defaultValues = {
 | 
			
		||||
  title: '',
 | 
			
		||||
  userId: '',
 | 
			
		||||
  level: 'free',
 | 
			
		||||
  category: 'center',
 | 
			
		||||
  startDate: dayjs(),
 | 
			
		||||
  endDate: dayjs().add(1, 'month'),
 | 
			
		||||
};
 | 
			
		||||
export const EditDialog = () => {
 | 
			
		||||
  const [form] = Form.useForm();
 | 
			
		||||
  const store = useDemoStore(
 | 
			
		||||
    useShallow((state) => ({
 | 
			
		||||
      formData: state.formData,
 | 
			
		||||
      setFormData: state.setFormData,
 | 
			
		||||
      showEdit: state.showEdit,
 | 
			
		||||
      setShowEdit: state.setShowEdit,
 | 
			
		||||
      updateData: state.updateData,
 | 
			
		||||
    })),
 | 
			
		||||
  );
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (store.showEdit) {
 | 
			
		||||
      if (store.formData) {
 | 
			
		||||
        form.setFieldsValue({
 | 
			
		||||
          ...store.formData,
 | 
			
		||||
          startDate: dayjs(store.formData.startDate),
 | 
			
		||||
          endDate: dayjs(store.formData.endDate),
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        form.setFieldsValue(defaultValues);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return () => {
 | 
			
		||||
      form.setFieldsValue(defaultValues);
 | 
			
		||||
    };
 | 
			
		||||
  }, [store.formData, store.showEdit]);
 | 
			
		||||
  const onSubmit = async (data: any) => {
 | 
			
		||||
    // 将dayjs对象转换为时间字符串
 | 
			
		||||
    const formattedData = {
 | 
			
		||||
      ...data,
 | 
			
		||||
      id: store.formData?.id,
 | 
			
		||||
      startDate: data.startDate ? data.startDate.format('YYYY-MM-DD') : undefined,
 | 
			
		||||
      endDate: data.endDate ? data.endDate.format('YYYY-MM-DD') : undefined,
 | 
			
		||||
    };
 | 
			
		||||
    const res = await store.updateData(formattedData, { refresh: true });
 | 
			
		||||
    if (res.code === 200) {
 | 
			
		||||
      store.setShowEdit(false);
 | 
			
		||||
      store.setFormData(undefined);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  const onCancel = () => {
 | 
			
		||||
    store.setShowEdit(false);
 | 
			
		||||
    store.setFormData(undefined);
 | 
			
		||||
  };
 | 
			
		||||
  const hasId = !!store.formData?.id;
 | 
			
		||||
  return (
 | 
			
		||||
    <Modal
 | 
			
		||||
      title={hasId ? '编辑' : '添加'}
 | 
			
		||||
      footer={null}
 | 
			
		||||
      open={store.showEdit}
 | 
			
		||||
      onCancel={onCancel}
 | 
			
		||||
      style={{
 | 
			
		||||
        top: 40,
 | 
			
		||||
      }}>
 | 
			
		||||
      <Form
 | 
			
		||||
        form={form}
 | 
			
		||||
        labelCol={{
 | 
			
		||||
          span: 6,
 | 
			
		||||
        }}
 | 
			
		||||
        wrapperCol={{
 | 
			
		||||
          span: 18,
 | 
			
		||||
        }}
 | 
			
		||||
        className='flex flex-col gap-4 pt-4 min-w-[400px]'
 | 
			
		||||
        onFinish={onSubmit}>
 | 
			
		||||
        <Form.Item name='id' hidden>
 | 
			
		||||
          <Input />
 | 
			
		||||
        </Form.Item>
 | 
			
		||||
        <Form.Item name='title' label='标题' rules={[{ required: true, message: '请输入标题' }]}>
 | 
			
		||||
          <Input placeholder='请输入标题' />
 | 
			
		||||
        </Form.Item>
 | 
			
		||||
        <Form.Item name='userId' label='用户ID' rules={[{ required: true, message: '请输入用户ID' }]}>
 | 
			
		||||
          <Input placeholder='请输入用户ID' />
 | 
			
		||||
        </Form.Item>
 | 
			
		||||
        <Form.Item name='level' label='等级' rules={[{ required: true, message: '请选择等级' }]}>
 | 
			
		||||
          <Select options={vipLevel} />
 | 
			
		||||
        </Form.Item>
 | 
			
		||||
        <Form.Item name='category' label='分类' rules={[{ required: true, message: '请选择分类' }]}>
 | 
			
		||||
          <Select options={VipCategory} />
 | 
			
		||||
        </Form.Item>
 | 
			
		||||
        <Form.Item name='startDate' label='开始日期' rules={[{ required: true, message: '请选择开始日期' }]}>
 | 
			
		||||
          <DatePicker format={'YYYY-MM-DD'} type='date' />
 | 
			
		||||
        </Form.Item>
 | 
			
		||||
        <Form.Item name='endDate' label='结束日期' rules={[{ required: true, message: '请选择结束日期' }]}>
 | 
			
		||||
          <DatePicker format={'YYYY-MM-DD'} type='date' />
 | 
			
		||||
        </Form.Item>
 | 
			
		||||
        <Form.Item label=' ' colon={false}>
 | 
			
		||||
          <Button htmlType='submit'>提交</Button>
 | 
			
		||||
        </Form.Item>
 | 
			
		||||
      </Form>
 | 
			
		||||
    </Modal>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const List = () => {
 | 
			
		||||
  const store = useDemoStore(
 | 
			
		||||
    useShallow((state) => ({
 | 
			
		||||
      list: state.list,
 | 
			
		||||
      pagination: state.pagination,
 | 
			
		||||
      init: state.init,
 | 
			
		||||
      setShowEdit: state.setShowEdit,
 | 
			
		||||
      deleteData: state.deleteData,
 | 
			
		||||
      setFormData: state.setFormData,
 | 
			
		||||
      getList: state.getList,
 | 
			
		||||
    })),
 | 
			
		||||
  );
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    store.init();
 | 
			
		||||
  }, []);
 | 
			
		||||
  const columns: ColumnType<any>[] = [
 | 
			
		||||
    {
 | 
			
		||||
      title: '标题',
 | 
			
		||||
      dataIndex: 'title',
 | 
			
		||||
      align: 'center',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '用户ID',
 | 
			
		||||
      dataIndex: 'userId',
 | 
			
		||||
      align: 'center',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '等级',
 | 
			
		||||
      dataIndex: 'level',
 | 
			
		||||
      align: 'center',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '分类',
 | 
			
		||||
      dataIndex: 'category',
 | 
			
		||||
      align: 'center',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '开始日期',
 | 
			
		||||
      dataIndex: 'startDate',
 | 
			
		||||
      align: 'center',
 | 
			
		||||
      render: (_, record) => {
 | 
			
		||||
        return record.startDate ? dayjs(record.startDate).format('YYYY-MM-DD') : '';
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '结束日期',
 | 
			
		||||
      dataIndex: 'endDate',
 | 
			
		||||
      align: 'center',
 | 
			
		||||
      render: (_, record) => {
 | 
			
		||||
        return record.endDate ? dayjs(record.endDate).format('YYYY-MM-DD') : '';
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '操作',
 | 
			
		||||
      dataIndex: 'action',
 | 
			
		||||
      align: 'center',
 | 
			
		||||
      render: (_, record) => (
 | 
			
		||||
        <Space>
 | 
			
		||||
          <Button
 | 
			
		||||
            type='link'
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              store.setShowEdit(true);
 | 
			
		||||
              store.setFormData(record);
 | 
			
		||||
            }}>
 | 
			
		||||
            编辑
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button type='link' onClick={() => store.deleteData(record.id)}>
 | 
			
		||||
            删除
 | 
			
		||||
          </Button>
 | 
			
		||||
        </Space>
 | 
			
		||||
      ),
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
  const [formSearch] = Form.useForm();
 | 
			
		||||
  const onSearch = (data: any) => {
 | 
			
		||||
    console.log('onSearch', data);
 | 
			
		||||
    store.getList(data);
 | 
			
		||||
  };
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='w-full h-full flex flex-col gap-2 bg-gray-100 p-4'>
 | 
			
		||||
      <div
 | 
			
		||||
        className='overflow-auto scrollbar relative'
 | 
			
		||||
        style={{
 | 
			
		||||
          height: 'calc(100% - 50px)',
 | 
			
		||||
          overflow: 'auto',
 | 
			
		||||
        }}>
 | 
			
		||||
        <Form form={formSearch} onFinish={onSearch} layout='inline' className='sticky top-0 left-0 z-10 flex gap-2 flex-wrap'>
 | 
			
		||||
          <Button type='primary' onClick={() => store.setShowEdit(true)}>
 | 
			
		||||
            添加
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Form.Item className='w-[200px]' name='search' label='标题'>
 | 
			
		||||
            <Input />
 | 
			
		||||
          </Form.Item>
 | 
			
		||||
          <Form.Item className='w-[200px]' name='level' label='等级'>
 | 
			
		||||
            <Select options={vipLevel} />
 | 
			
		||||
          </Form.Item>
 | 
			
		||||
          <Form.Item className='w-[22s0px]' name='userId' label='用户ID'>
 | 
			
		||||
            <Input />
 | 
			
		||||
          </Form.Item>
 | 
			
		||||
          <Form.Item className='w-[200px]' name='category' label='分类'>
 | 
			
		||||
            <Select options={VipCategory} />
 | 
			
		||||
          </Form.Item>
 | 
			
		||||
          <Form.Item>
 | 
			
		||||
            <Button type='primary' htmlType='submit'>
 | 
			
		||||
              搜索
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Form.Item>
 | 
			
		||||
        </Form>
 | 
			
		||||
        <Table
 | 
			
		||||
          className='mt-2'
 | 
			
		||||
          columns={columns}
 | 
			
		||||
          rowKey={(record) => record.id}
 | 
			
		||||
          dataSource={store.list}
 | 
			
		||||
          pagination={{
 | 
			
		||||
            pageSize: store.pagination.pageSize,
 | 
			
		||||
            total: store.pagination.total,
 | 
			
		||||
            current: store.pagination.page,
 | 
			
		||||
            onChange: (page, pageSize) => {
 | 
			
		||||
              store.getList({ page, pageSize });
 | 
			
		||||
              console.log('onChange', page, pageSize);
 | 
			
		||||
            },
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
      <EditDialog />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										161
									
								
								src/pages/vip/pages/VipInfo.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								src/pages/vip/pages/VipInfo.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,161 @@
 | 
			
		||||
import { useEffect } 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';
 | 
			
		||||
interface VipStore {
 | 
			
		||||
  vipList: any[];
 | 
			
		||||
  setVipList: (vipList: any[]) => void;
 | 
			
		||||
  init: () => Promise<void>;
 | 
			
		||||
  vipInfo?: {
 | 
			
		||||
    level: string;
 | 
			
		||||
    startDate: string;
 | 
			
		||||
    endDate: string;
 | 
			
		||||
    category: string;
 | 
			
		||||
  };
 | 
			
		||||
  setVipInfo: (vipInfo: any) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const useVipStore = create<VipStore>((set) => ({
 | 
			
		||||
  vipList: [],
 | 
			
		||||
  setVipList: (vipList: any[]) => set({ vipList }),
 | 
			
		||||
  vipInfo: {
 | 
			
		||||
    level: '',
 | 
			
		||||
    startDate: '',
 | 
			
		||||
    endDate: '',
 | 
			
		||||
    category: '',
 | 
			
		||||
  },
 | 
			
		||||
  setVipInfo: (vipInfo: any) => set({ vipInfo }),
 | 
			
		||||
  init: async () => {
 | 
			
		||||
    const res = await queryApi.getMeVipList();
 | 
			
		||||
    if (res.code === 200) {
 | 
			
		||||
      const list = res.data.list || [];
 | 
			
		||||
      set({ vipList: list });
 | 
			
		||||
      const vipCenterInfo = list.find((item: any) => item.category === 'center');
 | 
			
		||||
      set({ vipInfo: vipCenterInfo });
 | 
			
		||||
    } else {
 | 
			
		||||
      toast.error(res.message || '获取VIP列表失败');
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
export const VipInfo = () => {
 | 
			
		||||
  const store = useVipStore();
 | 
			
		||||
  const payModalStore = usePayModalStore(
 | 
			
		||||
    useShallow((state) => {
 | 
			
		||||
      return { setOpen: state.setOpen, setMoney: state.setMoney };
 | 
			
		||||
    }),
 | 
			
		||||
  );
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    store.init();
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const vipPlans = [
 | 
			
		||||
    {
 | 
			
		||||
      type: '普通方案',
 | 
			
		||||
      level: 'free',
 | 
			
		||||
      background: 'bg-white',
 | 
			
		||||
      textColor: 'text-gray-800',
 | 
			
		||||
      description: '满足简单的部署应用需求',
 | 
			
		||||
      price: '免费',
 | 
			
		||||
      buttonText: '进入部署中心',
 | 
			
		||||
      buttonClass: 'bg-white border border-blue-500 text-blue-500 hover:bg-blue-50',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      type: '支持会员',
 | 
			
		||||
      background: 'bg-blue-50',
 | 
			
		||||
      level: 'love',
 | 
			
		||||
      textColor: 'text-gray-800',
 | 
			
		||||
      description: '可以部署更多应用和更多增值服务',
 | 
			
		||||
      price: '1.0',
 | 
			
		||||
      buttonText: '立即开通',
 | 
			
		||||
      buttonClass: 'bg-blue-500 text-white hover:bg-blue-600',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      type: '高级会员',
 | 
			
		||||
      background: 'bg-gray-800',
 | 
			
		||||
      level: 'vip',
 | 
			
		||||
      textColor: 'text-amber-200',
 | 
			
		||||
      description: '应用定制和专属支持',
 | 
			
		||||
      price: '5.0',
 | 
			
		||||
      buttonText: '立即开通',
 | 
			
		||||
      buttonClass: 'bg-amber-100 text-gray-800 hover:bg-amber-200',
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
  const onBuyVip = (clickLevel: string) => {
 | 
			
		||||
    const currentLevel = store.vipInfo?.level || 'free';
 | 
			
		||||
    const currentLevelNumber = vipFeatureList.find((item) => item.level === currentLevel)?.levelNumber || 0;
 | 
			
		||||
    const clickLevelNumber = vipFeatureList.find((item) => item.level === clickLevel)?.levelNumber || 0;
 | 
			
		||||
 | 
			
		||||
    if (clickLevel === 'free') {
 | 
			
		||||
      window.location.href = '/root/center/';
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (clickLevelNumber <= currentLevelNumber) {
 | 
			
		||||
      toast.info('您已经是该会员等级');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    // 打开支付弹窗
 | 
			
		||||
    payModalStore.setOpen(true);
 | 
			
		||||
    payModalStore.setMoney(clickLevelNumber);
 | 
			
		||||
    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 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'>
 | 
			
		||||
          <h1 className='text-4xl md:text-5xl font-bold mb-4'>一切为了快速运行</h1>
 | 
			
		||||
          <p className='text-xl md:text-2xl font-light opacity-90'>方便部署轻量级的网页app应用,并可以随时随地访问</p>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <h1 className='text-3xl font-bold mb-16 text-gray-800'>功能权益对比</h1>
 | 
			
		||||
 | 
			
		||||
      <div className='flex flex-wrap justify-center gap-6 w-full max-w-6xl'>
 | 
			
		||||
        {vipPlans.map((plan, index) => {
 | 
			
		||||
          const vipFeatures = vipFeatureList.find((item) => item.level === plan.level);
 | 
			
		||||
          const planIsVip = plan.level === 'vip';
 | 
			
		||||
          const isActive = plan.level === store.vipInfo?.level;
 | 
			
		||||
          let planButtonText = plan.buttonText;
 | 
			
		||||
          if (isActive && plan.level !== 'free') {
 | 
			
		||||
            planButtonText = '已开通';
 | 
			
		||||
          }
 | 
			
		||||
          if (!isActive && plan.level === 'vip' && store.vipInfo?.level === 'love') {
 | 
			
		||||
            planButtonText = '升级';
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return (
 | 
			
		||||
            <div key={index} className={`${plan.background} rounded-lg p-8 flex flex-col items-center w-full max-w-xs`}>
 | 
			
		||||
              <h2 className={`text-3xl font-bold mb-4 ${plan.textColor}`}>{plan.level}</h2>
 | 
			
		||||
              <p className={`text-center mb-8 ${plan.textColor}`}>{plan.description}</p>
 | 
			
		||||
 | 
			
		||||
              <div className={`flex items-end mb-8 ${plan.textColor}`}>
 | 
			
		||||
                <span className='text-xl'>低至</span>
 | 
			
		||||
                <span className='text-6xl font-semibold mx-2'>{plan.price}</span>
 | 
			
		||||
                {plan.price !== '免费' && <span className='text-xl'>元/月</span>}
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
              <button
 | 
			
		||||
                className={`py-3 px-6 w-full rounded-md font-medium cursor-pointer transition-colors ${plan.buttonClass}`}
 | 
			
		||||
                onClick={() => onBuyVip(plan.level)}>
 | 
			
		||||
                {planButtonText}
 | 
			
		||||
              </button>
 | 
			
		||||
              <div className='flex flex-col items-start py-4'>
 | 
			
		||||
                {vipFeatures?.features.map((feature, index) => (
 | 
			
		||||
                  <div key={index} className='flex items-center mb-2'>
 | 
			
		||||
                    <span className={`${planIsVip ? 'text-white' : 'text-gray-600'} mr-2`}>{index + 1}.</span>
 | 
			
		||||
                    <span className={`${planIsVip ? 'text-white' : 'text-gray-800'}`}>{feature.title}</span>
 | 
			
		||||
                  </div>
 | 
			
		||||
                ))}
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          );
 | 
			
		||||
        })}
 | 
			
		||||
      </div>
 | 
			
		||||
      <PayModal />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										31
									
								
								src/pages/vip/pay-modal/PayModal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/pages/vip/pay-modal/PayModal.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
import { Modal } from 'antd';
 | 
			
		||||
import { create } from 'zustand';
 | 
			
		||||
 | 
			
		||||
interface PayModalStore {
 | 
			
		||||
  open: boolean;
 | 
			
		||||
  setOpen: (open: boolean) => void;
 | 
			
		||||
  money: number;
 | 
			
		||||
  setMoney: (money: number) => void;
 | 
			
		||||
}
 | 
			
		||||
export const usePayModalStore = create<PayModalStore>((set) => ({
 | 
			
		||||
  open: false,
 | 
			
		||||
  setOpen: (open: boolean) => set({ open }),
 | 
			
		||||
  money: 0,
 | 
			
		||||
  setMoney: (money: number) => set({ money }),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
export const PayModal = () => {
 | 
			
		||||
  const { open, setOpen, money } = usePayModalStore();
 | 
			
		||||
  return (
 | 
			
		||||
    <Modal open={open} onCancel={() => setOpen(false)} maskClosable={false} footer={null}>
 | 
			
		||||
      <div className='px-4 py-4 flex flex-col gap-4 select-none'>
 | 
			
		||||
        <h2 className='text-2xl font-bold mb-4'>支付确认</h2>
 | 
			
		||||
        <p className='text-lg'>请确认支付金额:{money} 元</p>
 | 
			
		||||
        <div className='flex gap-2'>
 | 
			
		||||
          <button className='bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 cursor-pointer'>微信支付</button>
 | 
			
		||||
          <button className='bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 cursor-pointer'>支付宝支付</button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </Modal>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										101
									
								
								src/pages/vip/query.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								src/pages/vip/query.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,101 @@
 | 
			
		||||
import { BaseQuery } from '@kevisual/query';
 | 
			
		||||
 | 
			
		||||
export const vipLevel = [
 | 
			
		||||
  {
 | 
			
		||||
    label: '免费会员',
 | 
			
		||||
    value: 'free',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    label: '普通会员',
 | 
			
		||||
    value: 'love',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    label: 'VIP会员',
 | 
			
		||||
    value: 'vip',
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
export const VipCategory = [
 | 
			
		||||
  {
 | 
			
		||||
    label: 'Deploy Center',
 | 
			
		||||
    value: 'center',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    label: 'AI Chat',
 | 
			
		||||
    value: 'chat',
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
export class QueryApi extends BaseQuery {
 | 
			
		||||
  constructor(options: { query: any }) {
 | 
			
		||||
    super(options);
 | 
			
		||||
  }
 | 
			
		||||
  async getList(params?: any, dataOpts?: any) {
 | 
			
		||||
    return this.query.post(
 | 
			
		||||
      {
 | 
			
		||||
        path: 'vip',
 | 
			
		||||
        key: 'list',
 | 
			
		||||
        ...params,
 | 
			
		||||
      },
 | 
			
		||||
      dataOpts,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  async getDetail(id?: string, dataOpts?: any) {
 | 
			
		||||
    return this.query.post(
 | 
			
		||||
      {
 | 
			
		||||
        path: 'vip',
 | 
			
		||||
        key: 'get',
 | 
			
		||||
        data: { id },
 | 
			
		||||
      },
 | 
			
		||||
      dataOpts,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  async update(data?: any, dataOpts?: any) {
 | 
			
		||||
    return this.query.post(
 | 
			
		||||
      {
 | 
			
		||||
        path: 'vip',
 | 
			
		||||
        key: 'update',
 | 
			
		||||
        data,
 | 
			
		||||
      },
 | 
			
		||||
      dataOpts,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  async delete(id?: string, dataOpts?: any) {
 | 
			
		||||
    return this.query.post(
 | 
			
		||||
      {
 | 
			
		||||
        path: 'vip',
 | 
			
		||||
        key: 'delete',
 | 
			
		||||
        data: { id },
 | 
			
		||||
      },
 | 
			
		||||
      dataOpts,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  /**
 | 
			
		||||
   * 获取我的VIP列表
 | 
			
		||||
   * @param dataOpts 数据选项
 | 
			
		||||
   * @returns
 | 
			
		||||
   */
 | 
			
		||||
  async getMeVipList(dataOpts?: any) {
 | 
			
		||||
    return this.query.post(
 | 
			
		||||
      {
 | 
			
		||||
        path: 'vip',
 | 
			
		||||
        key: 'me-vip-list',
 | 
			
		||||
      },
 | 
			
		||||
      dataOpts,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  /**
 | 
			
		||||
   * 获取我的VIP信息
 | 
			
		||||
   * @param category 分类
 | 
			
		||||
   * @param dataOpts 数据选项
 | 
			
		||||
   * @returns
 | 
			
		||||
   */
 | 
			
		||||
  async getMeVipInfo(category?: string, dataOpts?: any) {
 | 
			
		||||
    return this.query.post(
 | 
			
		||||
      {
 | 
			
		||||
        path: 'vip',
 | 
			
		||||
        key: 'me',
 | 
			
		||||
        category: category || 'center',
 | 
			
		||||
      },
 | 
			
		||||
      dataOpts,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										108
									
								
								src/pages/vip/store.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								src/pages/vip/store.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,108 @@
 | 
			
		||||
import { create } from 'zustand';
 | 
			
		||||
import { query } from '@/modules/query';
 | 
			
		||||
import { QueryApi } from './query';
 | 
			
		||||
import { toast } from 'react-toastify';
 | 
			
		||||
 | 
			
		||||
export const queryApi = new QueryApi({ query });
 | 
			
		||||
type Store = {
 | 
			
		||||
  list: any[];
 | 
			
		||||
  setList: (list: any[]) => void;
 | 
			
		||||
  pagination: {
 | 
			
		||||
    page: number;
 | 
			
		||||
    pageSize: number;
 | 
			
		||||
    total: number;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  setPagination: (pagination: { page: number; pageSize: number; total: number }) => void;
 | 
			
		||||
  data: any;
 | 
			
		||||
  setData: (data: any) => void;
 | 
			
		||||
  loading: boolean;
 | 
			
		||||
  setLoading: (loading: boolean) => void;
 | 
			
		||||
  formData: any;
 | 
			
		||||
  setFormData: (data: any) => void;
 | 
			
		||||
  showEdit: boolean;
 | 
			
		||||
  setShowEdit: (showEdit: boolean) => void;
 | 
			
		||||
  getList: (params?: { page?: number; pageSize?: number; category?: string; level?: string }) => Promise<any>;
 | 
			
		||||
  init: () => Promise<void>;
 | 
			
		||||
  getData: (id: string) => Promise<any>;
 | 
			
		||||
  updateData: (data: any, opts?: { refresh?: boolean }) => Promise<any>;
 | 
			
		||||
  deleteData: (id: string, opts?: { refresh?: boolean }) => Promise<any>;
 | 
			
		||||
};
 | 
			
		||||
export const useDemoStore = create<Store>((set, get) => ({
 | 
			
		||||
  list: [],
 | 
			
		||||
  setList: (list) => set({ list }),
 | 
			
		||||
  pagination: {
 | 
			
		||||
    page: 1,
 | 
			
		||||
    pageSize: 2,
 | 
			
		||||
    total: 0,
 | 
			
		||||
  },
 | 
			
		||||
  setPagination: (pagination) => set({ pagination }),
 | 
			
		||||
  data: null,
 | 
			
		||||
  setData: (data) => set({ data }),
 | 
			
		||||
  loading: false,
 | 
			
		||||
  setLoading: (loading) => set({ loading }),
 | 
			
		||||
  formData: null,
 | 
			
		||||
  setFormData: (formData) => set({ formData }),
 | 
			
		||||
  showEdit: false,
 | 
			
		||||
  setShowEdit: (showEdit) => set({ showEdit }),
 | 
			
		||||
  getList: async (params?: any) => {
 | 
			
		||||
    set({ loading: true });
 | 
			
		||||
    let { page, pageSize, ...rest } = params || {};
 | 
			
		||||
    const res = await queryApi.getList({
 | 
			
		||||
      page: page || 1,
 | 
			
		||||
      pageSize: pageSize || 10,
 | 
			
		||||
      ...rest,
 | 
			
		||||
    });
 | 
			
		||||
    set({ loading: false });
 | 
			
		||||
    if (res.code === 200) {
 | 
			
		||||
      set({
 | 
			
		||||
        list: res.data.list,
 | 
			
		||||
        pagination: res.data.pagination,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    return res;
 | 
			
		||||
  },
 | 
			
		||||
  init: async () => {
 | 
			
		||||
    await get().getList();
 | 
			
		||||
  },
 | 
			
		||||
  getData: async (id) => {
 | 
			
		||||
    set({ loading: true });
 | 
			
		||||
    const res = await queryApi.getDetail(id);
 | 
			
		||||
    set({ loading: false });
 | 
			
		||||
    if (res.code === 200) {
 | 
			
		||||
      const data = res.data;
 | 
			
		||||
      set({ data });
 | 
			
		||||
    }
 | 
			
		||||
    return res;
 | 
			
		||||
  },
 | 
			
		||||
  updateData: async (data, opts = { refresh: true }) => {
 | 
			
		||||
    set({ loading: true });
 | 
			
		||||
    const res = await queryApi.update(data);
 | 
			
		||||
    set({ loading: false });
 | 
			
		||||
    if (res.code === 200) {
 | 
			
		||||
      set({ data: res.data });
 | 
			
		||||
      toast.success('更新成功');
 | 
			
		||||
    } else {
 | 
			
		||||
      toast.error(res.message || '更新失败');
 | 
			
		||||
    }
 | 
			
		||||
    if (opts.refresh) {
 | 
			
		||||
      await get().getList();
 | 
			
		||||
    }
 | 
			
		||||
    return res;
 | 
			
		||||
  },
 | 
			
		||||
  deleteData: async (id, opts = { refresh: true }) => {
 | 
			
		||||
    set({ loading: true });
 | 
			
		||||
    const res = await queryApi.delete(id);
 | 
			
		||||
    set({ loading: false });
 | 
			
		||||
    if (res.code === 200) {
 | 
			
		||||
      set({ data: null });
 | 
			
		||||
      toast.success('删除成功');
 | 
			
		||||
    } else {
 | 
			
		||||
      toast.error(res.message || '删除失败');
 | 
			
		||||
    }
 | 
			
		||||
    if (opts.refresh) {
 | 
			
		||||
      await get().getList();
 | 
			
		||||
    }
 | 
			
		||||
    return res;
 | 
			
		||||
  },
 | 
			
		||||
}));
 | 
			
		||||
@@ -27,19 +27,20 @@ if (isDev) {
 | 
			
		||||
} else {
 | 
			
		||||
  target = 'https://kevisual.cn';
 | 
			
		||||
}
 | 
			
		||||
target = 'http://localhost:4006';
 | 
			
		||||
 | 
			
		||||
let proxy = {
 | 
			
		||||
  '/root/center/': {
 | 
			
		||||
    target: `https://${target}/root/center/`,
 | 
			
		||||
    target: `${target}/root/center/`,
 | 
			
		||||
  },
 | 
			
		||||
  '/root/system-lib/': {
 | 
			
		||||
    target: `https://${target}/root/system-lib/`,
 | 
			
		||||
    target: `${target}/root/system-lib/`,
 | 
			
		||||
  },
 | 
			
		||||
  '/user/login/': {
 | 
			
		||||
    target: `https://${target}/user/login/`,
 | 
			
		||||
    target: `${target}/user/login/`,
 | 
			
		||||
  },
 | 
			
		||||
  '/api': {
 | 
			
		||||
    target: `https://${target}`,
 | 
			
		||||
    target: `${target}`,
 | 
			
		||||
    changeOrigin: true,
 | 
			
		||||
    ws: true,
 | 
			
		||||
    rewriteWsOrigin: true,
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user