feat: success reset 60%

master
guofei 2024-06-10 21:20:29 +08:00
parent 12da5f60bd
commit 50237f3f75
13 changed files with 447 additions and 17 deletions

View File

@ -19,6 +19,7 @@
"antd": "^5.18.0", "antd": "^5.18.0",
"antd-style": "^3.6.2", "antd-style": "^3.6.2",
"axios": "^1.7.2", "axios": "^1.7.2",
"dayjs": "^1.11.11",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"qs": "^6.12.1", "qs": "^6.12.1",
"tailwindcss": "^3.4.3", "tailwindcss": "^3.4.3",

View File

@ -32,6 +32,9 @@ importers:
axios: axios:
specifier: ^1.7.2 specifier: ^1.7.2
version: 1.7.2 version: 1.7.2
dayjs:
specifier: ^1.11.11
version: 1.11.11
lodash-es: lodash-es:
specifier: ^4.17.21 specifier: ^4.17.21
version: 4.17.21 version: 4.17.21

View File

@ -0,0 +1,54 @@
import { userLogoutAPI } from '@/services/system/supderAdmin';
import { history } from '@umijs/max';
import { Dropdown } from 'antd';
import { JSXElementConstructor, ReactElement, ReactNode, ReactPortal } from 'react';
enum DrowType {
LOGOUT = 'LOGOUT',
}
const handleLogout = () => {
userLogoutAPI().then(({ code }) => {
if (code === 200) {
history.replace('/login');
}
});
};
const UserDropDownRender = (props: {
dom:
| string
| number
| boolean
| ReactElement<any, string | JSXElementConstructor<any>>
| Iterable<ReactNode>
| ReactPortal
| null
| undefined;
}) => {
const handleType = {
[DrowType.LOGOUT]: handleLogout,
};
return (
<>
<Dropdown
menu={{
items: [
{
key: DrowType.LOGOUT,
label: '退出登录',
},
],
onClick: ({ key }) => {
handleType[key as DrowType]();
},
}}
>
{props.dom}
</Dropdown>
</>
);
};
export { UserDropDownRender };

View File

