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

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 import joblib
from flask import Flask, request, jsonify, Blueprint, send_file from flask import Flask, request, jsonify, Blueprint, send_file
from flasgger import Swagger from flasgger import Swagger
from flask_cors import CORS
app = Flask(__name__) app = Flask(__name__)
api = Blueprint('api', __name__) api = Blueprint('api', __name__)
Swagger(app) Swagger(app)
CORS(app)
# Загружаем модель и scaler # Загружаем модель и scaler
model = load_model("my_model_1H.keras") model = load_model("my_model_1H.keras")
scaler = MinMaxScaler(feature_range=(0, 1)) scaler = MinMaxScaler(feature_range=(0, 1))

View File

@ -16,11 +16,13 @@ function HomePage() {
const [marketplaces, setMarketplaces] = useState([]); const [marketplaces, setMarketplaces] = useState([]);
const [productUrl, setProductUrl] = useState(''); const [productUrl, setProductUrl] = useState('');
const [showPriceHistoryError, setShowPriceHistoryError] = useState(false); const [showPriceHistoryError, setShowPriceHistoryError] = useState(false);
const [showResultError, setShowResultError] = useState(false);
const [selectedCategory, setSelectedCategory] = useState('');
useEffect(() => { useEffect(() => {
const fetchMarketplaces = async () => { const fetchMarketplaces = async () => {
try { 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); setMarketplaces(response.data);
} catch (error) { } catch (error) {
console.error('Error fetching marketplaces:', error); console.error('Error fetching marketplaces:', error);
@ -34,7 +36,8 @@ function HomePage() {
const fetchCategories = async () => { const fetchCategories = async () => {
try { try {
if (selectedMarketplace) { 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); setCategories(response.data);
} }
} catch (error) { } catch (error) {
@ -46,10 +49,17 @@ function HomePage() {
}, [selectedMarketplace]); }, [selectedMarketplace]);
const handleSubmit = () => { const handleSubmit = () => {
console.log('Отправлено:', startDate, endDate, selectedMarketplace); if (selectedCategory !== "" || selectedMarketplace !== "") {
navigate('/result', { navigate('/result', {
state: { startDate, endDate, selectedMarketplace } state: { startDate, endDate, selectedMarketplace, selectedCategory }
}); });
} else {
setShowResultError(true);
}
};
const handleCategoryChange = (event) => {
setSelectedCategory(event.target.value);
}; };
const handleProductUrlChange = (event) => { const handleProductUrlChange = (event) => {
@ -64,7 +74,8 @@ function HomePage() {
// Проверка существования товара по ссылке // Проверка существования товара по ссылке
try { 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) { if (response.ok) {
// Товар найден // Товар найден
navigate('/viewProduct', { state: { productUrl } }); navigate('/viewProduct', { state: { productUrl } });
@ -83,6 +94,7 @@ function HomePage() {
setSelectedMarketplace(''); setSelectedMarketplace('');
} else { } else {
setSelectedMarketplace(marketplaceName); setSelectedMarketplace(marketplaceName);
setSelectedCategory('');
} }
}; };
@ -146,7 +158,7 @@ function HomePage() {
height: '90%', height: '90%',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
justifyContent: 'center', justifyContent: 'center'
}} }}
> >
<Typography variant="h1" gutterBottom style={{ color: '#132a52', marginBottom: '1.5rem', fontWeight: 'bold' }}> <Typography variant="h1" gutterBottom style={{ color: '#132a52', marginBottom: '1.5rem', fontWeight: 'bold' }}>
@ -165,15 +177,17 @@ function HomePage() {
Выберите маркетплейсы, которые вас интересуют: Выберите маркетплейсы, которые вас интересуют:
</Typography> </Typography>
<Grid container spacing={2}> <Grid container spacing={2}>
{marketplaces.map((marketplace) => ( {marketplaces.map((marketplace) => {
if (marketplace === 'WILDBERRIES' || marketplace === 'OZON') {
return (
<Grid item key={marketplace.name}> <Grid item key={marketplace.name}>
<Button <Button
variant="contained" variant="contained"
color="primary" color="primary"
className="marketplace-button" className="marketplace-button"
style={{ style={{
backgroundColor: selectedMarketplace === marketplace.name ? marketplace.bgColor : '#fcfcf8', backgroundColor: selectedMarketplace === marketplace ? marketplace.bgColor : '#fcfcf8',
color: selectedMarketplace === marketplace.name ? marketplace.textColor : '#16305e', color: selectedMarketplace === marketplace ? marketplace.textColor : '#16305e',
borderRadius: '0.5rem', borderRadius: '0.5rem',
padding: '1rem', padding: '1rem',
display: 'flex', display: 'flex',
@ -182,19 +196,22 @@ function HomePage() {
alignItems: 'center', alignItems: 'center',
minWidth: '400px', minWidth: '400px',
}} }}
onClick={() => handleButtonClick(marketplace.name)} onClick={() => handleButtonClick(marketplace)}
> >
{marketplace.name === 'Wildberries' ? ( {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://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' }} /> <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' }}> <Typography variant="h5" gutterBottom style={{ color: selectedMarketplace === marketplace ? marketplace.textColor : '#16305e', marginBottom: '0rem' }}>
{marketplace.name} {marketplace}
</Typography> </Typography>
</Button> </Button>
</Grid> </Grid>
))} );
}
return null; // Для остальных названий, возвращаем null, чтобы не рендерить ничего
})}
</Grid> </Grid>
<br></br> <br></br>
{/* Комбобокс */} {/* Комбобокс */}
@ -202,15 +219,14 @@ function HomePage() {
Выберите категорию: Выберите категорию:
</Typography> </Typography>
<FormControl variant="outlined" style={{ minWidth: '97%' }}> <FormControl variant="outlined" style={{ minWidth: '97%' }}>
<Select <Select
labelId="marketplace-select-label" labelId="marketplace-select-label"
value={selectedMarketplace} value={selectedCategory}
onChange={handleMarketplaceChange} onChange={handleCategoryChange}
style={{ style={{
color: '#023247', color: '#023247',
borderColor: '#4875b2', borderColor: '#4875b2',
borderRadius: '0.5rem'// Цвет текста выпадающего списка borderRadius: '0.5rem', // Цвет текста выпадающего списка
}} }}
inputProps={{ inputProps={{
style: { style: {
@ -226,50 +242,15 @@ function HomePage() {
}} }}
> >
{categories.map((category) => ( {categories.map((category) => (
<MenuItem key={category.id} value={category.id}> <MenuItem key={category} value={category} onChange={handleCategoryChange}>
{category.name} {category}
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
<br></br> <br></br>
</FormControl> </FormControl>
<br></br> <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 <Button
variant="contained" variant="contained"
color="primary" color="primary"
@ -280,13 +261,18 @@ function HomePage() {
padding: '0.75rem 1.5rem', padding: '0.75rem 1.5rem',
backgroundColor: '#4875b2', backgroundColor: '#4875b2',
marginTop: '1.5rem', marginTop: '1.5rem',
minWidth: '97%' minWidth: '97%', // Можно оставить или изменить на '100%'
}} }}
> >
<Typography variant="h5" gutterBottom style={{ color: '#fcfcfb', marginBottom: '0rem' }}> <Typography variant="h5" gutterBottom style={{ color: '#fcfcfb', marginBottom: '0rem' }}>
Получить рекомендации Получить рекомендации
</Typography> </Typography>
</Button> </Button>
{showResultError && (
<Alert severity="error" style={{ width: '94%', marginTop: '1rem' }}>
Выберите маркетплейс и категорию
</Alert>
)}
</div> </div>
</div> </div>
</Grid> </Grid>
@ -306,8 +292,11 @@ function HomePage() {
<div style={{ <div style={{
position: 'relative', position: 'relative',
width: '97%', width: '97%',
height: '700px', height: 'auto', // Изменение на auto для высоты
overflow: 'hidden' overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}> }}>
<img <img
src="https://cdn.prod.website-files.com/61ebe5f773be1acd620f8208/61fb879dfccdca6a20c66d4a_e-commerce-marketplace.gif" src="https://cdn.prod.website-files.com/61ebe5f773be1acd620f8208/61fb879dfccdca6a20c66d4a_e-commerce-marketplace.gif"
@ -315,31 +304,29 @@ function HomePage() {
className="main-image" className="main-image"
style={{ style={{
borderRadius: '0.5rem', borderRadius: '0.5rem',
maxWidth: '80%', maxWidth: '66%',
height: 'auto', height: 'auto',
position: 'absolute',
top: '80%',
left: '50%',
transform: 'translate(-50%, -100%)'
}} }}
/> />
</div>
<Typography variant="h4" style={{ <Typography variant="h4" style={{
color: '#132a52', color: '#132a52',
position: 'absolute',
top: '60%',
width: '80%', width: '80%',
textAlign: 'center',
}}> }}>
Вы можете посмотреть историю изменения цены конкретного товара при помощи его URL. Вы можете посмотреть историю изменения цены конкретного товара при помощи его URL.
</Typography> </Typography>
<Typography variant="h4" style={{ <Typography variant="h4" style={{
color: '#132a52', color: '#132a52',
position: 'absolute', width: '80%',
top: '80%', textAlign: 'center',
width: '80%' marginTop: '1rem',
}}> }}>
Введите ссылку на товар: Введите ссылку на товар:
</Typography> </Typography>
</div>
<TextField <TextField
label="Ссылка на товар" label="Ссылка на товар"
placeholder="https://www.ozon.ru/..." placeholder="https://www.ozon.ru/..."
@ -348,32 +335,37 @@ function HomePage() {
value={productUrl} value={productUrl}
onChange={handleProductUrlChange} onChange={handleProductUrlChange}
style={{ style={{
marginTop: '-3.5rem', marginTop: '1rem', // Теперь пространство между текстом и полем ввода
color: '#132a52', maxWidth: '97%',
borderColor: '#4875b2',
borderRadius: '1rem',
maxWidth: '97%'
}} }}
/> />
<Button variant="contained" color="primary" onClick={handleViewPriceHistory}
<Button
variant="contained"
color="primary"
onClick={handleViewPriceHistory}
style={{ style={{
borderRadius: '0.5rem', borderRadius: '0.5rem',
padding: '0.75rem 1.5rem', padding: '0.75rem 1.5rem',
backgroundColor: '#4875b2', backgroundColor: '#4875b2',
marginTop: '1.5rem', marginTop: '1.5rem',
minWidth: '97%' minWidth: '97%'
}}> }}
>
<Typography variant="h5" gutterBottom style={{ color: '#fcfcfb', marginBottom: '0rem' }}> <Typography variant="h5" gutterBottom style={{ color: '#fcfcfb', marginBottom: '0rem' }}>
Посмотреть историю цены Посмотреть историю цены
</Typography> </Typography>
</Button> </Button>
{showPriceHistoryError && ( {showPriceHistoryError && (
<Alert severity="error" style={{ marginTop: '1rem' }}> <Alert severity="error" style={{ width: '94%', marginTop: '1rem', textAlign: 'center' }}>
Неверный URL товара или товар не найден. Неверный URL товара или товар не найден.
</Alert> </Alert>
)} )}
</div> </div>
</Grid> </Grid>
</Grid> </Grid>
</div> </div>
</ThemeProvider> </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 { useLocation, useNavigate } from 'react-router-dom';
import { ThemeProvider, createTheme } from '@mui/material/styles'; 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'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts';
const Result = () => { const Result = () => {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const { startDate, endDate, selectedMarketplace } = location.state || {}; const { startDate, endDate, selectedMarketplace, selectedCategory } = location.state || {};
const [predictData, setPredictData] = useState(null);
const generateDates = (startDate, endDate) => { const [loading, setLoading] = useState(true);
const dates = []; const [imageUrl, setImageUrl] = useState('');
let currentDate = new Date(startDate); useEffect(() => {
while (currentDate <= endDate) { const fetchPredictPrice = async () => {
dates.push(new Date(currentDate)); try {
currentDate.setDate(currentDate.getDate() + 1); 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);
} }
return dates;
}; };
const fetchChart = async () => {
const data = generateDates(startDate, endDate).map((date, index) => ({ try {
date: date, const response = await fetch('http://localhost:5000/api/plot');
price: Math.floor(Math.random() * 200) + 50 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({ const theme = createTheme({
palette: { palette: {
@ -72,7 +93,7 @@ const Result = () => {
Выбранный маркетплейс: {selectedMarketplace} Выбранный маркетплейс: {selectedMarketplace}
</Typography> </Typography>
<Typography variant="body1" gutterBottom style={{ color: '#023247' }}> <Typography variant="body1" gutterBottom style={{ color: '#023247' }}>
Период: {startDate?.toLocaleDateString()} по {endDate?.toLocaleDateString()} Выбранная категория: {selectedCategory}
</Typography> </Typography>
</CardContent> </CardContent>
</Card> </Card>
@ -82,19 +103,11 @@ const Result = () => {
<Card style={{ backgroundColor: '#fcfcf8', borderRadius: '1rem' }}> <Card style={{ backgroundColor: '#fcfcf8', borderRadius: '1rem' }}>
<CardHeader title="Анализ" style={{ color: '#023247' }} /> <CardHeader title="Анализ" style={{ color: '#023247' }} />
<CardContent> <CardContent>
<LineChart width={600} height={300} data={data}> {loading ? (
<XAxis <CircularProgress />
dataKey="date" ) : (
tickFormatter={(unixTime) => new Date(unixTime).toLocaleDateString()} <img src={imageUrl} alt="График предсказанных и фактических цен" style={{ width: '100%', borderRadius: '1rem' }} />
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>
</Card> </Card>
</Grid> </Grid>
@ -102,10 +115,13 @@ const Result = () => {
<Grid item xs={12}> <Grid item xs={12}>
<Card style={{ backgroundColor: '#fcfcf8', borderRadius: '1rem' }}> <Card style={{ backgroundColor: '#fcfcf8', borderRadius: '1rem' }}>
<CardHeader title="Рекомендации" style={{ color: '#023247' }} /> <CardHeader title="Рекомендации" style={{ color: '#023247' }} />
<CardContent> <CardContent>
<Typography variant="body1" gutterBottom style={{ color: '#023247' }}> <Typography variant="body1" gutterBottom style={{ color: '#023247' }}>
Здесь будут отображаться рекомендации, основанные на анализе данных. {predictData ? (
`Продукт лучше покупать в ${predictData.min_price_day.date}. Предсказанная цена: ${predictData.min_price_day.price} руб.`
) : (
"Загрузка данных..."
)}
</Typography> </Typography>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -31,16 +31,24 @@ const ViewProduct = () => {
const sign = zoneOffset >= 0 ? '+' : '-'; const sign = zoneOffset >= 0 ? '+' : '-';
// Исправлено: использование шаблонных строк для форматирования часового пояса
const formattedZoneOffset = `${sign}${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '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 [productData, setProductData] = useState(null);
const [chartData, setChartData] = useState([]); const [chartData, setChartData] = useState([]);
useEffect(() => { useEffect(() => {
const fetchProductData = async () => { const fetchProductData = async () => {
try { 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) { if (response.ok) {
const data = await response.json(); const data = await response.json();
setProductData(data); setProductData(data);
@ -54,14 +62,18 @@ const ViewProduct = () => {
const fetchPriceHistory = async () => { const fetchPriceHistory = async () => {
try { 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) { if (response.ok) {
const priceHistoryData = await response.json(); const priceHistoryData = await response.json();
// Преобразование данных в нужный формат
const priceHistory = Object.entries(priceHistoryData.priceHistory).map(([date, price]) => ({ const priceHistory = Object.entries(priceHistoryData.priceHistory).map(([date, price]) => ({
date: new Date(date), date: new Date(date), // Преобразование строки даты в объект Date
price: price, price: price,
})); }));
setChartData(priceHistory);
// Сортировка по дате
const sortedPriceHistory = priceHistory.sort((a, b) => a.date - b.date);
setChartData(sortedPriceHistory);
} else { } else {
console.error("Ошибка запроса к API"); console.error("Ошибка запроса к API");
} }
@ -70,11 +82,10 @@ const ViewProduct = () => {
} }
}; };
if (productUrl) {
fetchProductData(); fetchProductData();
fetchPriceHistory(); fetchPriceHistory();
} }, [productUrl, formattedFrom, to, formattedZoneOffset]);
}, [productUrl, from, to, formattedZoneOffset]);
const theme = createTheme({ const theme = createTheme({
palette: { palette: {
@ -127,13 +138,10 @@ const ViewProduct = () => {
{productData.marketplaceName} {productData.marketplaceName}
</Typography> </Typography>
<Typography variant="body1" gutterBottom style={{ color: theme.palette.secondary.main }}> <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> </a>
</Typography> </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' }}> <Typography variant="body1" gutterBottom style={{ color: theme.palette.secondary.main, fontWeight: 'bold' }}>
{productData.productName} {productData.productName}
</Typography> </Typography>
@ -165,6 +173,7 @@ const ViewProduct = () => {
</CardContent> </CardContent>
</Card> </Card>
</Grid> </Grid>
</Grid> </Grid>
)} )}
</Container> </Container>