Files
makemd/dashboard/src/pages/Settings/InstancePurchase.tsx
wurenzhi 22308fe042 refactor: 重构项目结构并优化代码
- 删除无用的文件和错误日志
- 创建统一的 imports 模块集中管理依赖
- 重构组件使用新的 imports 方式
- 修复文档路径大小写问题
- 优化类型定义和接口导出
- 更新依赖版本
- 改进错误处理和API配置
- 统一组件导出方式
2026-03-27 16:56:06 +08:00

543 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;