Соединение бек и фронт

This commit is contained in:
EkaterinaR 2024-11-19 23:24:52 +04:00
parent 942c028945
commit 5689c48c01
4 changed files with 182 additions and 164 deletions

View File

@ -8,12 +8,13 @@ import io
import joblib
from flask import Flask, request, jsonify, Blueprint, send_file
from flasgger import Swagger
from flask_cors import CORS
app = Flask(__name__)
api = Blueprint('api', __name__)
Swagger(app)
CORS(app)
# Загружаем модель и scaler
model = load_model("my_model_1H.keras")
scaler = MinMaxScaler(feature_range=(0, 1))

View File

@ -16,11 +16,13 @@ function HomePage() {
const [marketplaces, setMarketplaces] = useState([]);
const [productUrl, setProductUrl] = useState('');
const [showPriceHistoryError, setShowPriceHistoryError] = useState(false);
const [showResultError, setShowResultError] = useState(false);
const [selectedCategory, setSelectedCategory] = useState('');
useEffect(() => {
const fetchMarketplaces = async () => {
try {
const response = await axios.get('/api/v1/marketplaces');
const response = await axios.get('https://mgpj3mxm-8080.euw.devtunnels.ms/api/v1/marketplaces');
setMarketplaces(response.data);
} catch (error) {
console.error('Error fetching marketplaces:', error);
@ -34,7 +36,8 @@ function HomePage() {
const fetchCategories = async () => {
try {
if (selectedMarketplace) {
const response = await axios.get(`/api/v1/categories?marketplace=${selectedMarketplace}`);
console.log(selectedMarketplace)
const response = await axios.get(`https://mgpj3mxm-8080.euw.devtunnels.ms/api/v1/categories?marketplace=${selectedMarketplace}`);
setCategories(response.data);
}
} catch (error) {
@ -46,10 +49,17 @@ function HomePage() {
}, [selectedMarketplace]);
const handleSubmit = () => {
console.log('Отправлено:', startDate, endDate, selectedMarketplace);
navigate('/result', {
state: { startDate, endDate, selectedMarketplace }
});
if (selectedCategory !== "" || selectedMarketplace !== "") {
navigate('/result', {
state: { startDate, endDate, selectedMarketplace, selectedCategory }
});
} else {
setShowResultError(true);
}
};
const handleCategoryChange = (event) => {
setSelectedCategory(event.target.value);
};
const handleProductUrlChange = (event) => {
@ -64,7 +74,8 @@ function HomePage() {
// Проверка существования товара по ссылке
try {
const response = await fetch(`/api/v1/products/info?productUrl=${productUrl}`);
const response = await fetch(`https://mgpj3mxm-8080.euw.devtunnels.ms/api/v1/products/info?productUrl=${productUrl}`);
console.log(response.data);
if (response.ok) {
// Товар найден
navigate('/viewProduct', { state: { productUrl } });
@ -83,6 +94,7 @@ function HomePage() {
setSelectedMarketplace('');
} else {
setSelectedMarketplace(marketplaceName);
setSelectedCategory('');
}
};
@ -146,7 +158,7 @@ function HomePage() {
height: '90%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
justifyContent: 'center'
}}
>
<Typography variant="h1" gutterBottom style={{ color: '#132a52', marginBottom: '1.5rem', fontWeight: 'bold' }}>
@ -165,36 +177,41 @@ function HomePage() {
Выберите маркетплейсы, которые вас интересуют:
</Typography>
<Grid container spacing={2}>
{marketplaces.map((marketplace) => (
<Grid item key={marketplace.name}>
<Button
variant="contained"
color="primary"
className="marketplace-button"
style={{
backgroundColor: selectedMarketplace === marketplace.name ? marketplace.bgColor : '#fcfcf8',
color: selectedMarketplace === marketplace.name ? marketplace.textColor : '#16305e',
borderRadius: '0.5rem',
padding: '1rem',
display: 'flex',
flexDirection: 'column',
maxHeight: '150px',
alignItems: 'center',
minWidth: '400px',
}}
onClick={() => handleButtonClick(marketplace.name)}
>
{marketplace.name === 'Wildberries' ? (
<img src="https://png.klev.club/uploads/posts/2024-04/png-klev-club-dejs-p-wildberries-logotip-png-16.png" alt="Wildberries" style={{ width: '60px', height: '60px', marginBottom: '0.5rem' }} />
) : (
<img src="https://pngimg.com/d/ozon_PNG3.png" alt="Ozon" style={{ width: '60px', height: '60px', marginBottom: '0.5rem' }} />
)}
<Typography variant="h5" gutterBottom style={{ color: selectedMarketplace === marketplace.name ? marketplace.textColor : '#16305e', marginBottom: '0rem' }}>
{marketplace.name}
</Typography>
</Button>
</Grid>
))}
{marketplaces.map((marketplace) => {
if (marketplace === 'WILDBERRIES' || marketplace === 'OZON') {
return (
<Grid item key={marketplace.name}>
<Button
variant="contained"
color="primary"
className="marketplace-button"
style={{
backgroundColor: selectedMarketplace === marketplace ? marketplace.bgColor : '#fcfcf8',
color: selectedMarketplace === marketplace ? marketplace.textColor : '#16305e',
borderRadius: '0.5rem',
padding: '1rem',
display: 'flex',
flexDirection: 'column',
maxHeight: '150px',
alignItems: 'center',
minWidth: '400px',
}}
onClick={() => handleButtonClick(marketplace)}
>
{marketplace === 'WILDBERRIES' ? (
<img src="https://png.klev.club/uploads/posts/2024-04/png-klev-club-dejs-p-wildberries-logotip-png-16.png" alt="Wildberries" style={{ width: '60px', height: '60px', marginBottom: '0.5rem' }} />
) : (
<img src="https://pngimg.com/d/ozon_PNG3.png" alt="Ozon" style={{ width: '60px', height: '60px', marginBottom: '0.5rem' }} />
)}
<Typography variant="h5" gutterBottom style={{ color: selectedMarketplace === marketplace ? marketplace.textColor : '#16305e', marginBottom: '0rem' }}>
{marketplace}
</Typography>
</Button>
</Grid>
);
}
return null; // Для остальных названий, возвращаем null, чтобы не рендерить ничего
})}
</Grid>
<br></br>
{/* Комбобокс */}
@ -202,15 +219,14 @@ function HomePage() {
Выберите категорию:
</Typography>
<FormControl variant="outlined" style={{ minWidth: '97%' }}>
<Select
labelId="marketplace-select-label"
value={selectedMarketplace}
onChange={handleMarketplaceChange}
value={selectedCategory}
onChange={handleCategoryChange}
style={{
color: '#023247',
borderColor: '#4875b2',
borderRadius: '0.5rem'// Цвет текста выпадающего списка
borderRadius: '0.5rem', // Цвет текста выпадающего списка
}}
inputProps={{
style: {
@ -226,50 +242,15 @@ function HomePage() {
}}
>
{categories.map((category) => (
<MenuItem key={category.id} value={category.id}>
{category.name}
<MenuItem key={category} value={category} onChange={handleCategoryChange}>
{category}
</MenuItem>
))}
</Select>
<br></br>
</FormControl>
<br></br>
<Typography variant="h4" gutterBottom>
Введите период для сбора данных:
</Typography>
{startDateError && (
<Alert severity="error" style={{ marginBottom: '10px' }}>
Дата начала периода не может быть после сегодняшнего дня.
</Alert>
)}
{endDateError && (
<Alert severity="error" style={{ marginBottom: '10px' }}>
Дата окончания периода не может быть после сегодняшнего дня или раньше даты начала.
</Alert>
)}
<div className="date-pickers" style={{ display: 'flex', alignItems: 'center', marginTop: '0.5rem' }}>
<DatePicker
selected={startDate}
onChange={handleStartDateChange}
className="datePickerInput"
wrapperClassName="datePicker"
dateFormat="dd.MM.yyyy"
popperPlacement="bottom"
showMonthDropdown
/>
<span className="date-separator" style={{ margin: '0 0.5rem', color: '#2a8e9e', fontSize: '30px' }}> - </span>
<DatePicker
selected={endDate}
onChange={handleEndDateChange}
className="datePickerInput"
wrapperClassName="datePicker"
dateFormat="dd.MM.yyyy"
popperPlacement="bottom"
showMonthDropdown
/>
</div>
<Button
variant="contained"
color="primary"
@ -280,13 +261,18 @@ function HomePage() {
padding: '0.75rem 1.5rem',
backgroundColor: '#4875b2',
marginTop: '1.5rem',
minWidth: '97%'
minWidth: '97%', // Можно оставить или изменить на '100%'
}}
>
<Typography variant="h5" gutterBottom style={{ color: '#fcfcfb', marginBottom: '0rem' }}>
Получить рекомендации
</Typography>
</Button>
{showResultError && (
<Alert severity="error" style={{ width: '94%', marginTop: '1rem' }}>
Выберите маркетплейс и категорию
</Alert>
)}
</div>
</div>
</Grid>
@ -306,8 +292,11 @@ function HomePage() {
<div style={{
position: 'relative',
width: '97%',
height: '700px',
overflow: 'hidden'
height: 'auto', // Изменение на auto для высоты
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}>
<img
src="https://cdn.prod.website-files.com/61ebe5f773be1acd620f8208/61fb879dfccdca6a20c66d4a_e-commerce-marketplace.gif"
@ -315,31 +304,29 @@ function HomePage() {
className="main-image"
style={{
borderRadius: '0.5rem',
maxWidth: '80%',
maxWidth: '66%',
height: 'auto',
position: 'absolute',
top: '80%',
left: '50%',
transform: 'translate(-50%, -100%)'
}}
/>
<Typography variant="h4" style={{
color: '#132a52',
position: 'absolute',
top: '60%',
width: '80%',
}}>
Вы можете посмотреть историю изменения цены конкретного товара при помощи его URL.
</Typography>
<Typography variant="h4" style={{
color: '#132a52',
position: 'absolute',
top: '80%',
width: '80%'
}}>
Введите ссылку на товар:
</Typography>
</div>
<Typography variant="h4" style={{
color: '#132a52',
width: '80%',
textAlign: 'center',
}}>
Вы можете посмотреть историю изменения цены конкретного товара при помощи его URL.
</Typography>
<Typography variant="h4" style={{
color: '#132a52',
width: '80%',
textAlign: 'center',
marginTop: '1rem',
}}>
Введите ссылку на товар:
</Typography>
<TextField
label="Ссылка на товар"
placeholder="https://www.ozon.ru/..."
@ -348,32 +335,37 @@ function HomePage() {
value={productUrl}
onChange={handleProductUrlChange}
style={{
marginTop: '-3.5rem',
color: '#132a52',
borderColor: '#4875b2',
borderRadius: '1rem',
maxWidth: '97%'
marginTop: '1rem', // Теперь пространство между текстом и полем ввода
maxWidth: '97%',
}}
/>
<Button variant="contained" color="primary" onClick={handleViewPriceHistory}
<Button
variant="contained"
color="primary"
onClick={handleViewPriceHistory}
style={{
borderRadius: '0.5rem',
padding: '0.75rem 1.5rem',
backgroundColor: '#4875b2',
marginTop: '1.5rem',
minWidth: '97%'
}}>
}}
>
<Typography variant="h5" gutterBottom style={{ color: '#fcfcfb', marginBottom: '0rem' }}>
Посмотреть историю цены
</Typography>
</Button>
{showPriceHistoryError && (
<Alert severity="error" style={{ marginTop: '1rem' }}>
<Alert severity="error" style={{ width: '94%', marginTop: '1rem', textAlign: 'center' }}>
Неверный URL товара или товар не найден.
</Alert>
)}
</div>
</Grid>
</Grid>
</div>
</ThemeProvider>

View File

@ -1,28 +1,49 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import { Typography, Box, Grid, Container, Card, CardContent, CardHeader, Button } from '@mui/material';
import { Typography, Box, Grid, Container, Card, CardContent, CardHeader, Button, CircularProgress } from '@mui/material';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts';
const Result = () => {
const location = useLocation();
const navigate = useNavigate();
const { startDate, endDate, selectedMarketplace } = location.state || {};
const generateDates = (startDate, endDate) => {
const dates = [];
let currentDate = new Date(startDate);
while (currentDate <= endDate) {
dates.push(new Date(currentDate));
currentDate.setDate(currentDate.getDate() + 1);
}
return dates;
};
const data = generateDates(startDate, endDate).map((date, index) => ({
date: date,
price: Math.floor(Math.random() * 200) + 50
}));
const { startDate, endDate, selectedMarketplace, selectedCategory } = location.state || {};
const [predictData, setPredictData] = useState(null);
const [loading, setLoading] = useState(true);
const [imageUrl, setImageUrl] = useState('');
useEffect(() => {
const fetchPredictPrice = async () => {
try {
const response = await fetch('http://localhost:5000/api/predict_price');
if (response.ok) {
const data = await response.json();
console.log(data);
setPredictData(data);
} else {
console.error("Ошибка запроса к API");
}
} catch (error) {
console.error("Ошибка при получении данных:", error);
}
};
const fetchChart = async () => {
try {
const response = await fetch('http://localhost:5000/api/plot');
if (!response.ok) {
throw new Error('Network response was not ok');
}
const blob = await response.blob();
const url = URL.createObjectURL(blob);
setImageUrl(url);
} catch (error) {
console.error('Ошибка при загрузке графика:', error);
} finally {
setLoading(false);
}
};
fetchChart();
fetchPredictPrice();
}, []);
const theme = createTheme({
palette: {
@ -72,8 +93,8 @@ const Result = () => {
Выбранный маркетплейс: {selectedMarketplace}
</Typography>
<Typography variant="body1" gutterBottom style={{ color: '#023247' }}>
Период: {startDate?.toLocaleDateString()} по {endDate?.toLocaleDateString()}
</Typography>
Выбранная категория: {selectedCategory}
</Typography>
</CardContent>
</Card>
</Grid>
@ -81,31 +102,26 @@ const Result = () => {
<Grid item xs={12}>
<Card style={{ backgroundColor: '#fcfcf8', borderRadius: '1rem' }}>
<CardHeader title="Анализ" style={{ color: '#023247' }} />
<CardContent>
<LineChart width={600} height={300} data={data}>
<XAxis
dataKey="date"
tickFormatter={(unixTime) => new Date(unixTime).toLocaleDateString()}
tickMargin={10}
stroke="#023247"
/>
<YAxis stroke="#023247" />
<CartesianGrid stroke="#f5f5f5" />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="price" stroke="#2a8e9e" activeDot={{ r: 8 }} />
</LineChart>
</CardContent>
<CardContent>
{loading ? (
<CircularProgress />
) : (
<img src={imageUrl} alt="График предсказанных и фактических цен" style={{ width: '100%', borderRadius: '1rem' }} />
)}
</CardContent>
</Card>
</Grid>
<Grid item xs={12}>
<Card style={{ backgroundColor: '#fcfcf8', borderRadius: '1rem' }}>
<CardHeader title="Рекомендации" style={{ color: '#023247' }} />
<CardContent>
<Typography variant="body1" gutterBottom style={{ color: '#023247' }}>
Здесь будут отображаться рекомендации, основанные на анализе данных.
{predictData ? (
`Продукт лучше покупать в ${predictData.min_price_day.date}. Предсказанная цена: ${predictData.min_price_day.price} руб.`
) : (
"Загрузка данных..."
)}
</Typography>
</CardContent>
</Card>

View File

@ -31,16 +31,24 @@ const ViewProduct = () => {
const sign = zoneOffset >= 0 ? '+' : '-';
// Исправлено: использование шаблонных строк для форматирования часового пояса
const formattedZoneOffset = `${sign}${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
const { productUrl, from, to } = location.state || {};
// Определение 'to' как текущая дата
const to = date.toISOString().split('T')[0]; // Текущая дата в формате YYYY-MM-DD
const from = new Date(date);
from.setDate(date.getDate() - 30); // Дата минус 30 дней
const formattedFrom = from.toISOString().split('T')[0]; // Дата в формате YYYY-MM-DD
// Извлечение productUrl из состояния location
const { productUrl } = location.state || {};
const [productData, setProductData] = useState(null);
const [chartData, setChartData] = useState([]);
useEffect(() => {
const fetchProductData = async () => {
try {
const response = await fetch(`/api/v1/products/info?productUrl=${productUrl}`);
const response = await fetch(`https://mgpj3mxm-8080.euw.devtunnels.ms/api/v1/products/info?productUrl=${productUrl}`);
if (response.ok) {
const data = await response.json();
setProductData(data);
@ -54,14 +62,18 @@ const ViewProduct = () => {
const fetchPriceHistory = async () => {
try {
const response = await fetch(`/api/v1/products/price-history?productUrl=${productUrl}&from=${from}&to=${to}&zoneOffset=${formattedZoneOffset}`);
const response = await fetch(`https://mgpj3mxm-8080.euw.devtunnels.ms/api/v1/products/price-history?productUrl=${productUrl}&from=${formattedFrom}&to=${to}&zoneOffset=${formattedZoneOffset}`);
if (response.ok) {
const priceHistoryData = await response.json();
// Преобразование данных в нужный формат
const priceHistory = Object.entries(priceHistoryData.priceHistory).map(([date, price]) => ({
date: new Date(date),
date: new Date(date), // Преобразование строки даты в объект Date
price: price,
}));
setChartData(priceHistory);
// Сортировка по дате
const sortedPriceHistory = priceHistory.sort((a, b) => a.date - b.date);
setChartData(sortedPriceHistory);
} else {
console.error("Ошибка запроса к API");
}
@ -70,11 +82,10 @@ const ViewProduct = () => {
}
};
if (productUrl) {
fetchProductData();
fetchPriceHistory();
}
}, [productUrl, from, to, formattedZoneOffset]);
fetchProductData();
fetchPriceHistory();
}, [productUrl, formattedFrom, to, formattedZoneOffset]);
const theme = createTheme({
palette: {
@ -127,13 +138,10 @@ const ViewProduct = () => {
{productData.marketplaceName}
</Typography>
<Typography variant="body1" gutterBottom style={{ color: theme.palette.secondary.main }}>
<a href={productData.link} target="_blank" rel="noopener noreferrer" style={{ color: theme.palette.primary.main, textDecoration: 'underline' }}>
Ссылка на товар
<a href={productUrl} target="_blank" rel="noopener noreferrer" style={{ color: theme.palette.primary.main, textDecoration: 'underline' }}>
Перейти на страницу товара
</a>
</Typography>
<Typography variant="body1" gutterBottom style={{ color: theme.palette.secondary.main, fontWeight: 'bold' }}>
{productData.brand}
</Typography>
<Typography variant="body1" gutterBottom style={{ color: theme.palette.secondary.main, fontWeight: 'bold' }}>
{productData.productName}
</Typography>
@ -165,6 +173,7 @@ const ViewProduct = () => {
</CardContent>
</Card>
</Grid>
</Grid>
)}
</Container>