add: marketplace

This commit is contained in:
2025-07-19 04:40:46 +05:00
parent c39a8bc43c
commit 6ee1b67315
6 changed files with 1081 additions and 39 deletions

View File

@ -0,0 +1,355 @@
// src/renderer/components/PlayerInventory.tsx
import { useEffect, useState } from 'react';
import {
Box,
Typography,
Grid,
Card,
CardMedia,
CardContent,
Button,
CircularProgress,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TextField,
Alert,
} from '@mui/material';
import {
RequestPlayerInventory,
getPlayerInventory,
sellItem,
PlayerInventoryItem,
} from '../api';
interface PlayerInventoryProps {
username: string;
serverIp: string;
onSellSuccess?: () => void; // Callback для обновления маркетплейса после продажи
}
export default function PlayerInventory({
username,
serverIp,
onSellSuccess,
}: PlayerInventoryProps) {
const [loading, setLoading] = useState<boolean>(false);
const [inventoryItems, setInventoryItems] = useState<PlayerInventoryItem[]>(
[],
);
const [error, setError] = useState<string | null>(null);
const [sellDialogOpen, setSellDialogOpen] = useState<boolean>(false);
const [selectedItem, setSelectedItem] = useState<PlayerInventoryItem | null>(
null,
);
const [price, setPrice] = useState<number>(0);
const [amount, setAmount] = useState<number>(1);
const [sellLoading, setSellLoading] = useState<boolean>(false);
const [sellError, setSellError] = useState<string | null>(null);
// Функция для запроса инвентаря игрока
const fetchPlayerInventory = async () => {
try {
setLoading(true);
setError(null);
// Сначала делаем запрос на получение идентификатора запроса инвентаря
const inventoryRequest = await RequestPlayerInventory(serverIp, username);
const requestId = inventoryRequest.request_id;
// Затем начинаем опрашивать API для получения результата
let inventoryData = null;
let attempts = 0;
const maxAttempts = 10; // Максимальное количество попыток
while (!inventoryData && attempts < maxAttempts) {
attempts++;
try {
// Пауза перед следующим запросом
await new Promise((resolve) => setTimeout(resolve, 1000));
// Запрашиваем состояние инвентаря
const response = await getPlayerInventory(requestId);
// Если инвентарь загружен, сохраняем его
if (response.status === 'completed') {
inventoryData = response.result.inventory_data;
break;
}
} catch (e) {
console.log('Ожидание завершения запроса инвентаря...');
}
}
if (inventoryData) {
setInventoryItems(inventoryData);
} else {
setError('Не удалось получить инвентарь. Попробуйте еще раз.');
}
} catch (e) {
console.error('Ошибка при получении инвентаря:', e);
setError('Произошла ошибка при загрузке инвентаря.');
} finally {
setLoading(false);
}
};
// Открываем диалог для продажи предмета
const handleOpenSellDialog = (item: PlayerInventoryItem) => {
setSelectedItem(item);
setAmount(1);
setPrice(0);
setSellError(null);
setSellDialogOpen(true);
};
// Закрываем диалог
const handleCloseSellDialog = () => {
setSellDialogOpen(false);
setSelectedItem(null);
};
// Выставляем предмет на продажу
const handleSellItem = async () => {
if (!selectedItem) return;
try {
setSellLoading(true);
setSellError(null);
// Проверяем валидность введенных данных
if (price <= 0) {
setSellError('Цена должна быть больше 0');
return;
}
if (amount <= 0 || amount > selectedItem.amount) {
setSellError(`Количество должно быть от 1 до ${selectedItem.amount}`);
return;
}
// Отправляем запрос на продажу
const result = await sellItem(
username,
selectedItem.slot,
amount,
price,
serverIp,
);
// Проверяем статус операции
if (result.status === 'pending') {
// Закрываем диалог и обновляем инвентарь
handleCloseSellDialog();
// Показываем уведомление о том, что операция обрабатывается
// setNotification({ // Assuming setNotification is available in the context
// open: true,
// message: 'Предмет выставляется на продажу. Это может занять некоторое время.',
// type: 'info'
// });
// Через 5 секунд обновляем инвентарь
setTimeout(() => {
fetchPlayerInventory();
// Вызываем callback для обновления маркетплейса
if (onSellSuccess) {
onSellSuccess();
}
}, 5000);
}
} catch (e) {
console.error('Ошибка при продаже предмета:', e);
setSellError('Произошла ошибка при продаже предмета.');
} finally {
setSellLoading(false);
}
};
// Загружаем инвентарь при монтировании компонента
useEffect(() => {
fetchPlayerInventory();
}, [username, serverIp]);
// Получаем отображаемое имя предмета
const getItemDisplayName = (material: string) => {
return material
.replace(/_/g, ' ')
.toLowerCase()
.replace(/\b\w/g, (l) => l.toUpperCase());
};
return (
<Box sx={{ mt: 4 }}>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: 2,
}}
>
<Typography variant="h5" color="white">
Ваш инвентарь
</Typography>
<Button
variant="outlined"
onClick={fetchPlayerInventory}
disabled={loading}
>
Обновить
</Button>
</Box>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
{loading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', my: 4 }}>
<CircularProgress />
</Box>
) : (
<>
{inventoryItems.length === 0 ? (
<Typography
variant="body1"
color="white"
sx={{ textAlign: 'center', my: 4 }}
>
Ваш инвентарь пуст или не удалось загрузить предметы.
</Typography>
) : (
<Grid container spacing={2}>
{inventoryItems.map((item) =>
item.material !== 'AIR' && item.amount > 0 ? (
<Grid item xs={6} sm={4} md={3} lg={2} key={item.slot}>
<Card
sx={{
bgcolor: 'rgba(255, 255, 255, 0.05)',
cursor: 'pointer',
transition: 'transform 0.2s',
'&:hover': { transform: 'scale(1.03)' },
}}
onClick={() => handleOpenSellDialog(item)}
>
<CardMedia
component="img"
sx={{
height: 100,
objectFit: 'contain',
bgcolor: 'rgba(0, 0, 0, 0.2)',
p: 1,
imageRendering: 'pixelated',
}}
image={`/minecraft/${item.material.toLowerCase()}.png`}
alt={item.material}
/>
<CardContent sx={{ p: 1 }}>
<Typography variant="body2" color="white" noWrap>
{getItemDisplayName(item.material)}
</Typography>
<Typography variant="body2" color="white">
x{item.amount}
</Typography>
{Object.keys(item.enchants || {}).length > 0 && (
<Typography
variant="caption"
color="secondary"
sx={{ display: 'block' }}
>
Зачарования: {Object.keys(item.enchants).length}
</Typography>
)}
</CardContent>
</Card>
</Grid>
) : null,
)}
</Grid>
)}
</>
)}
{/* Диалог для продажи предмета */}
<Dialog open={sellDialogOpen} onClose={handleCloseSellDialog}>
<DialogTitle>Продать предмет</DialogTitle>
<DialogContent>
{selectedItem && (
<Box sx={{ mt: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<CardMedia
component="img"
sx={{
width: 50,
height: 50,
objectFit: 'contain',
mr: 2,
}}
image={`/items/${selectedItem.material.toLowerCase()}.png`}
alt={selectedItem.material}
/>
<Typography variant="h6">
{getItemDisplayName(selectedItem.material)}
</Typography>
</Box>
<Typography variant="body2" gutterBottom>
Всего доступно: {selectedItem.amount}
</Typography>
<TextField
label="Количество"
type="number"
fullWidth
margin="dense"
value={amount}
onChange={(e) =>
setAmount(
Math.min(
parseInt(e.target.value) || 0,
selectedItem.amount,
),
)
}
inputProps={{ min: 1, max: selectedItem.amount }}
/>
<TextField
label="Цена (за всё)"
type="number"
fullWidth
margin="dense"
value={price}
onChange={(e) => setPrice(parseInt(e.target.value) || 0)}
inputProps={{ min: 1 }}
/>
{sellError && (
<Alert severity="error" sx={{ mt: 2 }}>
{sellError}
</Alert>
)}
</Box>
)}
</DialogContent>
<DialogActions>
<Button onClick={handleCloseSellDialog}>Отмена</Button>
<Button
onClick={handleSellItem}
variant="contained"
color="primary"
disabled={sellLoading}
>
{sellLoading ? <CircularProgress size={24} /> : 'Продать'}
</Button>
</DialogActions>
</Dialog>
</Box>
);
}