feat():learning后台管理前端页面初始化

This commit is contained in:
yuj
2025-12-04 17:51:24 +08:00
commit 83a614bd75
97 changed files with 23324 additions and 0 deletions

732
src/pages/VmPricing.tsx Normal file
View File

@@ -0,0 +1,732 @@
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;