Init commit
This commit is contained in:
parent
88606c03bc
commit
459e2b1dea
|
@ -0,0 +1,32 @@
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Test reports
|
||||||
|
htmlcov/
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
|
||||||
|
# Alembic temp
|
||||||
|
alembic/versions/*.py
|
|
@ -0,0 +1,50 @@
|
||||||
|
# Backend for Glass Cutting Optimization
|
||||||
|
|
||||||
|
This directory contains the FastAPI backend application.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. **Create a virtual environment:**
|
||||||
|
```bash
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate # On Windows use `venv\Scripts\activate`
|
||||||
|
```
|
||||||
|
2. **Install dependencies:**
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
3. **Configure environment variables:**
|
||||||
|
Create a `.env` file in this directory based on `.env.example` (you'll create this later).
|
||||||
|
4. **Run database migrations (using Alembic):**
|
||||||
|
```bash
|
||||||
|
# Initialize alembic (only once)
|
||||||
|
# alembic init alembic
|
||||||
|
|
||||||
|
# Edit alembic.ini and alembic/env.py to point to your models and database URL
|
||||||
|
# ...
|
||||||
|
|
||||||
|
# Create a migration
|
||||||
|
# alembic revision --autogenerate -m "Initial migration"
|
||||||
|
|
||||||
|
# Apply the migration
|
||||||
|
# alembic upgrade head
|
||||||
|
```
|
||||||
|
5. **Run the development server:**
|
||||||
|
```bash
|
||||||
|
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
* `app/`: Main application code
|
||||||
|
* `core/`: Configuration, settings, dependencies
|
||||||
|
* `db/`: Database session management
|
||||||
|
* `models/`: SQLAlchemy models
|
||||||
|
* `schemas/`: Pydantic schemas
|
||||||
|
* `routers/`: API route handlers
|
||||||
|
* `services/`: Business logic
|
||||||
|
* `alembic/`: Database migration scripts
|
||||||
|
* `tests/`: Application tests
|
||||||
|
* `.env`: Environment variables (local)
|
||||||
|
* `alembic.ini`: Alembic configuration
|
||||||
|
* `requirements.txt`: Python dependencies
|
|
@ -0,0 +1,58 @@
|
||||||
|
# A generic, single database configuration.
|
||||||
|
|
||||||
|
[alembic]
|
||||||
|
# path to migration scripts
|
||||||
|
script_location = alembic
|
||||||
|
|
||||||
|
# template for migration file names, e.g. "%%(rev)s_%%(slug)s.py"
|
||||||
|
# file_template = %%(rev)s_%%(slug)s
|
||||||
|
|
||||||
|
# timezone to use when rendering the date within the migration file
|
||||||
|
# as well as the filename.
|
||||||
|
# If specified, requires the python-dateutil library.
|
||||||
|
# Defaults to None if not specified.
|
||||||
|
# timezone =
|
||||||
|
|
||||||
|
# sys.path path, will be prepended to sys.path if present.
|
||||||
|
# defaults to the current working directory.
|
||||||
|
# prepend_sys_path = .
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARN
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARN
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
||||||
|
|
||||||
|
# Database connection (placeholder - needs to be configured)
|
||||||
|
# sqlalchemy.url = driver://user:pass@localhost/dbname
|
||||||
|
# sqlalchemy.url = sqlite:///./test.db
|
||||||
|
sqlalchemy.url = ${DATABASE_URL}
|
|
@ -0,0 +1 @@
|
||||||
|
Generic single-database configuration.
|
|
@ -0,0 +1,93 @@
|
||||||
|
from logging.config import fileConfig
|
||||||
|
|
||||||
|
from sqlalchemy import engine_from_config
|
||||||
|
from sqlalchemy import pool
|
||||||
|
|
||||||
|
from alembic import context
|
||||||
|
|
||||||
|
# Добавляем импорты для доступа к моделям и переменным окружения
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Загружаем переменные окружения из .env файла
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Добавляем путь к корню проекта, чтобы можно было импортировать модули
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
from app.models import Base, Calculation
|
||||||
|
|
||||||
|
# Получаем URL базы данных из переменной окружения
|
||||||
|
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///glass_cutting.db")
|
||||||
|
|
||||||
|
# this is the Alembic Config object, which provides
|
||||||
|
# access to the values within the .ini file in use.
|
||||||
|
config = context.config
|
||||||
|
|
||||||
|
# Interpret the config file for Python logging.
|
||||||
|
# This line sets up loggers basically.
|
||||||
|
if config.config_file_name is not None:
|
||||||
|
fileConfig(config.config_file_name)
|
||||||
|
|
||||||
|
# Используем метаданные из наших моделей
|
||||||
|
target_metadata = Base.metadata
|
||||||
|
|
||||||
|
# other values from the config, defined by the needs of env.py,
|
||||||
|
# can be acquired:
|
||||||
|
# my_important_option = config.get_main_option("my_important_option")
|
||||||
|
# ... etc.
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_offline() -> None:
|
||||||
|
"""Run migrations in 'offline' mode.
|
||||||
|
|
||||||
|
This configures the context with just a URL
|
||||||
|
and not an Engine, though an Engine is acceptable
|
||||||
|
here as well. By skipping the Engine creation
|
||||||
|
we don't even need a DBAPI to be available.
|
||||||
|
|
||||||
|
Calls to context.execute() here emit the given string to the
|
||||||
|
script output.
|
||||||
|
|
||||||
|
"""
|
||||||
|
url = DATABASE_URL
|
||||||
|
context.configure(
|
||||||
|
url=url,
|
||||||
|
target_metadata=target_metadata,
|
||||||
|
literal_binds=True,
|
||||||
|
dialect_opts={"paramstyle": "named"},
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_online() -> None:
|
||||||
|
"""Run migrations in 'online' mode.
|
||||||
|
|
||||||
|
In this scenario we need to create an Engine
|
||||||
|
and associate a connection with the context.
|
||||||
|
|
||||||
|
"""
|
||||||
|
configuration = config.get_section(config.config_ini_section)
|
||||||
|
configuration["sqlalchemy.url"] = DATABASE_URL
|
||||||
|
connectable = engine_from_config(
|
||||||
|
configuration,
|
||||||
|
prefix="sqlalchemy.",
|
||||||
|
poolclass=pool.NullPool,
|
||||||
|
)
|
||||||
|
|
||||||
|
with connectable.connect() as connection:
|
||||||
|
context.configure(
|
||||||
|
connection=connection,
|
||||||
|
target_metadata=target_metadata
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
run_migrations_offline()
|
||||||
|
else:
|
||||||
|
run_migrations_online()
|
|
@ -0,0 +1,28 @@
|
||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision | comma,n}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
${imports if imports else ""}
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = ${repr(up_revision)}
|
||||||
|
down_revision: Union[str, None] = ${repr(down_revision)}
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||||
|
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema."""
|
||||||
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
${downgrades if downgrades else "pass"}
|
|
@ -0,0 +1,28 @@
|
||||||
|
import os
|
||||||
|
from pydantic_settings import BaseSettings
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Load .env file from the backend directory (one level up from core)
|
||||||
|
# BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
# load_dotenv(os.path.join(BASE_DIR, '..', '.env')) # Adjust path if needed
|
||||||
|
|
||||||
|
# Or simply let pydantic-settings handle it by default if .env is in the root execution dir
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
DATABASE_URL: str = "sqlite:///./default.db" # Default value if not in .env
|
||||||
|
# Настройки JWT удалены
|
||||||
|
# SECRET_KEY: str = "default_secret"
|
||||||
|
# ALGORITHM: str = "HS256"
|
||||||
|
# ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
env_file = ".env"
|
||||||
|
env_file_encoding = 'utf-8'
|
||||||
|
# If your .env file is not in the root directory where you run uvicorn,
|
||||||
|
# you might need to specify the path explicitly:
|
||||||
|
# env_file = '../.env' # Example if running from inside 'app' dir
|
||||||
|
|
||||||
|
|
||||||
|
settings = Settings()
|
|
@ -0,0 +1,21 @@
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
# Create SQLAlchemy engine
|
||||||
|
engine = create_engine(
|
||||||
|
settings.DATABASE_URL,
|
||||||
|
# Required for SQLite to prevent issues with FastAPI's async nature
|
||||||
|
connect_args={"check_same_thread": False} if "sqlite" in settings.DATABASE_URL else {}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a session factory
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
# Dependency to get DB session (we'll use this in routers)
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
|
@ -0,0 +1,35 @@
|
||||||
|
import asyncio
|
||||||
|
import random
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
# Делаем функцию асинхронной
|
||||||
|
async def solve_cutting_problem(input_params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Асинхронная заглушка для имитации сложного расчета раскройки стекла.
|
||||||
|
Возвращает предопределенный или слегка измененный JSON-ответ.
|
||||||
|
"""
|
||||||
|
print(f"Received input for async fuzzy solver: {input_params}")
|
||||||
|
|
||||||
|
# Имитация времени расчета с использованием asyncio.sleep
|
||||||
|
delay = random.uniform(0.5, 2.0)
|
||||||
|
print(f"Simulating calculation for {delay:.2f} seconds...")
|
||||||
|
await asyncio.sleep(delay) # Используем await asyncio.sleep вместо time.sleep
|
||||||
|
|
||||||
|
# Пример выходных данных (остается таким же)
|
||||||
|
output_data = {
|
||||||
|
"layout": [
|
||||||
|
{"piece_id": 1, "x": 10, "y": 10, "width": 500, "height": 300},
|
||||||
|
{"piece_id": 2, "x": 520, "y": 10, "width": 400, "height": 300},
|
||||||
|
# ... другие детали раскроя ...
|
||||||
|
],
|
||||||
|
"waste_percentage": round(random.uniform(5.0, 15.0), 2),
|
||||||
|
"number_of_cuts": random.randint(5, 20),
|
||||||
|
"processing_time_ms": int(delay * 1000) # Используем задержку как время обработки
|
||||||
|
}
|
||||||
|
|
||||||
|
# Можно добавить логику на основе input_params, если нужно для тестов
|
||||||
|
if input_params.get("optimize_for") == "speed":
|
||||||
|
output_data["number_of_cuts"] = random.randint(15, 25) # Больше резов - быстрее?
|
||||||
|
|
||||||
|
print(f"Async fuzzy solver generated output: {output_data}")
|
||||||
|
return output_data
|
|
@ -0,0 +1,37 @@
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware # Импортируем middleware
|
||||||
|
from app.routers import calculation # Импортируем роутер
|
||||||
|
|
||||||
|
app = FastAPI(title="Glass Cutting Optimization API")
|
||||||
|
|
||||||
|
# --- Настройка CORS ---
|
||||||
|
# Список разрешенных источников (origins)
|
||||||
|
# Для разработки можно разрешить все или указать адрес вашего React-приложения
|
||||||
|
origins = [
|
||||||
|
"http://localhost", # Если React запускается на http://localhost:port
|
||||||
|
"http://localhost:3000", # Стандартный порт для create-react-app
|
||||||
|
"http://localhost:5173", # Стандартный порт для Vite/React
|
||||||
|
# "http://127.0.0.1:5173", # Можно добавить и IP-адрес
|
||||||
|
# "*" # Разрешить все источники (будьте осторожны в production)
|
||||||
|
]
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=origins, # Указываем разрешенные источники
|
||||||
|
allow_credentials=True, # Разрешаем куки (если потребуются в будущем)
|
||||||
|
allow_methods=["*"], # Разрешаем все методы (GET, POST, PUT, etc.)
|
||||||
|
allow_headers=["*"], # Разрешаем все заголовки
|
||||||
|
)
|
||||||
|
# --- Конец настройки CORS ---
|
||||||
|
|
||||||
|
|
||||||
|
# Подключаем роутер расчетов
|
||||||
|
app.include_router(calculation.router)
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def read_root():
|
||||||
|
return {"message": "Welcome to the Glass Cutting Optimization API"}
|
||||||
|
|
||||||
|
# Убедимся, что здесь нет подключения auth роутера
|
||||||
|
# from .routers import calculation # Позже подключим calculation
|
||||||
|
# app.include_router(calculation.router)
|
|
@ -0,0 +1,2 @@
|
||||||
|
from .base import Base
|
||||||
|
from .calculation import Calculation
|
|
@ -0,0 +1,8 @@
|
||||||
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||||
|
from sqlalchemy import Integer
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
# Define a base class using SQLAlchemy 2.0 style
|
||||||
|
class Base(DeclarativeBase):
|
||||||
|
# Define a primary key type annotation for convenience
|
||||||
|
pass
|
|
@ -0,0 +1,22 @@
|
||||||
|
import datetime
|
||||||
|
from sqlalchemy import Column, String, DateTime, JSON, Float, Integer, func
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
from .base import Base
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
|
||||||
|
class Calculation(Base):
|
||||||
|
__tablename__ = "calculations"
|
||||||
|
|
||||||
|
#
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True, index=True)
|
||||||
|
input_params: Mapped[Dict[str, Any]] = mapped_column(JSON, nullable=False)
|
||||||
|
output_results: Mapped[Dict[str, Any] | None] = mapped_column(JSON, nullable=True)
|
||||||
|
timestamp: Mapped[datetime.datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True), server_default=func.now()
|
||||||
|
)
|
||||||
|
model_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||||
|
objective_score: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Calculation(id={self.id}, timestamp='{self.timestamp}')>"
|
|
@ -0,0 +1,20 @@
|
||||||
|
from sqlalchemy import Column, Integer, String, JSON, Float, DateTime
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class Calculation(Base):
|
||||||
|
__tablename__ = "calculations"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
input_params = Column(JSON)
|
||||||
|
output_results = Column(JSON)
|
||||||
|
timestamp = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
model_name = Column(String, nullable=True)
|
||||||
|
objective_score = Column(Float, nullable=True)
|
||||||
|
# Удалите эти строки, если они есть:
|
||||||
|
# user_id = Column(Integer, ForeignKey("users.id"))
|
||||||
|
# user = relationship("User", back_populates="calculations")
|
||||||
|
|
||||||
|
# Можно вообще удалить класс User, если он больше не нужен
|
|
@ -0,0 +1,60 @@
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from app.schemas.calculation import CalculationCreate, CalculationRead
|
||||||
|
from app.services import calculation_service as service
|
||||||
|
from app.db.session import get_db
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/calculation", # Общий префикс для эндпоинтов этого роутера
|
||||||
|
tags=["Calculations"], # Тег для группировки в документации Swagger
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.post("/", response_model=CalculationRead, status_code=status.HTTP_201_CREATED)
|
||||||
|
async def run_calculation(
|
||||||
|
calc_in: CalculationCreate,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Запускает новый расчет раскройки стекла (асинхронно).
|
||||||
|
|
||||||
|
- Принимает `input_params` и опционально `model_name`.
|
||||||
|
- Вызывает внутреннюю асинхронную логику расчета (заглушку).
|
||||||
|
- Сохраняет входные параметры и результаты в БД.
|
||||||
|
- Возвращает созданную запись о расчете.
|
||||||
|
"""
|
||||||
|
print("Received request to run calculation")
|
||||||
|
calculation = await service.create_calculation(db=db, calc_in=calc_in)
|
||||||
|
print("Calculation service finished, returning response")
|
||||||
|
return calculation
|
||||||
|
|
||||||
|
@router.get("/{calculation_id}", response_model=CalculationRead)
|
||||||
|
async def read_calculation(
|
||||||
|
calculation_id: int,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Получает информацию о конкретном расчете по его ID.
|
||||||
|
"""
|
||||||
|
db_calc = service.get_calculation_by_id(db=db, calc_id=calculation_id)
|
||||||
|
if db_calc is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Calculation not found"
|
||||||
|
)
|
||||||
|
return db_calc
|
||||||
|
|
||||||
|
# Определяем эндпоинт для истории до эндпоинта с ID,
|
||||||
|
# чтобы FastAPI не принял "history" за ID
|
||||||
|
@router.get("/history/", response_model=List[CalculationRead])
|
||||||
|
async def read_calculation_history(
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 100,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Получает историю всех выполненных расчетов (с пагинацией).
|
||||||
|
"""
|
||||||
|
calculations = service.get_calculations(db=db, skip=skip, limit=limit)
|
||||||
|
return calculations
|
|
@ -0,0 +1,35 @@
|
||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
|
||||||
|
# --- Базовая схема ---
|
||||||
|
# Общие поля для расчета
|
||||||
|
class CalculationBase(BaseModel):
|
||||||
|
input_params: Dict[str, Any]
|
||||||
|
model_name: Optional[str] = None # Модель может быть выбрана сервером
|
||||||
|
|
||||||
|
# --- Схема для создания (запуска) расчета ---
|
||||||
|
# Данные, которые пользователь отправляет для запуска
|
||||||
|
class CalculationCreate(CalculationBase):
|
||||||
|
pass # Пока совпадает с Base, но может быть расширена
|
||||||
|
|
||||||
|
# --- Схема для чтения ---
|
||||||
|
# Полная информация о расчете, возвращаемая API
|
||||||
|
class CalculationRead(CalculationBase):
|
||||||
|
id: int
|
||||||
|
output_results: Optional[Dict[str, Any]] = None
|
||||||
|
timestamp: datetime
|
||||||
|
objective_score: Optional[float] = None
|
||||||
|
|
||||||
|
# Включаем режим ORM
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
# В Pydantic v1 было:
|
||||||
|
# class Config:
|
||||||
|
# orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
# --- Опционально: Схема для обновления (если результаты добавляются позже) ---
|
||||||
|
class CalculationUpdate(BaseModel):
|
||||||
|
output_results: Optional[Dict[str, Any]] = None
|
||||||
|
objective_score: Optional[float] = None
|
||||||
|
model_name: Optional[str] = None # Если модель определяется по результату
|
|
@ -0,0 +1,11 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
class Token(BaseModel):
|
||||||
|
access_token: str
|
||||||
|
token_type: str
|
||||||
|
|
||||||
|
class TokenData(BaseModel):
|
||||||
|
username: Optional[str] = None
|
||||||
|
# Можно использовать id пользователя, если удобнее
|
||||||
|
# user_id: Optional[int] = None
|
|
@ -0,0 +1,68 @@
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from typing import List, Optional, Dict, Any
|
||||||
|
|
||||||
|
from app.models.calculation import Calculation
|
||||||
|
from app.schemas.calculation import CalculationCreate, CalculationUpdate
|
||||||
|
from app.fuzzy_solver import solve_cutting_problem # Импортируем асинхронную заглушку
|
||||||
|
|
||||||
|
# Делаем функцию создания асинхронной
|
||||||
|
async def create_calculation(db: Session, calc_in: CalculationCreate) -> Calculation:
|
||||||
|
"""
|
||||||
|
Асинхронно создает запись о расчете, вызывает заглушку и сохраняет результат.
|
||||||
|
"""
|
||||||
|
# 1. Вызываем асинхронную заглушку расчета
|
||||||
|
print("Calling async solver...")
|
||||||
|
output_results = await solve_cutting_problem(calc_in.input_params)
|
||||||
|
print("Async solver finished.")
|
||||||
|
|
||||||
|
# 2. Определяем другие параметры (логика остается)
|
||||||
|
objective_score = output_results.get("waste_percentage")
|
||||||
|
model_name = calc_in.model_name or "default_fuzzy_model_v1"
|
||||||
|
|
||||||
|
# 3. Создаем объект модели SQLAlchemy
|
||||||
|
# Операции с БД остаются синхронными в этом примере
|
||||||
|
# Для полной асинхронности потребовался бы async-драйвер и AsyncSession
|
||||||
|
print("Creating DB object...")
|
||||||
|
db_calc = Calculation(
|
||||||
|
input_params=calc_in.input_params,
|
||||||
|
output_results=output_results,
|
||||||
|
objective_score=objective_score,
|
||||||
|
model_name=model_name
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4. Сохраняем в базу данных (синхронные операции)
|
||||||
|
print("Adding to DB session...")
|
||||||
|
db.add(db_calc)
|
||||||
|
print("Committing DB session...")
|
||||||
|
db.commit()
|
||||||
|
print("Refreshing DB object...")
|
||||||
|
db.refresh(db_calc)
|
||||||
|
print("Calculation created and saved.")
|
||||||
|
return db_calc
|
||||||
|
|
||||||
|
# Функции чтения можно оставить синхронными, т.к. они быстрые
|
||||||
|
# и используют синхронную сессию
|
||||||
|
def get_calculation_by_id(db: Session, calc_id: int) -> Optional[Calculation]:
|
||||||
|
"""
|
||||||
|
Получает расчет по его ID.
|
||||||
|
"""
|
||||||
|
return db.query(Calculation).filter(Calculation.id == calc_id).first()
|
||||||
|
|
||||||
|
def get_calculations(db: Session, skip: int = 0, limit: int = 100) -> List[Calculation]:
|
||||||
|
"""
|
||||||
|
Получает список всех расчетов с пагинацией.
|
||||||
|
"""
|
||||||
|
return db.query(Calculation).offset(skip).limit(limit).all()
|
||||||
|
|
||||||
|
# Опционально: Функция для обновления расчета (если нужно)
|
||||||
|
# def update_calculation(db: Session, calc_id: int, calc_update: CalculationUpdate) -> Optional[Calculation]:
|
||||||
|
# db_calc = get_calculation_by_id(db, calc_id)
|
||||||
|
# if not db_calc:
|
||||||
|
# return None
|
||||||
|
# update_data = calc_update.model_dump(exclude_unset=True) # Pydantic v2
|
||||||
|
# # update_data = calc_update.dict(exclude_unset=True) # Pydantic v1
|
||||||
|
# for key, value in update_data.items():
|
||||||
|
# setattr(db_calc, key, value)
|
||||||
|
# db.commit()
|
||||||
|
# db.refresh(db_calc)
|
||||||
|
# return db_calc
|
|
@ -0,0 +1,10 @@
|
||||||
|
fastapi[all]
|
||||||
|
uvicorn[standard]
|
||||||
|
sqlalchemy==2.0.*
|
||||||
|
pydantic[email]
|
||||||
|
python-dotenv
|
||||||
|
alembic
|
||||||
|
psycopg2-binary # Или другой драйвер БД, если нужно, для SQLite не требуется явно, но SQLAlchemy может его использовать
|
||||||
|
# Позже добавим: pytest, httpx, selenium, locust, etc.
|
||||||
|
pytest
|
||||||
|
httpx
|
|
@ -0,0 +1,60 @@
|
||||||
|
import pytest
|
||||||
|
from typing import Generator, Any
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker, Session
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Добавляем корень проекта в sys.path для импорта app
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||||
|
|
||||||
|
from app.main import app as main_app # Импортируем наше FastAPI приложение
|
||||||
|
from app.db.session import get_db, Base # Импортируем get_db и Base
|
||||||
|
from app.core.config import settings # Импортируем настройки
|
||||||
|
|
||||||
|
# Используем отдельную SQLite базу данных для тестов (in-memory)
|
||||||
|
TEST_DATABASE_URL = "sqlite:///:memory:"
|
||||||
|
|
||||||
|
engine = create_engine(
|
||||||
|
TEST_DATABASE_URL, connect_args={"check_same_thread": False}
|
||||||
|
)
|
||||||
|
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
# Фикстура для создания таблиц перед тестами и удаления после
|
||||||
|
@pytest.fixture(scope="session", autouse=True)
|
||||||
|
def setup_db():
|
||||||
|
Base.metadata.create_all(bind=engine) # Создаем таблицы
|
||||||
|
yield
|
||||||
|
Base.metadata.drop_all(bind=engine) # Удаляем таблицы после тестов
|
||||||
|
|
||||||
|
# Фикстура для переопределения зависимости get_db
|
||||||
|
@pytest.fixture(scope="function") # scope="function" чтобы каждая функция получала чистую сессию
|
||||||
|
def db_session() -> Generator[Session, Any, None]:
|
||||||
|
connection = engine.connect()
|
||||||
|
transaction = connection.begin()
|
||||||
|
session = TestingSessionLocal(bind=connection)
|
||||||
|
yield session
|
||||||
|
session.close()
|
||||||
|
transaction.rollback() # Откатываем изменения после каждого теста
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
# Фикстура для FastAPI TestClient
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def client(db_session: Session) -> Generator[TestClient, Any, None]:
|
||||||
|
# Функция для переопределения зависимости get_db
|
||||||
|
def override_get_db():
|
||||||
|
try:
|
||||||
|
yield db_session
|
||||||
|
finally:
|
||||||
|
# Сессия закрывается в фикстуре db_session
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Применяем переопределение зависимости к нашему приложению
|
||||||
|
main_app.dependency_overrides[get_db] = override_get_db
|
||||||
|
# Создаем TestClient
|
||||||
|
with TestClient(main_app) as c:
|
||||||
|
yield c
|
||||||
|
# Очищаем переопределение после тестов модуля
|
||||||
|
main_app.dependency_overrides.clear()
|
|
@ -0,0 +1,97 @@
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
# Импортируем модели и схемы (если нужно для assert'ов)
|
||||||
|
from app.models.calculation import Calculation
|
||||||
|
from app.schemas.calculation import CalculationRead
|
||||||
|
|
||||||
|
# Используем фикстуры client и db_session из conftest.py
|
||||||
|
|
||||||
|
def test_create_calculation(client: TestClient, db_session: Session):
|
||||||
|
"""Тест успешного создания расчета."""
|
||||||
|
input_data = {
|
||||||
|
"input_params": {"width": 1000, "height": 800, "pieces": [1, 2, 3]},
|
||||||
|
"model_name": "test_model"
|
||||||
|
}
|
||||||
|
response = client.post("/calculation/", json=input_data)
|
||||||
|
|
||||||
|
assert response.status_code == 201
|
||||||
|
data = response.json()
|
||||||
|
assert data["input_params"] == input_data["input_params"]
|
||||||
|
assert data["model_name"] == input_data["model_name"]
|
||||||
|
assert "id" in data
|
||||||
|
assert "timestamp" in data
|
||||||
|
assert "output_results" in data # Заглушка должна вернуть результаты
|
||||||
|
assert data["output_results"] is not None
|
||||||
|
# Можно добавить более детальные проверки output_results, если заглушка детерминирована
|
||||||
|
# assert "waste_percentage" in data["output_results"]
|
||||||
|
assert "objective_score" in data # waste_percentage используется как objective_score
|
||||||
|
|
||||||
|
# Проверяем, что запись действительно появилась в тестовой БД
|
||||||
|
calc_id = data["id"]
|
||||||
|
db_calc = db_session.query(Calculation).filter(Calculation.id == calc_id).first()
|
||||||
|
assert db_calc is not None
|
||||||
|
assert db_calc.input_params == input_data["input_params"]
|
||||||
|
assert db_calc.model_name == input_data["model_name"]
|
||||||
|
assert db_calc.output_results is not None
|
||||||
|
|
||||||
|
def test_read_calculation(client: TestClient, db_session: Session):
|
||||||
|
"""Тест получения расчета по ID."""
|
||||||
|
# Сначала создаем расчет для теста
|
||||||
|
input_data = {"input_params": {"size": "large"}, "model_name": "reader_test"}
|
||||||
|
response_create = client.post("/calculation/", json=input_data)
|
||||||
|
assert response_create.status_code == 201
|
||||||
|
created_data = response_create.json()
|
||||||
|
calc_id = created_data["id"]
|
||||||
|
|
||||||
|
# Теперь запрашиваем созданный расчет
|
||||||
|
response_read = client.get(f"/calculation/{calc_id}")
|
||||||
|
assert response_read.status_code == 200
|
||||||
|
read_data = response_read.json()
|
||||||
|
assert read_data["id"] == calc_id
|
||||||
|
assert read_data["input_params"] == input_data["input_params"]
|
||||||
|
assert read_data["model_name"] == input_data["model_name"]
|
||||||
|
assert read_data["output_results"] == created_data["output_results"] # Сравниваем с тем, что вернул POST
|
||||||
|
assert read_data["timestamp"] is not None
|
||||||
|
assert read_data["objective_score"] is not None
|
||||||
|
|
||||||
|
def test_read_calculation_not_found(client: TestClient):
|
||||||
|
"""Тест получения несуществующего расчета."""
|
||||||
|
response = client.get("/calculation/99999") # Используем ID, которого точно нет
|
||||||
|
assert response.status_code == 404
|
||||||
|
assert response.json() == {"detail": "Calculation not found"}
|
||||||
|
|
||||||
|
def test_read_calculation_history(client: TestClient, db_session: Session):
|
||||||
|
"""Тест получения истории расчетов."""
|
||||||
|
# Очистим таблицу на всякий случай (хотя фикстура должна это делать)
|
||||||
|
# db_session.query(Calculation).delete()
|
||||||
|
# db_session.commit()
|
||||||
|
|
||||||
|
# Создаем несколько расчетов
|
||||||
|
client.post("/calculation/", json={"input_params": {"test": 1}})
|
||||||
|
client.post("/calculation/", json={"input_params": {"test": 2}})
|
||||||
|
|
||||||
|
# Запрашиваем историю
|
||||||
|
response = client.get("/calculation/history/")
|
||||||
|
assert response.status_code == 200
|
||||||
|
history_data = response.json()
|
||||||
|
assert isinstance(history_data, list)
|
||||||
|
assert len(history_data) >= 2 # Проверяем, что созданные расчеты есть в истории
|
||||||
|
|
||||||
|
# Проверяем структуру элементов истории
|
||||||
|
if len(history_data) > 0:
|
||||||
|
item = history_data[0]
|
||||||
|
assert "id" in item
|
||||||
|
assert "input_params" in item
|
||||||
|
assert "output_results" in item
|
||||||
|
assert "timestamp" in item
|
||||||
|
assert "model_name" in item
|
||||||
|
assert "objective_score" in item
|
||||||
|
|
||||||
|
def test_create_calculation_invalid_input(client: TestClient):
|
||||||
|
"""Тест создания расчета с невалидными данными (проверка Pydantic)."""
|
||||||
|
# Отправляем данные без обязательного поля input_params
|
||||||
|
response = client.post("/calculation/", json={"model_name": "invalid"})
|
||||||
|
assert response.status_code == 422 # Ошибка валидации Pydantic
|
||||||
|
# Можно проверить тело ошибки, если нужно
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
|
@ -0,0 +1,54 @@
|
||||||
|
# React + TypeScript + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default tseslint.config({
|
||||||
|
extends: [
|
||||||
|
// Remove ...tseslint.configs.recommended and replace with this
|
||||||
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
// Alternatively, use this for stricter rules
|
||||||
|
...tseslint.configs.strictTypeChecked,
|
||||||
|
// Optionally, add this for stylistic rules
|
||||||
|
...tseslint.configs.stylisticTypeChecked,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
// other options...
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// eslint.config.js
|
||||||
|
import reactX from 'eslint-plugin-react-x'
|
||||||
|
import reactDom from 'eslint-plugin-react-dom'
|
||||||
|
|
||||||
|
export default tseslint.config({
|
||||||
|
plugins: {
|
||||||
|
// Add the react-x and react-dom plugins
|
||||||
|
'react-x': reactX,
|
||||||
|
'react-dom': reactDom,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// other rules...
|
||||||
|
// Enable its recommended typescript rules
|
||||||
|
...reactX.configs['recommended-typescript'].rules,
|
||||||
|
...reactDom.configs.recommended.rules,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
|
@ -0,0 +1,28 @@
|
||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + React + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.8.4",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-hook-form": "^7.55.0",
|
||||||
|
"react-router-dom": "^7.5.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.21.0",
|
||||||
|
"@types/react": "^19.0.10",
|
||||||
|
"@types/react-dom": "^19.0.4",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"eslint": "^9.21.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.1.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
|
"globals": "^15.15.0",
|
||||||
|
"typescript": "~5.7.2",
|
||||||
|
"typescript-eslint": "^8.24.1",
|
||||||
|
"vite": "^6.2.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,108 @@
|
||||||
|
#root {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 6em;
|
||||||
|
padding: 1.5em;
|
||||||
|
will-change: filter;
|
||||||
|
transition: filter 300ms;
|
||||||
|
}
|
||||||
|
.logo:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #646cffaa);
|
||||||
|
}
|
||||||
|
.logo.react:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes logo-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
a:nth-of-type(2) .logo {
|
||||||
|
animation: logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-the-docs {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.App {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh; /* Минимальная высота на весь экран */
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-header {
|
||||||
|
background-color: #282c34; /* Темный фон шапки */
|
||||||
|
padding: 20px;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between; /* Разместить заголовок и навигацию по краям */
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-header nav a {
|
||||||
|
color: #61dafb; /* Цвет ссылок в шапке */
|
||||||
|
margin-left: 15px; /* Отступ между ссылками */
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-header nav a:hover {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex-grow: 1; /* Основной контент занимает все доступное пространство */
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 1200px; /* Ограничим максимальную ширину контента */
|
||||||
|
margin: 0 auto; /* Центрируем контент */
|
||||||
|
width: 90%; /* Небольшой отступ по бокам на больших экранах */
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
background-color: #f8f9fa; /* Светлый фон футера */
|
||||||
|
padding: 10px 20px;
|
||||||
|
text-align: center;
|
||||||
|
border-top: 1px solid #e7e7e7;
|
||||||
|
margin-top: auto; /* Прижимаем футер к низу */
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Базовые стили для контейнеров страниц */
|
||||||
|
.page-container {
|
||||||
|
background-color: #fff; /* Белый фон для основного контента */
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* Небольшая тень */
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-container h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #343a40; /* Темный цвет заголовков страниц */
|
||||||
|
border-bottom: 2px solid #007bff; /* Подчеркивание заголовка */
|
||||||
|
padding-bottom: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import AppRouter from './router';
|
||||||
|
import './App.css';
|
||||||
|
|
||||||
|
const App: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="App">
|
||||||
|
<header className="App-header">
|
||||||
|
<h1>Раскройка Стекла</h1>
|
||||||
|
<nav>
|
||||||
|
<Link to="/calculate" style={{ marginRight: '10px' }}>Новый расчет</Link>
|
||||||
|
<Link to="/history">История</Link>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<AppRouter />
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
<p>© 2024 Система Раскройки</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
|
@ -0,0 +1,68 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
// Заменяем заглушки на импорт реальных типов
|
||||||
|
import { CalculationCreate, CalculationRead } from '../types';
|
||||||
|
|
||||||
|
// URL вашего FastAPI бэкенда
|
||||||
|
// Убедитесь, что порт совпадает с тем, на котором запущен uvicorn (по умолчанию 8000)
|
||||||
|
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000';
|
||||||
|
|
||||||
|
const apiClient = axios.create({
|
||||||
|
baseURL: API_BASE_URL,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запускает новый расчет
|
||||||
|
* @param data Входные параметры для расчета
|
||||||
|
* @returns Данные созданного расчета
|
||||||
|
*/
|
||||||
|
// Используем импортированные типы
|
||||||
|
export const runCalculation = async (data: CalculationCreate): Promise<CalculationRead> => {
|
||||||
|
try {
|
||||||
|
// Указываем тип ответа
|
||||||
|
const response = await apiClient.post<CalculationRead>('/calculation/', data);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error running calculation:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает детали конкретного расчета по ID
|
||||||
|
* @param id ID расчета
|
||||||
|
* @returns Данные расчета
|
||||||
|
*/
|
||||||
|
// Используем импортированные типы
|
||||||
|
export const getCalculationById = async (id: number): Promise<CalculationRead> => {
|
||||||
|
try {
|
||||||
|
// Указываем тип ответа
|
||||||
|
const response = await apiClient.get<CalculationRead>(`/calculation/${id}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching calculation ${id}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает историю расчетов
|
||||||
|
* @param skip Смещение для пагинации
|
||||||
|
* @param limit Лимит записей на страницу
|
||||||
|
* @returns Список расчетов
|
||||||
|
*/
|
||||||
|
// Используем импортированные типы
|
||||||
|
export const getCalculationHistory = async (skip: number = 0, limit: number = 100): Promise<CalculationRead[]> => {
|
||||||
|
try {
|
||||||
|
// Указываем тип ответа
|
||||||
|
const response = await apiClient.get<CalculationRead[]>(`/calculation/history/?skip=${skip}&limit=${limit}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching calculation history:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default apiClient;
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
After Width: | Height: | Size: 4.0 KiB |
|
@ -0,0 +1,17 @@
|
||||||
|
.pre {
|
||||||
|
background-color: #f8f9fa; /* Очень светлый фон */
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
white-space: pre-wrap; /* Перенос строк */
|
||||||
|
word-break: break-all; /* Перенос длинных слов */
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
font-size: 0.9em;
|
||||||
|
max-height: 300px; /* Ограничим высоту */
|
||||||
|
overflow-y: auto; /* Добавим скролл */
|
||||||
|
}
|
||||||
|
.noData {
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import React from 'react';
|
||||||
|
import styles from './JsonViewer.module.css';
|
||||||
|
|
||||||
|
const JsonViewer: React.FC<{ data: any }> = ({ data }) => {
|
||||||
|
if (data === undefined || data === null) return <div className={styles.noData}>-</div>;
|
||||||
|
try {
|
||||||
|
return <pre className={styles.pre}>{JSON.stringify(data, null, 2)}</pre>;
|
||||||
|
} catch (e) {
|
||||||
|
return <div className={styles.error}>Не удалось отобразить JSON</div>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default JsonViewer;
|
|
@ -0,0 +1,52 @@
|
||||||
|
.container {
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
margin-top: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.svgCanvas {
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
display: block; /* Убрать лишний отступ под SVG */
|
||||||
|
max-height: 500px; /* Ограничить высоту */
|
||||||
|
background-color: #f8f9fa; /* Фон для области SVG */
|
||||||
|
}
|
||||||
|
|
||||||
|
.pieceRect {
|
||||||
|
fill: #cfe2f3; /* Цвет детали */
|
||||||
|
stroke: #6c757d; /* Цвет обводки */
|
||||||
|
stroke-width: 1;
|
||||||
|
transition: fill 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pieceGroup:hover .pieceRect {
|
||||||
|
fill: #a4c2f4; /* Цвет при наведении */
|
||||||
|
}
|
||||||
|
|
||||||
|
.pieceText {
|
||||||
|
font-family: sans-serif;
|
||||||
|
fill: #212529; /* Цвет текста */
|
||||||
|
pointer-events: none; /* Чтобы текст не мешал наведению на прямоугольник */
|
||||||
|
}
|
||||||
|
|
||||||
|
.pieceId {
|
||||||
|
font-size: 10px;
|
||||||
|
text-anchor: middle;
|
||||||
|
alignment-baseline: middle;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.pieceSize {
|
||||||
|
font-size: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noData {
|
||||||
|
color: #6c757d;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { LayoutPiece } from '../../types'; // Импортируем тип
|
||||||
|
import styles from './LayoutVisualizer.module.css';
|
||||||
|
|
||||||
|
const LayoutVisualizer: React.FC<{ layout: LayoutPiece[] | undefined | null}> = ({ layout }) => {
|
||||||
|
if (!layout || !Array.isArray(layout) || layout.length === 0) {
|
||||||
|
return <p className={styles.noData}>Данные раскроя отсутствуют или некорректны.</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Адаптивный viewBox
|
||||||
|
const PADDING = 10;
|
||||||
|
const maxX = Math.max(...layout.map(p => p.x + p.width)) + PADDING;
|
||||||
|
const maxY = Math.max(...layout.map(p => p.y + p.height)) + PADDING;
|
||||||
|
// Сохраняем соотношение сторон, если нужно, или используем фиксированную высоту/ширину
|
||||||
|
const viewBoxWidth = maxX || 200; // Fallback width
|
||||||
|
const viewBoxHeight = maxY || 200; // Fallback height
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<h4>Визуализация раскроя</h4>
|
||||||
|
<svg
|
||||||
|
className={styles.svgCanvas}
|
||||||
|
width="100%"
|
||||||
|
// preserveAspectRatio="xMidYMid meet" // Сохранять пропорции
|
||||||
|
viewBox={`0 0 ${viewBoxWidth} ${viewBoxHeight}`}
|
||||||
|
>
|
||||||
|
{/* Можно добавить фон листа, если известны его размеры */}
|
||||||
|
{/* <rect x="0" y="0" width={sheetWidth} height={sheetHeight} fill="#eee" /> */}
|
||||||
|
{layout.map((piece) => (
|
||||||
|
<g key={piece.piece_id} className={styles.pieceGroup}>
|
||||||
|
<rect
|
||||||
|
x={piece.x}
|
||||||
|
y={piece.y}
|
||||||
|
width={piece.width}
|
||||||
|
height={piece.height}
|
||||||
|
className={styles.pieceRect}
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
x={piece.x + piece.width / 2}
|
||||||
|
y={piece.y + piece.height / 2}
|
||||||
|
className={`${styles.pieceText} ${styles.pieceId}`}
|
||||||
|
>
|
||||||
|
{piece.piece_id}
|
||||||
|
</text>
|
||||||
|
<text
|
||||||
|
x={piece.x + 5}
|
||||||
|
y={piece.y + 15} // Немного опустим текст размера
|
||||||
|
className={`${styles.pieceText} ${styles.pieceSize}`}
|
||||||
|
>
|
||||||
|
{piece.width}x{piece.height}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LayoutVisualizer;
|
|
@ -0,0 +1,31 @@
|
||||||
|
/* Сброс базовых стилей */
|
||||||
|
body, html, #root {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
background-color: #f4f7f6; /* Светлый фон для всей страницы */
|
||||||
|
color: #333; /* Основной цвет текста */
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
|
monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
|
import App from './App'
|
||||||
|
import './index.css' // Глобальные стили
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<BrowserRouter>
|
||||||
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
|
@ -0,0 +1,63 @@
|
||||||
|
.formContainer {
|
||||||
|
/* Используем класс из App.css для общего стиля */
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGroup {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #495057; /* Цвет метки */
|
||||||
|
}
|
||||||
|
|
||||||
|
.input,
|
||||||
|
.textarea {
|
||||||
|
width: calc(100% - 18px); /* Учитываем padding и border */
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #ced4da; /* Граница поля ввода */
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:focus,
|
||||||
|
.textarea:focus {
|
||||||
|
border-color: #80bdff;
|
||||||
|
outline: 0;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.textarea {
|
||||||
|
min-height: 100px;
|
||||||
|
resize: vertical; /* Разрешить изменять размер по вертикали */
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorMessage {
|
||||||
|
color: #dc3545; /* Красный цвет для ошибок */
|
||||||
|
font-size: 0.875em;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submitButton {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 1rem;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submitButton:hover:not(:disabled) {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submitButton:disabled {
|
||||||
|
background-color: #6c757d; /* Серый цвет для неактивной кнопки */
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
|
@ -0,0 +1,154 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useForm, SubmitHandler } from 'react-hook-form';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { runCalculation } from '../api/calculationApi';
|
||||||
|
import { CalculationCreate, CalculationFormData } from '../types';
|
||||||
|
// Импортируем стили как объект
|
||||||
|
import styles from './CalculationForm.module.css'; // Убедитесь, что этот файл существует
|
||||||
|
|
||||||
|
const CalculationForm: React.FC = () => {
|
||||||
|
console.log("CalculationForm Рендерится! (Полная версия)"); // Обновим лог
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<CalculationFormData>({
|
||||||
|
defaultValues: {
|
||||||
|
sheetWidth: 2500,
|
||||||
|
sheetHeight: 1600,
|
||||||
|
pieceList: "500x300\n400x300\n200x200",
|
||||||
|
modelName: "default_fuzzy_model_v1"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit: SubmitHandler<CalculationFormData> = async (formData) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
console.log('Form Data:', formData);
|
||||||
|
|
||||||
|
const apiData: CalculationCreate = {
|
||||||
|
input_params: {
|
||||||
|
sheet_dimensions: {
|
||||||
|
width: formData.sheetWidth,
|
||||||
|
height: formData.sheetHeight,
|
||||||
|
},
|
||||||
|
pieces_raw: formData.pieceList,
|
||||||
|
},
|
||||||
|
model_name: formData.modelName || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Sending to API:', apiData);
|
||||||
|
const result = await runCalculation(apiData);
|
||||||
|
console.log('API Result:', result);
|
||||||
|
navigate(`/calculation/${result.id}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("API Error:", err);
|
||||||
|
// Попробуем получить более детальную ошибку от axios, если возможно
|
||||||
|
let errorMessage = 'Ошибка при запуске расчета. Попробуйте снова.';
|
||||||
|
if (axios.isAxiosError(err)) { // Проверка типа ошибки axios
|
||||||
|
if (err.response) {
|
||||||
|
// Ошибка пришла с ответом от сервера (например, 4xx, 5xx)
|
||||||
|
errorMessage = `Ошибка сервера: ${err.response.status}. ${err.response.data?.detail || err.message}`;
|
||||||
|
} else if (err.request) {
|
||||||
|
// Запрос был сделан, но ответа не было (сеть, CORS)
|
||||||
|
errorMessage = `Ошибка сети или CORS. Не удалось связаться с сервером. (${err.message})`;
|
||||||
|
} else {
|
||||||
|
// Ошибка при настройке запроса
|
||||||
|
errorMessage = `Ошибка настройки запроса: ${err.message}`;
|
||||||
|
}
|
||||||
|
} else if (err instanceof Error) { // Обычная ошибка JS
|
||||||
|
errorMessage = err.message;
|
||||||
|
}
|
||||||
|
setError(errorMessage);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
// Используем класс из App.css и стили модуля
|
||||||
|
<div className={`page-container ${styles.formContainer}`}>
|
||||||
|
<h2>Ввод параметров расчета</h2>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
{/* Поле Ширина листа */}
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label htmlFor="sheetWidth" className={styles.label}>Ширина листа (мм):</label>
|
||||||
|
<input
|
||||||
|
id="sheetWidth"
|
||||||
|
type="number"
|
||||||
|
className={styles.input}
|
||||||
|
{...register('sheetWidth', {
|
||||||
|
required: 'Ширина обязательна',
|
||||||
|
valueAsNumber: true,
|
||||||
|
min: { value: 100, message: 'Минимальная ширина 100 мм' },
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
{errors.sheetWidth && <p className={styles.errorMessage}>{errors.sheetWidth.message}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Поле Высота листа */}
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label htmlFor="sheetHeight" className={styles.label}>Высота листа (мм):</label>
|
||||||
|
<input
|
||||||
|
id="sheetHeight"
|
||||||
|
type="number"
|
||||||
|
className={styles.input}
|
||||||
|
{...register('sheetHeight', {
|
||||||
|
required: 'Высота обязательна',
|
||||||
|
valueAsNumber: true,
|
||||||
|
min: { value: 100, message: 'Минимальная высота 100 мм' },
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
{errors.sheetHeight && <p className={styles.errorMessage}>{errors.sheetHeight.message}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Поле Список деталей */}
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label htmlFor="pieceList" className={styles.label}>Список деталей (формат: ШхВ, каждая с новой строки):</label>
|
||||||
|
<textarea
|
||||||
|
id="pieceList"
|
||||||
|
rows={6}
|
||||||
|
className={styles.textarea}
|
||||||
|
{...register('pieceList', {
|
||||||
|
required: 'Список деталей обязателен',
|
||||||
|
pattern: {
|
||||||
|
value: /^(\d+x\d+\s*(\r\n|\r|\n)?)+$/,
|
||||||
|
message: 'Неверный формат списка деталей (ожидается ШхВ на каждой строке)'
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
{errors.pieceList && <p className={styles.errorMessage}>{errors.pieceList.message}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Поле Имя модели (опционально) */}
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label htmlFor="modelName" className={styles.label}>Имя модели (опционально):</label>
|
||||||
|
<input
|
||||||
|
id="modelName"
|
||||||
|
type="text"
|
||||||
|
className={styles.input}
|
||||||
|
{...register('modelName')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Отображение общей ошибки API */}
|
||||||
|
{error && <p className={styles.errorMessage}>{error}</p>}
|
||||||
|
|
||||||
|
{/* Кнопка отправки */}
|
||||||
|
<button type="submit" disabled={isLoading} className={styles.submitButton}>
|
||||||
|
{isLoading ? 'Загрузка...' : 'Запустить расчет'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Импорт axios для проверки типа ошибки
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export default CalculationForm;
|
|
@ -0,0 +1,37 @@
|
||||||
|
.resultContainer {
|
||||||
|
/* Общий стиль страницы */
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailSection {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
.detailSection:last-child {
|
||||||
|
border-bottom: none; /* Убрать границу у последнего блока */
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailTitle {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #495057; /* Цвет заголовка секции */
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailValue {
|
||||||
|
color: #343a40; /* Цвет значения */
|
||||||
|
/* Можно добавить стили для лучшего отображения данных */
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading,
|
||||||
|
.error {
|
||||||
|
margin-top: 20px;
|
||||||
|
font-style: italic;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: #dc3545;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { getCalculationById } from '../api/calculationApi';
|
||||||
|
import { CalculationRead } from '../types';
|
||||||
|
// Импортируем вспомогательные компоненты
|
||||||
|
import JsonViewer from '../components/common/JsonViewer';
|
||||||
|
import LayoutVisualizer from '../components/common/LayoutVisualizer';
|
||||||
|
// Импортируем стили
|
||||||
|
import styles from './CalculationResult.module.css';
|
||||||
|
|
||||||
|
const CalculationResult: React.FC = () => {
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const [calculation, setCalculation] = useState<CalculationRead | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!id) {
|
||||||
|
setError("ID расчета не указан.");
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const calculationId = parseInt(id, 10); // Преобразуем ID в число
|
||||||
|
if (isNaN(calculationId)) {
|
||||||
|
setError("Некорректный ID расчета.");
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchCalculation = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const data = await getCalculationById(calculationId);
|
||||||
|
setCalculation(data);
|
||||||
|
} catch (err: any) { // Указываем тип any или AxiosError
|
||||||
|
console.error(`Failed to fetch calculation ${calculationId}:`, err);
|
||||||
|
// Проверяем, была ли это ошибка 404 от нашего API
|
||||||
|
if (err.response && err.response.status === 404) {
|
||||||
|
setError(`Расчет с ID ${calculationId} не найден.`);
|
||||||
|
} else {
|
||||||
|
setError('Не удалось загрузить детали расчета.');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchCalculation();
|
||||||
|
}, [id]); // Зависимость от ID, чтобы перезагружать при изменении ID в URL
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div className={styles.loading}>Загрузка данных расчета ID: {id}...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div className={styles.error}>Ошибка: {error}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!calculation) {
|
||||||
|
// Это состояние не должно достигаться, если ID корректен и нет ошибки,
|
||||||
|
// но лучше добавить проверку
|
||||||
|
return <div>Данные расчета не найдены.</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`page-container ${styles.resultContainer}`}>
|
||||||
|
<h2>Результаты расчета ID: {calculation.id}</h2>
|
||||||
|
|
||||||
|
<div className={styles.detailSection}>
|
||||||
|
<div className={styles.detailTitle}>Время запуска:</div>
|
||||||
|
<div className={styles.detailValue}>{new Date(calculation.timestamp).toLocaleString()}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.detailSection}>
|
||||||
|
<div className={styles.detailTitle}>Использованная модель:</div>
|
||||||
|
<div className={styles.detailValue}>{calculation.model_name || '-'}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.detailSection}>
|
||||||
|
<div className={styles.detailTitle}>Входные параметры:</div>
|
||||||
|
<JsonViewer data={calculation.input_params} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.detailSection}>
|
||||||
|
<div className={styles.detailTitle}>Результаты расчета (JSON):</div>
|
||||||
|
<JsonViewer data={calculation.output_results} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.detailSection}>
|
||||||
|
<div className={styles.detailTitle}>Целевая функция (Остаток %):</div>
|
||||||
|
<div className={styles.detailValue}>{calculation.objective_score?.toFixed(2) ?? '-'}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Добавляем визуализацию, если есть данные */}
|
||||||
|
{calculation.output_results && calculation.output_results.layout && (
|
||||||
|
<div className={styles.detailSection}>
|
||||||
|
<LayoutVisualizer layout={calculation.output_results.layout} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CalculationResult;
|
|
@ -0,0 +1,50 @@
|
||||||
|
.historyContainer {
|
||||||
|
/* Общий стиль страницы */
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 20px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); /* Тень для таблицы */
|
||||||
|
overflow: hidden; /* Для скругления углов */
|
||||||
|
border-radius: 8px; /* Скругление углов таблицы */
|
||||||
|
}
|
||||||
|
|
||||||
|
.th,
|
||||||
|
.td {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 12px 15px; /* Увеличим padding */
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.th {
|
||||||
|
background-color: #f8f9fa; /* Светлый фон заголовков */
|
||||||
|
font-weight: bold;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Зебра для строк */
|
||||||
|
.tbody tr:nth-of-type(even) {
|
||||||
|
background-color: #fdfdfe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbody tr:hover {
|
||||||
|
background-color: #f1f1f1; /* Подсветка строки при наведении */
|
||||||
|
}
|
||||||
|
|
||||||
|
.td a { /* Стилизация ссылки "Подробнее" */
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading,
|
||||||
|
.error,
|
||||||
|
.empty {
|
||||||
|
margin-top: 20px;
|
||||||
|
font-style: italic;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: #dc3545;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Link } from 'react-router-dom'; // Для ссылок на детальные страницы
|
||||||
|
import { getCalculationHistory } from '../api/calculationApi'; // API функция
|
||||||
|
import { CalculationRead } from '../types'; // Тип расчета
|
||||||
|
// Импортируем стили
|
||||||
|
import styles from './History.module.css';
|
||||||
|
|
||||||
|
const History: React.FC = () => {
|
||||||
|
// Состояние для хранения списка расчетов
|
||||||
|
const [calculations, setCalculations] = useState<CalculationRead[]>([]);
|
||||||
|
// Состояние загрузки
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
// Состояние ошибки
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Загрузка данных при монтировании компонента
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchHistory = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const data = await getCalculationHistory(0, 100); // Загружаем первые 100 записей
|
||||||
|
setCalculations(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to fetch calculation history:", err);
|
||||||
|
setError('Не удалось загрузить историю расчетов.');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchHistory();
|
||||||
|
}, []); // Пустой массив зависимостей означает, что эффект выполнится один раз при монтировании
|
||||||
|
|
||||||
|
// --- Отображение состояний ---
|
||||||
|
if (isLoading) {
|
||||||
|
return <div className={styles.loading}>Загрузка истории...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div className={styles.error}>Ошибка: {error}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (calculations.length === 0) {
|
||||||
|
return <div className={styles.empty}>История расчетов пуста.</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
// Используем класс из App.css и стили модуля
|
||||||
|
<div className={`page-container ${styles.historyContainer}`}>
|
||||||
|
<h2>История расчетов</h2>
|
||||||
|
<table className={styles.table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className={styles.th}>ID</th>
|
||||||
|
<th className={styles.th}>Время запуска</th>
|
||||||
|
<th className={styles.th}>Модель</th>
|
||||||
|
<th className={styles.th}>Целевая функция (Остаток %)</th>
|
||||||
|
<th className={styles.th}>Действия</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className={styles.tbody}>
|
||||||
|
{calculations.map((calc) => (
|
||||||
|
<tr key={calc.id}>
|
||||||
|
<td className={styles.td}>{calc.id}</td>
|
||||||
|
<td className={styles.td}>{new Date(calc.timestamp).toLocaleString()}</td>
|
||||||
|
<td className={styles.td}>{calc.model_name || '-'}</td>
|
||||||
|
<td className={styles.td}>{calc.objective_score?.toFixed(2) ?? '-'}</td>
|
||||||
|
<td className={styles.td}>
|
||||||
|
<Link to={`/calculation/${calc.id}`}>Подробнее</Link>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{/* Здесь можно добавить пагинацию, если записей много */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default History;
|
|
@ -0,0 +1,25 @@
|
||||||
|
import React, { Suspense, lazy } from 'react';
|
||||||
|
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
// Ленивая загрузка страниц
|
||||||
|
const CalculationFormPage = lazy(() => import('../pages/CalculationForm'));
|
||||||
|
const HistoryPage = lazy(() => import('../pages/History'));
|
||||||
|
const CalculationResultPage = lazy(() => import('../pages/CalculationResult'));
|
||||||
|
|
||||||
|
const LoadingIndicator = () => <div>Загрузка...</div>;
|
||||||
|
|
||||||
|
const AppRouter: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<LoadingIndicator />}>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/calculate" element={<CalculationFormPage />} />
|
||||||
|
<Route path="/history" element={<HistoryPage />} />
|
||||||
|
<Route path="/calculation/:id" element={<CalculationResultPage />} />
|
||||||
|
<Route path="/" element={<Navigate to="/calculate" replace />} />
|
||||||
|
<Route path="*" element={<div>404 - Страница не найдена</div>} />
|
||||||
|
</Routes>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppRouter;
|
|
@ -0,0 +1,39 @@
|
||||||
|
// Типы, соответствующие Pydantic схемам из backend/app/schemas/calculation.py
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Данные, необходимые для запуска расчета (соответствует CalculationCreate)
|
||||||
|
*/
|
||||||
|
export interface CalculationCreate {
|
||||||
|
input_params: Record<string, any>; // JSON -> Record<string, any>
|
||||||
|
model_name?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Полные данные о расчете (соответствует CalculationRead)
|
||||||
|
*/
|
||||||
|
export interface CalculationRead {
|
||||||
|
id: number;
|
||||||
|
input_params: Record<string, any>;
|
||||||
|
output_results?: Record<string, any> | null;
|
||||||
|
timestamp: string; // DateTime -> string
|
||||||
|
model_name?: string | null;
|
||||||
|
objective_score?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Опционально: Тип для элемента в массиве layout из output_results заглушки
|
||||||
|
export interface LayoutPiece {
|
||||||
|
piece_id: number;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавим тип для данных формы (может немного отличаться от CalculationCreate)
|
||||||
|
export interface CalculationFormData {
|
||||||
|
// Примерные поля, которые могут быть в форме
|
||||||
|
sheetWidth: number;
|
||||||
|
sheetHeight: number;
|
||||||
|
pieceList: string; // Например, строка с размерами деталей "200x300, 150x100"
|
||||||
|
modelName?: string;
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="vite/client" />
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
|
/* Add Vite client types */
|
||||||
|
"types": ["vite/client"],
|
||||||
|
|
||||||
|
/* Added for compatibility */
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
})
|
|
@ -0,0 +1,17 @@
|
||||||
|
from sqlalchemy import create_engine, inspect
|
||||||
|
|
||||||
|
# Подключаемся к БД
|
||||||
|
engine = create_engine("sqlite:///./glass_cutting.db")
|
||||||
|
inspector = inspect(engine)
|
||||||
|
|
||||||
|
# Получаем список таблиц
|
||||||
|
print("Таблицы в БД:", inspector.get_table_names())
|
||||||
|
|
||||||
|
# Получаем информацию по конкретной таблице
|
||||||
|
if "calculations" in inspector.get_table_names():
|
||||||
|
columns = inspector.get_columns("calculations")
|
||||||
|
print("\nСтруктура таблицы 'calculations':")
|
||||||
|
for column in columns:
|
||||||
|
print(f"{column['name']} ({column['type']})")
|
||||||
|
else:
|
||||||
|
print("Таблица 'calculations' не найдена.")
|
Loading…
Reference in New Issue