generated from template/vite-react-template
Compare commits
4 Commits
8e41c38dde
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| fe41c01f6a | |||
| 5f59158021 | |||
| 8bd517cfcb | |||
| 4a9727dd71 |
11
package.json
11
package.json
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "vite-react",
|
||||
"name": "vip",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"basename": "/",
|
||||
"basename": "/root/vip",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build:css": "tailwindcss -i ./src/index.css -o ./dist/render.css --minify",
|
||||
"postbuild2": "pnpm build:css",
|
||||
"preview": "vite preview",
|
||||
"pub": "envision deploy ./dist -k vite-react -v 0.0.1",
|
||||
"pub": "envision deploy ./dist -k vip -v 0.0.1 -u",
|
||||
"dev:lib": "turbo dev",
|
||||
"git:submodule": "git submodule update --init --recursive",
|
||||
"cmd": "tsx ./script/index.ts "
|
||||
@@ -21,15 +21,20 @@
|
||||
"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/load": "^0.0.6",
|
||||
"@kevisual/router": "0.0.10",
|
||||
"@mui/material": "^7.0.1",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"antd": "^5.24.6",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucide-react": "^0.487.0",
|
||||
"nanoid": "^5.1.5",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-hook-form": "^7.55.0",
|
||||
|
||||
1151
pnpm-lock.yaml
generated
1151
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
1
src/constant.ts
Normal file
1
src/constant.ts
Normal file
@@ -0,0 +1 @@
|
||||
import { slashedBasename } from './modules/basename';
|
||||
@@ -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 +1,2 @@
|
||||
export const basename = DEV_SERVER ? '/' : BASE_NAME;
|
||||
export const slashedBasename = basename ? `${basename}/` : '/';
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,17 +1,20 @@
|
||||
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 { App as AppTrade } from './trade';
|
||||
import '@ant-design/v5-patch-for-react-19';
|
||||
|
||||
export const App = () => {
|
||||
return <div className='bg-slate-200 w-full h-full border'>123</div>;
|
||||
};
|
||||
|
||||
export const AppRoute = () => {
|
||||
return (
|
||||
<Router>
|
||||
<Router basename={basename}>
|
||||
<Routes>
|
||||
<Route path='/' element={<Navigate to='/app-demo/list' />} />
|
||||
<Route path='/app-demo/*' element={<AppDemo />} />
|
||||
<Route path='/trade' element={<AppTrade />} />
|
||||
<Route path='*' element={<AppVip />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
);
|
||||
|
||||
10
src/pages/trade/index.tsx
Normal file
10
src/pages/trade/index.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Routes, Route } from 'react-router';
|
||||
import { List } from './pages/List';
|
||||
|
||||
export const App = () => {
|
||||
return (
|
||||
<Routes>
|
||||
<Route index element={<List />} />
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
269
src/pages/trade/pages/List.tsx
Normal file
269
src/pages/trade/pages/List.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useTradeStore } from '../store';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { Modal, Space } from 'antd';
|
||||
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: '',
|
||||
level: 'free',
|
||||
category: 'center',
|
||||
startDate: dayjs(),
|
||||
endDate: dayjs().add(1, 'month'),
|
||||
};
|
||||
export const EditDialog = () => {
|
||||
const [form] = Form.useForm();
|
||||
const store = useTradeStore(
|
||||
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 label=' ' colon={false}>
|
||||
<Button htmlType='submit'>提交</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export const List = () => {
|
||||
const store = useTradeStore(
|
||||
useShallow((state) => ({
|
||||
list: state.list,
|
||||
pagination: state.pagination,
|
||||
init: state.init,
|
||||
setShowEdit: state.setShowEdit,
|
||||
deleteData: state.deleteData,
|
||||
setFormData: state.setFormData,
|
||||
userList: state.userList,
|
||||
getList: state.getList,
|
||||
})),
|
||||
);
|
||||
useEffect(() => {
|
||||
store.init();
|
||||
}, []);
|
||||
const columns: ColumnType<any>[] = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: 'out_trade_no',
|
||||
dataIndex: 'out_trade_no',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: 'Subject',
|
||||
dataIndex: 'subject',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '金额',
|
||||
dataIndex: 'money',
|
||||
align: 'center',
|
||||
render: (_, record) => {
|
||||
try {
|
||||
return (record.money / 100).toFixed(2);
|
||||
} catch (error) {
|
||||
return record.money;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '支付方式',
|
||||
dataIndex: 'type',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: 'uid',
|
||||
dataIndex: 'uid',
|
||||
align: 'center',
|
||||
render: (_, record) => {
|
||||
const user = store.userList?.find?.((item) => item.id === record.uid);
|
||||
return user?.username || record.uid;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '数据',
|
||||
dataIndex: 'data',
|
||||
align: 'center',
|
||||
render: (_, record) => {
|
||||
const target = record.data?.target;
|
||||
// return JSON.stringify(target);
|
||||
return <VipTarget target={target} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
align: 'center',
|
||||
render: (_, record) => {
|
||||
return record.createdAt ? dayjs(record.createdAt).format('YYYY-MM-DD HH:mm:ss') : '';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
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>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
56
src/pages/trade/query.ts
Normal file
56
src/pages/trade/query.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { BaseQuery } from '@kevisual/query';
|
||||
|
||||
export class TradeQuery extends BaseQuery {
|
||||
constructor(options: { query: any }) {
|
||||
super(options);
|
||||
}
|
||||
async getList(params?: any, dataOpts?: any) {
|
||||
return this.query.post(
|
||||
{
|
||||
path: 'trade',
|
||||
key: 'list',
|
||||
...params,
|
||||
},
|
||||
dataOpts,
|
||||
);
|
||||
}
|
||||
async getDetail(id?: string, dataOpts?: any) {
|
||||
return this.query.post(
|
||||
{
|
||||
path: 'trade',
|
||||
key: 'get',
|
||||
data: { id },
|
||||
},
|
||||
dataOpts,
|
||||
);
|
||||
}
|
||||
async update(data?: any, dataOpts?: any) {
|
||||
return this.query.post(
|
||||
{
|
||||
path: 'trade',
|
||||
key: 'update',
|
||||
data,
|
||||
},
|
||||
dataOpts,
|
||||
);
|
||||
}
|
||||
async delete(id?: string, dataOpts?: any) {
|
||||
return this.query.post(
|
||||
{
|
||||
path: 'trade',
|
||||
key: 'delete',
|
||||
data: { id },
|
||||
},
|
||||
dataOpts,
|
||||
);
|
||||
}
|
||||
async getMe(dataOpts?: any) {
|
||||
return this.query.post<{ id: string }>(
|
||||
{
|
||||
path: 'user',
|
||||
key: 'me',
|
||||
},
|
||||
dataOpts,
|
||||
);
|
||||
}
|
||||
}
|
||||
111
src/pages/trade/store.ts
Normal file
111
src/pages/trade/store.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { create } from 'zustand';
|
||||
import { query } from '@/modules/query';
|
||||
import { TradeQuery } from './query';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
export const queryApi = new TradeQuery({ query });
|
||||
type Store = {
|
||||
list: any[];
|
||||
setList: (list: any[]) => void;
|
||||
pagination: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
};
|
||||
|
||||
userList: any[];
|
||||
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 useTradeStore = create<Store>((set, get) => ({
|
||||
list: [],
|
||||
setList: (list) => set({ list }),
|
||||
userList: [],
|
||||
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,
|
||||
userList: res.data.userList || [],
|
||||
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;
|
||||
},
|
||||
}));
|
||||
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: '部署20个以内的前端应用',
|
||||
description: '可以部署10个以内的应用,包括网页应用、小程序、H5等',
|
||||
},
|
||||
{
|
||||
title: '资源存储不超过50MB',
|
||||
},
|
||||
{
|
||||
title: '应用下载',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
level: 'love',
|
||||
levelNumber: 1,
|
||||
title: '支持会员',
|
||||
description: '支持一下',
|
||||
features: [
|
||||
{
|
||||
title: '部署50个以内的前端应用',
|
||||
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>
|
||||
);
|
||||
};
|
||||
191
src/pages/vip/pages/VipInfo.tsx
Normal file
191
src/pages/vip/pages/VipInfo.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
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;
|
||||
init: () => Promise<void>;
|
||||
vipInfo?: {
|
||||
level: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
category: string;
|
||||
};
|
||||
user?: { id: string; [key: string]: any };
|
||||
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 { open: state.open, setOpen: state.setOpen, setMoney: state.setMoney, setLevel: state.setLevel };
|
||||
}),
|
||||
);
|
||||
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('您当前会员等级为' + currentLevel + ',无法降级, 会直接覆盖。需要修改请联系管理员。');
|
||||
} else if (clickLevelNumber === currentLevelNumber) {
|
||||
} else if (clickLevelNumber > currentLevelNumber) {
|
||||
currentLevelNumber != 0 && toast.info('您当前会员等级为' + currentLevel + ',会直接覆盖原有会员等级。并重新计算会员时间。');
|
||||
}
|
||||
// 打开支付弹窗
|
||||
payModalStore.setOpen(true);
|
||||
payModalStore.setMoney(clickLevelNumber);
|
||||
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 (
|
||||
<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'>
|
||||
<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>
|
||||
{payModalStore.open && <PayModal />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
149
src/pages/vip/pay-modal/PayModal.tsx
Normal file
149
src/pages/vip/pay-modal/PayModal.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import { Modal } from 'antd';
|
||||
import { create } from 'zustand';
|
||||
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;
|
||||
setOpen: (open: boolean) => void;
|
||||
money: number;
|
||||
setMoney: (money: number) => void;
|
||||
level: string;
|
||||
setLevel: (level: string) => void;
|
||||
}
|
||||
export const usePayModalStore = create<PayModalStore>((set) => ({
|
||||
open: false,
|
||||
setOpen: (open: boolean) => set({ open }),
|
||||
money: 0,
|
||||
setMoney: (money: number) => set({ money }),
|
||||
level: 'money',
|
||||
setLevel: (level: string) => set({ level }),
|
||||
}));
|
||||
|
||||
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);
|
||||
setWxQrCode(wxQrCode);
|
||||
};
|
||||
const onPay = async (payMode: 'wx' | 'alipay') => {
|
||||
const res = await queryApi.vipPay({
|
||||
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);
|
||||
setWxQrCode(wxQrCode);
|
||||
} else {
|
||||
const form = res.data.form;
|
||||
setAlipayIframeURL(form);
|
||||
}
|
||||
} else {
|
||||
toast.error(res.message || '支付失败');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<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>
|
||||
{payMode === 'wx' && wxQrCode && <img className='w-[210px] h-[210px] ' src={wxQrCode} alt='微信支付' />}
|
||||
{payMode === 'alipay' && alipayIframeURL && <iframe src={alipayIframeURL} className='w-[210px] h-[210px]' />}
|
||||
<div className='flex gap-2'>
|
||||
<button
|
||||
className='bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 cursor-pointer'
|
||||
onClick={() => {
|
||||
setPayMode('wx');
|
||||
onPay('wx');
|
||||
}}>
|
||||
微信支付
|
||||
</button>
|
||||
<button
|
||||
className='bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 cursor-pointer'
|
||||
onClick={() => {
|
||||
setPayMode('alipay');
|
||||
onPay('alipay');
|
||||
}}>
|
||||
支付宝支付
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
130
src/pages/vip/query.ts
Normal file
130
src/pages/vip/query.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
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: 'ai-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,
|
||||
);
|
||||
}
|
||||
async vipPay(
|
||||
data?: {
|
||||
level: string;
|
||||
payMode?: string;
|
||||
out_trade_no?: string;
|
||||
month?: number;
|
||||
money: number;
|
||||
},
|
||||
dataOpts?: any,
|
||||
) {
|
||||
return this.query.post(
|
||||
{
|
||||
path: 'vip',
|
||||
key: 'pay',
|
||||
data,
|
||||
},
|
||||
dataOpts,
|
||||
);
|
||||
}
|
||||
async vipCheckPayStatus(data?: { out_trade_no: string }, dataOpts?: any) {
|
||||
return this.query.post(
|
||||
{
|
||||
path: 'vip',
|
||||
key: 'check-pay-status',
|
||||
data,
|
||||
},
|
||||
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;
|
||||
},
|
||||
}));
|
||||
6
src/uitls/qrcode.ts
Normal file
6
src/uitls/qrcode.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import QRCode from 'qrcode';
|
||||
|
||||
export const generateQRCode = async (url: string) => {
|
||||
const qrCode = await QRCode.toDataURL(url);
|
||||
return qrCode;
|
||||
};
|
||||
@@ -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