@ -1,12 +1,14 @@
import { PageContainer, ProLayout } from '@ant-design/pro-components'; import { PageContainer, ProLayout } from '@ant-design/pro-components';
import { Link, Outlet, useAppData, useRouteProps } from '@umijs/max'; import { Link, Outlet, useAppData, useModel, useRouteProps } from '@umijs/max';
import { App, ConfigProvider } from 'antd'; import { App, ConfigProvider } from 'antd';
import zh_CN from 'antd/locale/zh_CN'; import zh_CN from 'antd/locale/zh_CN';
import React from 'react'; import React from 'react';
import { UserDropDownRender } from './UserDropdown';
export const BasicLayout: React.FC = () => { export const BasicLayout: React.FC = () => {
const { clientRoutes } = useAppData(); const { clientRoutes } = useAppData();
const { useContainer } = useRouteProps(); const { useContainer } = useRouteProps();
const { initialState } = useModel('@@initialState');
return ( return (
<ConfigProvider locale={zh_CN}> <ConfigProvider locale={zh_CN}>
@ -16,9 +18,9 @@ export const BasicLayout: React.FC = () => {
logo={null} logo={null}
layout="mix" layout="mix"
avatarProps={{ avatarProps={{
src: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
size: 'small', size: 'small',
title: 'xxx', title: initialState?.userName,
render: (props, dom) => <UserDropDownRender dom={dom}></UserDropDownRender>,
}} }}
route={clientRoutes.find((e) => e.path === '/')} route={clientRoutes.find((e) => e.path === '/')}
contentStyle={{ contentStyle={{

View File

@ -0,0 +1,107 @@
import { userDilatationAPI } from '@/services/system/user';
import { ActionType, ProFormDependency, ProFormRadio, ProFormText } from '@ant-design/pro-components';
import { App, Form, Modal, Spin } from 'antd';
import { useEffect, useState } from 'react';
type PropTypes = {
visible: boolean;
onCancel: () => void;
tableRef: React.RefObject<EditRow<ActionType>>;
editRow: EditRow<User.UserItem>;
};
const DilatationModel = (props: PropTypes) => {
const { message } = App.useApp();
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const { visible, editRow, tableRef, onCancel } = props;
const [title, setTitle] = useState<string>();
const handleOnCancel = () => {
form.resetFields();
onCancel();
};
const handleSubmit = async () => {
const values = await form.validateFields();
if (!/^[0-9]+$/.test(values.size)) {
message.warning('请输入数字');
return;
}
if (parseInt(values.size) <= 0) {
message.warning('扩容大小需大于0');
return;
}
try {
setLoading(true);
const result = await userDilatationAPI({ userId: editRow!.id, type: values.type, addSize: values.size });
if (result.code === 200) {
message.success(result.message);
handleOnCancel();
tableRef.current?.reload();
}
} finally {
setLoading(false);
}
};
useEffect(() => {
setTitle('用户扩容');
}, []);
return (
<Modal
destroyOnClose
forceRender
title={title}
width={700}
open={visible}
afterClose={handleOnCancel}
onCancel={handleOnCancel}
onOk={handleSubmit}
>
<Spin spinning={loading}>
<Form form={form} labelCol={{ span: 5 }}>
<ProFormRadio.Group
width="md"
required
initialValue={'MB'}
name="type"
label="类型"
options={[
{
label: 'MB',
value: 'MB',
},
{
label: 'GB',
value: 'GB',
},
]}
></ProFormRadio.Group>
<ProFormText
initialValue={0}
width="md"
name="size"
required
label="容量"
placeholder="请输入扩容容量"
rules={[{ required: true, message: '请输入扩容容量' }]}
/>
<ProFormDependency name={['type', 'size']}>
{({ type, size }) => {
return (
<div className="text-red text-center text-xl">
{editRow?.email}{size} {type}
</div>
);
}}
</ProFormDependency>
</Form>
</Spin>
</Modal>
);
};
export default DilatationModel;

View File

@ -1,6 +1,8 @@
import { getUserByIdAPI } from '@/services/system/user'; import { getUserByIdAPI } from '@/services/system/user';
import { UxOrderStatus, UxOrderStatusTag } from '@/utils/const';
import { formatDateTime } from '@/utils/format';
import { ActionType } from '@ant-design/pro-components'; import { ActionType } from '@ant-design/pro-components';
import { Descriptions, Form, Modal, Spin, Tabs } from 'antd'; import { Descriptions, Empty, Form, Modal, Spin, Table, Tabs } from 'antd';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
type PropTypes = { type PropTypes = {
@ -17,6 +19,7 @@ const UserDetailsModal = (props: PropTypes) => {
const [userInfo, setUserInfo] = useState<User.UserItem>(); const [userInfo, setUserInfo] = useState<User.UserItem>();
const [projectCount, setProjectCount] = useState<User.UserItemByInfo['projectCount']>(0); const [projectCount, setProjectCount] = useState<User.UserItemByInfo['projectCount']>(0);
const [materialsCount, setMaterialsCount] = useState<User.UserItemByInfo['materialsCount']>(0); const [materialsCount, setMaterialsCount] = useState<User.UserItemByInfo['materialsCount']>(0);
const [dilatationLogs, setDilatationLogs] = useState<User.UserItemByInfo['dilatationLogs']>([]);
const [materialsGroup, setMaterialsGroup] = useState<User.UserItemByInfo['materialsGroup']>([]); const [materialsGroup, setMaterialsGroup] = useState<User.UserItemByInfo['materialsGroup']>([]);
const { visible, editRow, onCancel } = props; const { visible, editRow, onCancel } = props;
@ -34,6 +37,7 @@ const UserDetailsModal = (props: PropTypes) => {
if (result.code === 200) { if (result.code === 200) {
setUserInfo(result.data.userInfo); setUserInfo(result.data.userInfo);
setProjectCount(result.data.projectCount); setProjectCount(result.data.projectCount);
setDilatationLogs(result.data.dilatationLogs);
setMaterialsCount(result.data.materialsCount); setMaterialsCount(result.data.materialsCount);
setMaterialsGroup(result.data.materialsGroup); setMaterialsGroup(result.data.materialsGroup);
} }
@ -53,6 +57,21 @@ const UserDetailsModal = (props: PropTypes) => {
} }
}, [visible]); }, [visible]);
const RenderPackage = ({ userInfo }: { userInfo: User.UserItem }) => {
if (userInfo.package) {
return (
userInfo?.package?.name +
'/' +
userInfo?.package?.showMoney +
'元' +
'/' +
userInfo?.package?.materialSpace +
userInfo?.package?.type
);
}
return '-';
};
const RenderUserDetails = () => { const RenderUserDetails = () => {
if (!userInfo) return <></>; if (!userInfo) return <></>;
return ( return (
@ -60,22 +79,16 @@ const UserDetailsModal = (props: PropTypes) => {
<Descriptions.Item label="邮箱">{userInfo.email}</Descriptions.Item> <Descriptions.Item label="邮箱">{userInfo.email}</Descriptions.Item>
<Descriptions.Item label="用户名称">{userInfo.userName}</Descriptions.Item> <Descriptions.Item label="用户名称">{userInfo.userName}</Descriptions.Item>
<Descriptions.Item label="套餐信息"> <Descriptions.Item label="套餐信息">
{userInfo?.package?.name + <RenderPackage userInfo={userInfo} />
'/' +
userInfo?.package?.showMoney +
'元' +
'/' +
userInfo?.package?.materialSpace +
userInfo?.package?.type}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="总存储空间"> <Descriptions.Item label="总存储空间">
{userInfo?.increasedStorageInKb && (userInfo.increasedStorageInKb / 1024).toFixed(2) + 'MB'} {userInfo?.increasedStorageInKb && (userInfo.increasedStorageInKb / 1024).toFixed(2)}MB
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="已使用空间"> <Descriptions.Item label="已使用空间">
{userInfo?.UserProfile && (userInfo.UserProfile.storageUsedInKb / 1024).toFixed(2) + 'MB'} {userInfo?.UserProfile && ((userInfo?.UserProfile?.storageUsedInKb ?? 0) / 1024).toFixed(2)}MB
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="剩余存储空间"> <Descriptions.Item label="剩余存储空间">
{((userInfo.increasedStorageInKb! - userInfo!.UserProfile!.storageUsedInKb) / 1024).toFixed(2) + 'MB'} {((userInfo.increasedStorageInKb! - (userInfo?.UserProfile?.storageUsedInKb ?? 0)) / 1024).toFixed(2)}MB
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="项目数量">{projectCount}</Descriptions.Item> <Descriptions.Item label="项目数量">{projectCount}</Descriptions.Item>
<Descriptions.Item label="素材数量">{materialsCount}</Descriptions.Item> <Descriptions.Item label="素材数量">{materialsCount}</Descriptions.Item>
@ -94,6 +107,114 @@ const UserDetailsModal = (props: PropTypes) => {
); );
}; };
const RenderUserOrders = () => {
if (userInfo && userInfo.Order!.length === 0) {
return (
<>
<Empty />
</>
);
}
return (
<Table
dataSource={userInfo!.Order}
columns={[
{
title: '订单号',
dataIndex: 'orderNo',
align: 'center',
render: (_, record: Order.OrderItem) => {
if (record.isApply === true) {
return record.orderNo + '(试用)';
}
return record.orderNo;
},
},
{
title: '第三方订单号',
dataIndex: 'platformOrderStatus',
align: 'center',
},
{
title: '支付类型',
dataIndex: 'payType',
align: 'center',
render: (_, record: Order.OrderItem) => {
// 支付类型 1. 微信 2. 支付宝 3. paypal -1: 后台创建
if (record.payType === 1) {
return '微信';
} else if (record.payType === 2) {
return '支付宝';
} else if (record.payType === 3) {
return 'paypal';
}
return '后台创建';
},
},
{
title: '套餐',
dataIndex: 'goodsName',
align: 'center',
},
{
title: '订单状态',
dataIndex: 'orderStatus',
align: 'center',
render: (_, record: Order.OrderItem) => {
const color = Reflect.get(UxOrderStatusTag, record.orderStatus);
return (
<div className="flex items-center justify-center">
<div className="w-[10px] h-[10px] mr-2" style={{ background: color }}></div>
{Reflect.get(UxOrderStatus, record.orderStatus)}
</div>
);
},
},
{
title: '订单创建时间',
dataIndex: 'createdDateTime',
align: 'center',
},
]}
></Table>
);
};
const RenderDilatationLogs = () => {
if (dilatationLogs.length === 0) {
return (
<>
<Empty />
</>
);
}
return (
<Table
dataSource={dilatationLogs}
columns={[
{
title: '扩容大小',
align: 'center',
render: (_, record: User.UserItemByInfo['dilatationLogs'][0]) => {
return record.size + record.type;
},
},
{
title: '扩容时间',
dataIndex: 'createDate',
align: 'center',
render: (_, record: User.UserItemByInfo['dilatationLogs'][0]) => formatDateTime(record.createDate),
},
{
title: '备注',
dataIndex: 'desc',
align: 'center',
},
]}
></Table>
);
};
return ( return (
<Modal <Modal
destroyOnClose destroyOnClose
@ -109,8 +230,12 @@ const UserDetailsModal = (props: PropTypes) => {
<Tabs.TabPane key={'details'} tab={'用户详情'}> <Tabs.TabPane key={'details'} tab={'用户详情'}>
<RenderUserDetails /> <RenderUserDetails />
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane key={'handleLogs'} tab={'操作记录'}></Tabs.TabPane> <Tabs.TabPane key={'handleLogs'} tab={'扩容记录'}>
<Tabs.TabPane key={'orderInfo'} tab={'订单信息'}></Tabs.TabPane> <RenderDilatationLogs />
</Tabs.TabPane>
<Tabs.TabPane key={'orderInfo'} tab={'订单信息'}>
<RenderUserOrders />
</Tabs.TabPane>
</Tabs> </Tabs>
</Spin> </Spin>
</Modal> </Modal>

View File

@ -1,8 +1,11 @@
import { changeStatusAPI, delUxUserAPI, getUserListAPI } from '@/services/system/user'; import { changeStatusAPI, delUxUserAPI, getUserListAPI } from '@/services/system/user';
import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components'; import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components';
import { App, Button, Space, TableProps, message } from 'antd'; import { App, Button, Radio, Space, TableProps, message } from 'antd';
import { formatDateTime } from '@/utils/format';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import CreateModel from './CreateModule'; import CreateModel from './CreateModule';
import DilatationModel from './Dilatation';
import OpenPackageModel from './OpenPackage'; import OpenPackageModel from './OpenPackage';
import UserDetailsModal from './UserDetails'; import UserDetailsModal from './UserDetails';
@ -13,6 +16,9 @@ const Page = () => {
const [createModalVisible, setCreateModalVisible] = useState<boolean>(false); const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);
const [openPackageVisible, setOpenPackageVisible] = useState<boolean>(false); const [openPackageVisible, setOpenPackageVisible] = useState<boolean>(false);
const [userDetailsVisible, setUserDetailsVisible] = useState<boolean>(false); const [userDetailsVisible, setUserDetailsVisible] = useState<boolean>(false);
const [kuoRongVisible, setKuoRongVisible] = useState<boolean>(false);
const [showType, setShowType] = useState<string>('MB');
const [showTypeValue, setShowTypeValue] = useState<number>(1024);
const [editRow, setEditRow] = useState<EditRow<User.UserItem>>(); const [editRow, setEditRow] = useState<EditRow<User.UserItem>>();
const [tableParams, setTableParams] = useState<TableParams>({ const [tableParams, setTableParams] = useState<TableParams>({
pagination: { pagination: {
@ -43,6 +49,11 @@ const Page = () => {
}); });
}; };
const handledDilatation = (record: User.UserItem) => {
setEditRow(record);
setKuoRongVisible(true);
};
const columns: ProColumns[] = [ const columns: ProColumns[] = [
{ {
title: '邮箱', title: '邮箱',
@ -89,6 +100,45 @@ const Page = () => {
); );
}, },
}, },
{
title: '剩余容量',
dataIndex: 'packageId',
align: 'center',
hideInSearch: true,
renderText: (_, record: User.UserItem) => {
return (
<div>
{((record.increasedStorageInKb! - (record?.UserProfile?.storageUsedInKb ?? 0)) / showTypeValue).toFixed(2) +
showType}
<Button type="link" onClick={() => handledDilatation(record)}>
</Button>
</div>
);
},
filterDropdown: () => {
return (
<div className="p-2">
<Radio.Group
onChange={(e) => {
const type = e.target.value;
setShowType(type);
if (type === 'GB') {
setShowTypeValue(Math.pow(1024, 2));
} else {
setShowTypeValue(1024);
}
}}
value={showType}
>
<Radio value={'MB'}>MB</Radio>
<Radio value={'GB'}>GB</Radio>
</Radio.Group>
</div>
);
},
},
{ {
title: '用户来源', title: '用户来源',
dataIndex: 'userSource', dataIndex: 'userSource',
@ -126,17 +176,27 @@ const Page = () => {
); );
}, },
}, },
{
title: '套餐过期时间',
dataIndex: 'expired',
align: 'center',
hideInSearch: true,
renderText: (_, record: User.UserItem) => (!record.expired ? '-' : formatDateTime(record.expired)),
},
{ {
title: '账户创建时间', title: '账户创建时间',
dataIndex: 'createdDateTime', dataIndex: 'createdDateTime',
align: 'center', align: 'center',
hideInSearch: true, hideInSearch: true,
renderText: (_, record: User.UserItem) =>
!record.createdDateTime ? '-' : formatDateTime(record.createdDateTime),
}, },
{ {
title: '最后登录时间', title: '最后登录时间',
dataIndex: 'lastLoginTime', dataIndex: 'lastLoginTime',
align: 'center', align: 'center',
hideInSearch: true, hideInSearch: true,
renderText: (_, record: User.UserItem) => (!record.lastLoginTime ? '-' : formatDateTime(record.lastLoginTime)),
}, },
{ {
title: '操作', title: '操作',
@ -271,6 +331,16 @@ const Page = () => {
tableRef={tableRef} tableRef={tableRef}
editRow={editRow} editRow={editRow}
/> />
{/* 扩容 */}
<DilatationModel
visible={kuoRongVisible}
onCancel={() => {
setEditRow(undefined);
setKuoRongVisible(false);
}}
tableRef={tableRef}
editRow={editRow}
/>
</> </>
); );
}; };

View File

@ -0,0 +1,31 @@
declare namespace Order {
type OrderItem = {
id?: string;
userEmail: string;
userId: string;
userName?: string;
user?: User.UserItem;
payType: number;
payTypeStr: string;
orderNo: string;
packageId: string;
package?: Package.PackageItem;
packageJson?: string;
orderStatus: number;
platformOrderStatus?: string;
platformNo?: string;
goodsName: string;
price: number;
quantity?: number;
payCode?: string;
payCodeExpired?: string;
platformNotifyTime?: string;
orderType: number;
createSource: number;
createSouceBy?: string;
createdDateTime: string;
lastUpdateDateTime?: string;
comment?: string;
isApply: boolean;
};
}

View File

@ -19,3 +19,7 @@ export const editSupderAdminAPI = (data: SuperAdmin.SuperAdminItem): Promise<API
export const delSuperAdminAPI = (id: string): Promise<API.ResponstBody> => { export const delSuperAdminAPI = (id: string): Promise<API.ResponstBody> => {
return request.delete(`/system/superAdmin/${id}`); return request.delete(`/system/superAdmin/${id}`);
}; };
export const userLogoutAPI = (): Promise<API.ResponstBody> => {
return request.post(`/system/superAdmin/logout`);
};

View File

@ -24,3 +24,11 @@ export const delUxUserAPI = (id: string): Promise<API.ResponstBody> => {
export const openPackageAPI = (params: User.UserItem): Promise<API.ResponstBody> => { export const openPackageAPI = (params: User.UserItem): Promise<API.ResponstBody> => {
return request.post('/system/user/openPackage', params); return request.post('/system/user/openPackage', params);
}; };
export const userDilatationAPI = (params: {
userId: string;
addSize: number;
type: string;
}): Promise<API.ResponstBody> => {
return request.post('/system/user/dilatation', params);
};

View File

@ -27,6 +27,7 @@ declare namespace User {
package?: Package.PackageItem; package?: Package.PackageItem;
UserProfile?: UserProfile; UserProfile?: UserProfile;
Order?: Order.OrderItem[];
}; };
type UserItemByInfo = { type UserItemByInfo = {
@ -35,5 +36,6 @@ declare namespace User {
projectCount: number; projectCount: number;
projectCount: number; projectCount: number;
materialsGroup: { _count: number; materialType: string }[]; materialsGroup: { _count: number; materialType: string }[];
dilatationLogs: { id: string; type: string; size: string; desc: string; createDate: string }[];
}; };
} }

16
src/utils/const.ts 100644
View File

@ -0,0 +1,16 @@
// 订单状态: 1: 待支付 2. 已支付 3. 订单取消 4. 支付失败 5. 订单超时支付(二维码过期)
export const UxOrderStatus: Record<number, string> = {
1: '待支付',
2: '已支付',
3: '订单取消',
4: '支付失败',
5: '订单超时支付(二维码过期)',
};
export const UxOrderStatusTag: Record<number, string> = {
1: '#3498db', // 蓝色
2: '#27ae60', // 绿色
3: '#c8d6e5', // 灰色
4: '#e74c3c', // 红色
5: '订单超时支付(二维码过期)', //
};

View File

@ -1,4 +1,11 @@
// @ts-ignore
import dayjs from 'dayjs';
// 示例方法,没有实际意义 // 示例方法,没有实际意义
export function trim(str: string) { export function trim(str: string) {
return str.trim(); return str.trim();
} }
export const formatDateTime = (time: string) => {
return dayjs(time).format('YYYY-MM-DD HH:ss:mm');
};