Files
makemd/dashboard/src/pages/Settings/InstancePurchase.tsx

543 lines
17 KiB
TypeScript
Raw Normal View History

import {
useState,
useEffect,
Card,
Row,
Col,
Button,
Tag,
Typography,
Space,
Radio,
Select,
Spin,
message,
Steps,
Divider,
Alert,
ConfigProvider,
theme,
SaveOutlined,
CopyOutlined,
DatabaseOutlined,
DownloadOutlined,
DollarOutlined,
CheckCircleOutlined,
CreditCardOutlined,
AimOutlined,
PieChartOutlined,
Title,
Text,
Paragraph,
FC,
} from '@/imports';
import { instanceDataSource, InstanceType, InstanceConfig } from '@/services/instanceDataSource';
import { useUser } from '@/contexts/UserContext';
const InstancePurchase: FC = () => {
const { currentUser } = useUser();
const [currentStep, setCurrentStep] = useState(0);
const [instanceTypes, setInstanceTypes] = useState<InstanceType[]>([]);
const [selectedInstanceType, setSelectedInstanceType] = useState<string>('medium');
const [instanceConfig, setInstanceConfig] = useState<InstanceConfig>({
cpu: 4,
memory: 8,
storage: 200,
bandwidth: 50
});
const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>('monthly');
const [price, setPrice] = useState({ monthly: 0, yearly: 0, discount: 0 });
const [paymentMethod, setPaymentMethod] = useState<string>('creditCard');
const [loading, setLoading] = useState(false);
const [configOptions, setConfigOptions] = useState({
cpuOptions: [] as number[],
memoryOptions: [] as number[],
storageOptions: [] as number[],
bandwidthOptions: [] as number[]
});
// 检查用户是否为 ADMIN
const isAdmin = currentUser?.role === 'ADMIN';
useEffect(() => {
loadInstanceTypes();
loadConfigOptions();
}, []);
useEffect(() => {
if (selectedInstanceType) {
calculatePrice();
}
}, [selectedInstanceType, instanceConfig, billingCycle]);
const loadInstanceTypes = async () => {
setLoading(true);
try {
const types = await instanceDataSource.getInstanceTypes();
setInstanceTypes(types);
if (types.length > 0) {
const defaultType = types.find(t => t.recommended) || types[0];
setSelectedInstanceType(defaultType.id);
setInstanceConfig(defaultType.defaultConfig);
}
} catch (error) {
message.error('Failed to load instance types');
} finally {
setLoading(false);
}
};
const loadConfigOptions = async () => {
try {
const options = await instanceDataSource.getInstanceConfigOptions();
setConfigOptions(options);
} catch (error) {
message.error('Failed to load config options');
}
};
const calculatePrice = async () => {
try {
const priceData = await instanceDataSource.calculatePrice(
selectedInstanceType,
instanceConfig,
billingCycle
);
setPrice(priceData);
} catch (error) {
message.error('Failed to calculate price');
}
};
const handleInstanceTypeChange = (typeId: string) => {
setSelectedInstanceType(typeId);
const instanceType = instanceTypes.find(t => t.id === typeId);
if (instanceType) {
setInstanceConfig(instanceType.defaultConfig);
}
};
const handleConfigChange = (key: keyof InstanceConfig, value: number) => {
setInstanceConfig(prev => ({
...prev,
[key]: value
}));
};
const handleNext = () => {
if (currentStep < 3) {
setCurrentStep(currentStep + 1);
}
};
const handlePrevious = () => {
if (currentStep > 0) {
setCurrentStep(currentStep - 1);
}
};
const handleSubmit = async () => {
setLoading(true);
try {
const order = await instanceDataSource.createOrder(
selectedInstanceType,
instanceConfig,
billingCycle,
paymentMethod
);
message.success('Order created successfully!');
setCurrentStep(4);
} catch (error) {
message.error('Failed to create order');
} finally {
setLoading(false);
}
};
const renderInstanceTypeSelection = () => (
<Card title={isAdmin ? "选择实例类型" : "选择套餐"}>
<Row gutter={[16, 16]}>
{instanceTypes.map(type => (
<Col xs={24} sm={12} lg={8} key={type.id}>
<Card
hoverable
style={{
borderColor: selectedInstanceType === type.id ? '#1890ff' : undefined,
borderWidth: selectedInstanceType === type.id ? 2 : 1,
borderRadius: '12px',
cursor: 'pointer'
}}
bodyStyle={{ padding: '24px' }}
onClick={() => handleInstanceTypeChange(type.id)}
>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '48px', marginBottom: '16px' }}>
{type.icon}
</div>
<Title level={4} style={{ margin: 0, marginBottom: '8px' }}>
{type.name}
</Title>
<Tag color="blue" style={{ marginBottom: '16px' }}>{type.cupSize}</Tag>
{type.recommended && (
<Tag color="gold" style={{ marginBottom: '16px' }}></Tag>
)}
<Paragraph type="secondary" style={{ marginBottom: '16px' }}>
{type.description}
</Paragraph>
{isAdmin && (
<>
<Divider style={{ margin: '16px 0' }} />
<Space direction="vertical" style={{ width: '100%' }}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Text>CPU</Text>
<Text strong>{type.defaultConfig.cpu} </Text>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Text></Text>
<Text strong>{type.defaultConfig.memory} GB</Text>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Text></Text>
<Text strong>{type.defaultConfig.storage} GB</Text>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Text></Text>
<Text strong>{type.defaultConfig.bandwidth} Mbps</Text>
</div>
</Space>
</>
)}
</div>
</Card>
</Col>
))}
</Row>
</Card>
);
const renderInstanceConfig = () => (
<Card title="配置实例">
<Row gutter={[16, 16]}>
<Col xs={24} sm={12} md={6}>
<Card size="small" title={<Space><CpuOutlined /> CPU核心数</Space>}>
<Select
style={{ width: '100%' }}
value={instanceConfig.cpu}
onChange={(value) => handleConfigChange('cpu', value)}
options={configOptions.cpuOptions.map(option => ({
value: option,
label: `${option}`
}))}
/>
</Card>
</Col>
<Col xs={24} sm={12} md={6}>
<Card size="small" title={<Space><ServerOutlined /> </Space>}>
<Select
style={{ width: '100%' }}
value={instanceConfig.memory}
onChange={(value) => handleConfigChange('memory', value)}
options={configOptions.memoryOptions.map(option => ({
value: option,
label: `${option} GB`
}))}
/>
</Card>
</Col>
<Col xs={24} sm={12} md={6}>
<Card size="small" title={<Space><DatabaseOutlined /> </Space>}>
<Select
style={{ width: '100%' }}
value={instanceConfig.storage}
onChange={(value) => handleConfigChange('storage', value)}
options={configOptions.storageOptions.map(option => ({
value: option,
label: `${option} GB`
}))}
/>
</Card>
</Col>
<Col xs={24} sm={12} md={6}>
<Card size="small" title={<Space><DownloadOutlined /> </Space>}>
<Select
style={{ width: '100%' }}
value={instanceConfig.bandwidth}
onChange={(value) => handleConfigChange('bandwidth', value)}
options={configOptions.bandwidthOptions.map(option => ({
value: option,
label: `${option} Mbps`
}))}
/>
</Card>
</Col>
</Row>
<Card style={{ marginTop: '16px' }} title="计费周期">
<Radio.Group
value={billingCycle}
onChange={(e) => setBillingCycle(e.target.value)}
buttonStyle="solid"
size="large"
>
<Radio.Button value="monthly">
<Tag style={{ marginLeft: '8px' }}>¥{price.monthly}</Tag>
</Radio.Button>
<Radio.Button value="yearly">
<Tag color="red" style={{ marginLeft: '8px' }}>¥{price.yearly} ({price.discount}%)</Tag>
</Radio.Button>
</Radio.Group>
<Alert
message="价格说明"
description={`根据您选择的配置,${billingCycle === 'monthly' ? '月付' : '年付'}价格为 ¥${billingCycle === 'monthly' ? price.monthly : price.yearly}`}
type="info"
showIcon
style={{ marginTop: '16px' }}
/>
</Card>
</Card>
);
const renderPaymentMethod = () => (
<Card title="选择支付方式">
<Radio.Group
value={paymentMethod}
onChange={(e) => setPaymentMethod(e.target.value)}
style={{ width: '100%' }}
>
<Card
hoverable
style={{
marginBottom: '16px',
borderColor: paymentMethod === 'creditCard' ? '#1890ff' : undefined,
borderWidth: paymentMethod === 'creditCard' ? 2 : 1,
cursor: 'pointer'
}}
onClick={() => setPaymentMethod('creditCard')}
>
<Row align="middle">
<Col span={4}>
<CreditCardOutlined style={{ fontSize: '24px', color: '#1890ff' }} />
</Col>
<Col span={16}>
<Text strong></Text>
<Paragraph type="secondary">VisaMastercardAmerican Express等</Paragraph>
</Col>
<Col span={4}>
<Radio value="creditCard" />
</Col>
</Row>
</Card>
<Card
hoverable
style={{
marginBottom: '16px',
borderColor: paymentMethod === 'alipay' ? '#1890ff' : undefined,
borderWidth: paymentMethod === 'alipay' ? 2 : 1,
cursor: 'pointer'
}}
onClick={() => setPaymentMethod('alipay')}
>
<Row align="middle">
<Col span={4}>
<AliPayOutlined style={{ fontSize: '24px', color: '#1890ff' }} />
</Col>
<Col span={16}>
<Text strong></Text>
<Paragraph type="secondary">使</Paragraph>
</Col>
<Col span={4}>
<Radio value="alipay" />
</Col>
</Row>
</Card>
<Card
hoverable
style={{
borderColor: paymentMethod === 'wechat' ? '#1890ff' : undefined,
borderWidth: paymentMethod === 'wechat' ? 2 : 1,
cursor: 'pointer'
}}
onClick={() => setPaymentMethod('wechat')}
>
<Row align="middle">
<Col span={4}>
<WechatOutlined style={{ fontSize: '24px', color: '#1890ff' }} />
</Col>
<Col span={16}>
<Text strong></Text>
<Paragraph type="secondary">使</Paragraph>
</Col>
<Col span={4}>
<Radio value="wechat" />
</Col>
</Row>
</Card>
</Radio.Group>
</Card>
);
const renderOrderSummary = () => {
const instanceType = instanceTypes.find(t => t.id === selectedInstanceType);
return (
<Card title="订单摘要">
<Space direction="vertical" style={{ width: '100%' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '8px 0' }}>
<Text></Text>
<Text strong>{instanceType?.name}</Text>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '8px 0' }}>
<Text>CPU</Text>
<Text strong>{instanceConfig.cpu} </Text>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '8px 0' }}>
<Text></Text>
<Text strong>{instanceConfig.memory} GB</Text>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '8px 0' }}>
<Text></Text>
<Text strong>{instanceConfig.storage} GB</Text>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '8px 0' }}>
<Text></Text>
<Text strong>{instanceConfig.bandwidth} Mbps</Text>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '8px 0' }}>
<Text></Text>
<Text strong>{billingCycle === 'monthly' ? '月付' : '年付'}</Text>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '8px 0' }}>
<Text></Text>
<Text strong>
{paymentMethod === 'creditCard' ? '信用卡' :
paymentMethod === 'alipay' ? '支付宝' : '微信支付'}
</Text>
</div>
<Divider />
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '8px 0', fontSize: '18px' }}>
<Text strong></Text>
<Text strong style={{ color: '#1890ff' }}>
¥{billingCycle === 'monthly' ? price.monthly : price.yearly}
</Text>
</div>
</Space>
</Card>
);
};
const renderSuccess = () => (
<Card title="购买成功">
<div style={{ textAlign: 'center', padding: '48px 0' }}>
<CheckCircleOutlined style={{ fontSize: '72px', color: '#52c41a', marginBottom: '24px' }} />
<Title level={3} style={{ marginBottom: '16px' }}></Title>
<Paragraph style={{ marginBottom: '24px' }}>
</Paragraph>
<Button type="primary" size="large">
</Button>
</div>
</Card>
);
// 根据用户角色生成步骤
const steps = isAdmin ? [
{ title: '选择实例类型' },
{ title: '配置实例' },
{ title: '选择支付方式' },
{ title: '确认订单' },
{ title: '购买成功' }
] : [
{ title: '选择套餐' },
{ title: '选择支付方式' },
{ title: '确认订单' },
{ title: '购买成功' }
];
// 处理下一步逻辑
const handleNext = () => {
if (isAdmin) {
if (currentStep < 3) {
setCurrentStep(currentStep + 1);
}
} else {
if (currentStep < 2) {
setCurrentStep(currentStep + 1);
}
}
};
// 处理上一步逻辑
const handlePrevious = () => {
if (currentStep > 0) {
setCurrentStep(currentStep - 1);
}
};
return (
<div className="instance-purchase" style={{ padding: '24px', background: '#f5f5f5', minHeight: '100vh' }}>
<ConfigProvider
theme={{
token: {
colorPrimary: '#1890ff',
borderRadius: 8,
},
}}
>
<div className="page-header" style={{ marginBottom: '24px' }}>
<Title level={2} style={{ margin: 0 }}>
<ServerOutlined style={{ marginRight: '8px' }} />
</Title>
<Paragraph type="secondary">
{isAdmin ? '选择适合您业务需求的实例配置' : '选择适合您业务需求的套餐'}
</Paragraph>
</div>
<Steps
current={currentStep}
items={steps}
style={{ marginBottom: '32px' }}
/>
<Spin spinning={loading}>
{currentStep === 0 && renderInstanceTypeSelection()}
{isAdmin && currentStep === 1 && renderInstanceConfig()}
{currentStep === (isAdmin ? 2 : 1) && renderPaymentMethod()}
{currentStep === (isAdmin ? 3 : 2) && renderOrderSummary()}
{currentStep === (isAdmin ? 4 : 3) && renderSuccess()}
</Spin>
<div style={{ marginTop: '32px', textAlign: 'center' }}>
{currentStep > 0 && currentStep < (isAdmin ? 4 : 3) && (
<Button
style={{ marginRight: '16px' }}
onClick={handlePrevious}
>
</Button>
)}
{currentStep < (isAdmin ? 3 : 2) && (
<Button
type="primary"
onClick={handleNext}
>
</Button>
)}
{currentStep === (isAdmin ? 3 : 2) && (
<Button
type="primary"
onClick={handleSubmit}
loading={loading}
>
</Button>
)}
</div>
</ConfigProvider>
</div>
);
};
export default InstancePurchase;