generated from template/vite-react-template
init
This commit is contained in:
parent
8e41c38dde
commit
4a9727dd71
@ -21,10 +21,12 @@
|
|||||||
"author": "abearxiong <xiongxiao@xiongxiao.me>",
|
"author": "abearxiong <xiongxiao@xiongxiao.me>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@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/router": "0.0.10",
|
"@kevisual/router": "0.0.10",
|
||||||
"@mui/material": "^7.0.1",
|
"@mui/material": "^7.0.1",
|
||||||
|
"antd": "^5.24.6",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"lodash-es": "^4.17.21",
|
"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 "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 { createRoot } from 'react-dom/client';
|
||||||
import { App, AppRoute } from './pages/App.tsx';
|
import { App, AppRoute } from './pages/App.tsx';
|
||||||
import { CustomThemeProvider } from '@kevisual/components/theme/index.tsx';
|
import { ConfigProvider } from 'antd';
|
||||||
|
import { ToastContainer } from 'react-toastify';
|
||||||
console.log('cu',)
|
// import 'react-toastify/dist/ReactToastify.css';
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<CustomThemeProvider>
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
components: {
|
||||||
|
Modal: {
|
||||||
|
// zIndex: 1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}>
|
||||||
<AppRoute />
|
<AppRoute />
|
||||||
</CustomThemeProvider>,
|
<ToastContainer />
|
||||||
|
</ConfigProvider>,
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { basename } from '../modules/basename';
|
import { basename } from '../modules/basename';
|
||||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
console.log('basename', basename);
|
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 = () => {
|
export const App = () => {
|
||||||
return <div className='bg-slate-200 w-full h-full border'>123</div>;
|
return <div className='bg-slate-200 w-full h-full border'>123</div>;
|
||||||
};
|
};
|
||||||
@ -10,8 +12,7 @@ export const AppRoute = () => {
|
|||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path='/' element={<Navigate to='/app-demo/list' />} />
|
<Route path='*' element={<AppVip />} />
|
||||||
<Route path='/app-demo/*' element={<AppDemo />} />
|
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</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 {
|
} else {
|
||||||
target = 'https://kevisual.cn';
|
target = 'https://kevisual.cn';
|
||||||
}
|
}
|
||||||
|
target = 'http://localhost:4006';
|
||||||
|
|
||||||
let proxy = {
|
let proxy = {
|
||||||
'/root/center/': {
|
'/root/center/': {
|
||||||
target: `https://${target}/root/center/`,
|
target: `${target}/root/center/`,
|
||||||
},
|
},
|
||||||
'/root/system-lib/': {
|
'/root/system-lib/': {
|
||||||
target: `https://${target}/root/system-lib/`,
|
target: `${target}/root/system-lib/`,
|
||||||
},
|
},
|
||||||
'/user/login/': {
|
'/user/login/': {
|
||||||
target: `https://${target}/user/login/`,
|
target: `${target}/user/login/`,
|
||||||
},
|
},
|
||||||
'/api': {
|
'/api': {
|
||||||
target: `https://${target}`,
|
target: `${target}`,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
ws: true,
|
ws: true,
|
||||||
rewriteWsOrigin: true,
|
rewriteWsOrigin: true,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user