Files
goalfylearning-admin-web/src/pages/VmPricing.tsx

733 lines
21 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 React, { useState, useEffect } from 'react';
import {
Card,
Table,
Button,
Input,
Select,
Space,
Form,
message,
Switch,
Typography,
InputNumber,
Tabs,
Modal,
Popconfirm,
} from 'antd';
import { SearchOutlined, ReloadOutlined, EditOutlined, PlusOutlined, DeleteOutlined, CheckOutlined } from '@ant-design/icons';
import { VmPricingApi } from '../services/api';
const { Option } = Select;
const { Title } = Typography;
const { TabPane } = Tabs;
interface VmSpecItem {
id: number;
spec_type: string;
cpu_cores: number;
memory_gb: number;
description?: string;
cost_price_per_minute: number;
markup_rate: number;
is_active: boolean;
created_at: string;
updated_at: string;
}
interface VmTemplateItem {
id: number;
spec_type: string;
template_id: string;
is_default: boolean;
created_at: string;
updated_at: string;
}
const VmPricing: React.FC = () => {
const [loading, setLoading] = useState(false);
const [specsData, setSpecsData] = useState<VmSpecItem[]>([]);
const [templatesData, setTemplatesData] = useState<VmTemplateItem[]>([]);
const [specsTotal, setSpecsTotal] = useState(0);
const [templatesTotal, setTemplatesTotal] = useState(0);
const [specsPage, setSpecsPage] = useState(1);
const [templatesPage, setTemplatesPage] = useState(1);
const [specsPageSize, setSpecsPageSize] = useState(10);
const [templatesPageSize, setTemplatesPageSize] = useState(10);
const [activeTab, setActiveTab] = useState('specs');
const [editingCell, setEditingCell] = useState<{ rowId: number; field: string } | null>(null);
const [editingValue, setEditingValue] = useState<number>(0);
const [specModalVisible, setSpecModalVisible] = useState(false);
const [templateModalVisible, setTemplateModalVisible] = useState(false);
const [specsForm] = Form.useForm();
const [templatesForm] = Form.useForm();
const [templateForm] = Form.useForm();
const [specForm] = Form.useForm();
const [specTypes, setSpecTypes] = useState<string[]>([]);
// 获取规格类型列表(用于模板表单)
const fetchSpecTypes = async () => {
try {
const response = await VmPricingApi.getSpecs({ size: 1000 });
const types = [...new Set((response.data || []).map((item: VmSpecItem) => item.spec_type))];
setSpecTypes(types);
} catch (error) {
console.error('获取规格类型失败:', error);
}
};
// 获取规格数据
const fetchSpecs = async (searchParams: any = {}) => {
setLoading(true);
try {
const response = await VmPricingApi.getSpecs({
...searchParams,
page: specsPage,
size: specsPageSize,
});
setSpecsData(response.data || []);
setSpecsTotal(response.total || 0);
} catch (error) {
console.error('获取规格数据失败:', error);
message.error('获取规格数据失败');
} finally {
setLoading(false);
}
};
// 获取模板数据
const fetchTemplates = async (searchParams: any = {}) => {
setLoading(true);
try {
const response = await VmPricingApi.getTemplates({
...searchParams,
page: templatesPage,
size: templatesPageSize,
});
setTemplatesData(response.data || []);
setTemplatesTotal(response.total || 0);
} catch (error) {
console.error('获取模板数据失败:', error);
message.error('获取模板数据失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchSpecTypes(); // 获取规格类型列表
if (activeTab === 'specs') {
fetchSpecs();
} else {
fetchTemplates();
}
}, [specsPage, specsPageSize, templatesPage, templatesPageSize, activeTab]);
useEffect(() => {
// 当规格数据更新时,更新规格类型列表
if (specsData.length > 0) {
const types = [...new Set(specsData.map(item => item.spec_type))];
setSpecTypes(types);
}
}, [specsData]);
// 更新规格
const handleUpdateSpec = async (id: number, field: string, value: number | boolean) => {
try {
await VmPricingApi.updateSpec(id, { [field]: value });
message.success('更新成功');
setEditingCell(null);
fetchSpecs();
} catch (error) {
message.error('更新失败');
console.error('更新规格失败:', error);
}
};
// 更新状态
const handleUpdateStatus = async (id: number, isActive: boolean) => {
handleUpdateSpec(id, 'is_active', isActive);
};
// 搜索规格
const handleSpecsSearch = (values: any) => {
setSpecsPage(1);
fetchSpecs(values);
};
// 重置规格搜索
const handleSpecsReset = () => {
specsForm.resetFields();
setSpecsPage(1);
fetchSpecs();
};
// 搜索模板
const handleTemplatesSearch = (values: any) => {
setTemplatesPage(1);
fetchTemplates(values);
};
// 重置模板搜索
const handleTemplatesReset = () => {
templatesForm.resetFields();
setTemplatesPage(1);
fetchTemplates();
};
// 开始编辑
const handleEdit = (rowId: number, field: string, value: number) => {
setEditingCell({ rowId, field });
setEditingValue(value);
};
// 保存编辑
const handleSaveEdit = (rowId: number, field: string) => {
let valueToSave = editingValue;
// 加价率需要转换为小数(前端显示为百分比,后端存储为小数)
if (field === 'markup_rate') {
valueToSave = editingValue / 100;
}
handleUpdateSpec(rowId, field, valueToSave);
};
// 取消编辑
const handleCancelEdit = () => {
setEditingCell(null);
};
// 创建规格
const handleCreateSpec = async (values: any) => {
try {
// 转换加价率:前端输入百分比,后端存储小数
const data = {
...values,
markup_rate: values.markup_rate ? values.markup_rate / 100 : undefined,
};
await VmPricingApi.createSpec(data);
message.success('规格创建成功');
setSpecModalVisible(false);
specForm.resetFields();
fetchSpecs();
fetchSpecTypes(); // 刷新规格类型列表
} catch (error: any) {
message.error(error?.response?.data?.message || '规格创建失败');
console.error('创建规格失败:', error);
}
};
// 创建模板
const handleCreateTemplate = async (values: any) => {
try {
await VmPricingApi.createTemplate(values);
message.success('模板创建成功');
setTemplateModalVisible(false);
templateForm.resetFields();
fetchTemplates();
} catch (error: any) {
message.error(error?.response?.data?.message || '模板创建失败');
console.error('创建模板失败:', error);
}
};
// 删除规格
const handleDeleteSpec = async (id: number) => {
try {
await VmPricingApi.deleteSpec(id);
message.success('规格删除成功');
fetchSpecs();
fetchSpecTypes(); // 刷新规格类型列表
} catch (error) {
message.error('规格删除失败');
console.error('删除规格失败:', error);
}
};
// 删除模板
const handleDeleteTemplate = async (id: number) => {
try {
await VmPricingApi.deleteTemplate(id);
message.success('模板删除成功');
fetchTemplates();
} catch (error) {
message.error('模板删除失败');
console.error('删除模板失败:', error);
}
};
// 设置默认模板
const handleSetDefaultTemplate = async (id: number) => {
try {
await VmPricingApi.setDefaultTemplate(id);
message.success('默认模板设置成功');
fetchTemplates();
} catch (error) {
message.error('设置默认模板失败');
console.error('设置默认模板失败:', error);
}
};
// 规格表格列
const specsColumns = [
{
title: '配置类型',
dataIndex: 'spec_type',
key: 'spec_type',
width: 120,
},
{
title: 'CPU核心',
dataIndex: 'cpu_cores',
key: 'cpu_cores',
width: 100,
},
{
title: '内存(GB)',
dataIndex: 'memory_gb',
key: 'memory_gb',
width: 100,
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
width: 150,
render: (text: string) => text || '-',
},
{
title: '成本价($/分钟)',
dataIndex: 'cost_price_per_minute',
key: 'cost_price_per_minute',
width: 160,
render: (value: number, record: VmSpecItem) => {
if (editingCell?.rowId === record.id && editingCell?.field === 'cost_price_per_minute') {
return (
<Space.Compact>
<InputNumber
value={editingValue}
onChange={(val) => setEditingValue(val || 0)}
precision={8}
min={0}
style={{ width: 120 }}
autoFocus
/>
<Button
type="primary"
size="small"
onClick={() => handleSaveEdit(record.id, 'cost_price_per_minute')}
>
</Button>
<Button size="small" onClick={handleCancelEdit}>
</Button>
</Space.Compact>
);
}
return (
<div
style={{ cursor: 'pointer', display: 'flex', alignItems: 'center' }}
onClick={() => handleEdit(record.id, 'cost_price_per_minute', value)}
>
<span>${value.toFixed(8)}</span>
<EditOutlined style={{ marginLeft: 8, fontSize: 12, color: '#1890ff' }} />
</div>
);
},
},
{
title: '加价率(%)',
dataIndex: 'markup_rate',
key: 'markup_rate',
width: 140,
render: (value: number, record: VmSpecItem) => {
if (editingCell?.rowId === record.id && editingCell?.field === 'markup_rate') {
return (
<Space.Compact>
<InputNumber
value={editingValue}
onChange={(val) => setEditingValue(val || 0)}
precision={4}
min={0}
max={100}
style={{ width: 100 }}
autoFocus
formatter={(value) => `${value}%`}
parser={(value) => value!.replace('%', '')}
/>
<Button
type="primary"
size="small"
onClick={() => handleSaveEdit(record.id, 'markup_rate')}
>
</Button>
<Button size="small" onClick={handleCancelEdit}>
</Button>
</Space.Compact>
);
}
return (
<div
style={{ cursor: 'pointer', display: 'flex', alignItems: 'center' }}
onClick={() => handleEdit(record.id, 'markup_rate', value * 100)}
>
<span>{(value * 100).toFixed(2)}%</span>
<EditOutlined style={{ marginLeft: 8, fontSize: 12, color: '#1890ff' }} />
</div>
);
},
},
{
title: '状态',
dataIndex: 'is_active',
key: 'is_active',
width: 80,
render: (value: boolean, record: VmSpecItem) => (
<Switch
checked={value}
onChange={(checked) => handleUpdateStatus(record.id, checked)}
/>
),
},
{
title: '操作',
key: 'action',
width: 100,
render: (_: any, record: VmSpecItem) => (
<Popconfirm
title="确定要删除这个规格吗?"
description="删除后该规格的所有配置将无法使用"
onConfirm={() => handleDeleteSpec(record.id)}
okText="确定"
cancelText="取消"
>
<Button
type="link"
danger
size="small"
icon={<DeleteOutlined />}
>
</Button>
</Popconfirm>
),
},
];
// 模板表格列
const templatesColumns = [
{
title: '配置类型',
dataIndex: 'spec_type',
key: 'spec_type',
width: 120,
},
{
title: '模板ID',
dataIndex: 'template_id',
key: 'template_id',
width: 200,
},
{
title: '默认模板',
dataIndex: 'is_default',
key: 'is_default',
width: 100,
render: (value: boolean, record: VmTemplateItem) => (
value ? (
<span style={{ color: '#52c41a' }}>
<CheckOutlined />
</span>
) : (
<Button
type="link"
size="small"
onClick={() => handleSetDefaultTemplate(record.id)}
>
</Button>
)
),
},
{
title: '操作',
key: 'action',
width: 120,
render: (_: any, record: VmTemplateItem) => (
<Popconfirm
title="确定要删除这个模板吗?"
onConfirm={() => handleDeleteTemplate(record.id)}
okText="确定"
cancelText="取消"
>
<Button
type="link"
danger
size="small"
icon={<DeleteOutlined />}
>
</Button>
</Popconfirm>
),
},
];
return (
<Card title={<Title level={4}></Title>}>
<Tabs activeKey={activeTab} onChange={setActiveTab}>
<TabPane tab="规格价格" key="specs">
<Space style={{ marginBottom: 16 }}>
<Form form={specsForm} onFinish={handleSpecsSearch} layout="inline">
<Form.Item name="spec_type">
<Input placeholder="配置类型" style={{ width: 150 }} />
</Form.Item>
<Form.Item name="status">
<Select placeholder="状态" allowClear style={{ width: 120 }}>
<Option value="active"></Option>
<Option value="inactive"></Option>
</Select>
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit" icon={<SearchOutlined />}>
</Button>
<Button onClick={handleSpecsReset} icon={<ReloadOutlined />}>
</Button>
</Space>
</Form.Item>
</Form>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setSpecModalVisible(true)}
>
</Button>
</Space>
<Table
dataSource={specsData}
columns={specsColumns}
rowKey="id"
loading={loading}
pagination={{
current: specsPage,
pageSize: specsPageSize,
total: specsTotal,
showSizeChanger: true,
showTotal: (total) => `${total}`,
onChange: (page, pageSize) => {
setSpecsPage(page);
setSpecsPageSize(pageSize || 10);
},
}}
/>
</TabPane>
<TabPane tab="模板配置" key="templates">
<Space style={{ marginBottom: 16 }}>
<Form form={templatesForm} onFinish={handleTemplatesSearch} layout="inline">
<Form.Item name="spec_type">
<Input placeholder="配置类型" style={{ width: 150 }} />
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit" icon={<SearchOutlined />}>
</Button>
<Button onClick={handleTemplatesReset} icon={<ReloadOutlined />}>
</Button>
</Space>
</Form.Item>
</Form>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setTemplateModalVisible(true)}
>
</Button>
</Space>
<Table
dataSource={templatesData}
columns={templatesColumns}
rowKey="id"
loading={loading}
pagination={{
current: templatesPage,
pageSize: templatesPageSize,
total: templatesTotal,
showSizeChanger: true,
showTotal: (total) => `${total}`,
onChange: (page, pageSize) => {
setTemplatesPage(page);
setTemplatesPageSize(pageSize || 10);
},
}}
/>
</TabPane>
</Tabs>
{/* 添加规格模态框 */}
<Modal
title="添加规格"
open={specModalVisible}
onCancel={() => {
setSpecModalVisible(false);
specForm.resetFields();
}}
footer={null}
>
<Form
form={specForm}
layout="vertical"
onFinish={handleCreateSpec}
>
<Form.Item
name="spec_type"
label="配置类型"
rules={[{ required: true, message: '请输入配置类型' }]}
>
<Input placeholder="如2c4g, 4c8g, 6c12g" />
</Form.Item>
<Form.Item
name="cpu_cores"
label="CPU核心数"
rules={[{ required: true, message: '请输入CPU核心数' }]}
>
<InputNumber min={1} style={{ width: '100%' }} placeholder="请输入CPU核心数" />
</Form.Item>
<Form.Item
name="memory_gb"
label="内存(GB)"
rules={[{ required: true, message: '请输入内存大小' }]}
>
<InputNumber min={1} style={{ width: '100%' }} placeholder="请输入内存大小(GB)" />
</Form.Item>
<Form.Item
name="description"
label="描述"
>
<Input placeholder="配置描述(可选)" />
</Form.Item>
<Form.Item
name="cost_price_per_minute"
label="成本价($/分钟)"
rules={[{ required: true, message: '请输入成本价' }]}
>
<InputNumber
min={0}
precision={8}
style={{ width: '100%' }}
placeholder="请输入成本价(美元/分钟)"
/>
</Form.Item>
<Form.Item
name="markup_rate"
label="加价率(%)"
initialValue={30}
>
<InputNumber
min={0}
max={100}
precision={2}
style={{ width: '100%' }}
placeholder="默认30%"
formatter={(value) => `${value}%`}
parser={(value) => value!.replace('%', '')}
/>
</Form.Item>
<Form.Item
name="is_active"
label="启用"
valuePropName="checked"
initialValue={true}
>
<Switch />
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit">
</Button>
<Button onClick={() => {
setSpecModalVisible(false);
specForm.resetFields();
}}>
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
{/* 添加模板模态框 */}
<Modal
title="添加模板"
open={templateModalVisible}
onCancel={() => {
setTemplateModalVisible(false);
templateForm.resetFields();
}}
footer={null}
>
<Form
form={templateForm}
layout="vertical"
onFinish={handleCreateTemplate}
>
<Form.Item
name="spec_type"
label="配置类型"
rules={[{ required: true, message: '请选择配置类型' }]}
>
<Select placeholder="请选择配置类型" showSearch>
{specTypes.map(type => (
<Option key={type} value={type}>{type}</Option>
))}
</Select>
</Form.Item>
<Form.Item
name="template_id"
label="模板ID"
rules={[{ required: true, message: '请输入模板ID' }]}
>
<Input placeholder="请输入E2B模板ID" />
</Form.Item>
<Form.Item
name="is_default"
label="设为默认模板"
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit">
</Button>
<Button onClick={() => {
setTemplateModalVisible(false);
templateForm.resetFields();
}}>
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
</Card>
);
};
export default VmPricing;