Хотите создать полноценное веб-приложение без JavaScript, используя только Python? В этой статье мы с нуля создадим простой фитнес-трекер, используя исключительно Python и Reflex, и увидим, как легко можно объединить бэкенд и фронтенд логику, работать с базой данных и создавать реактивный пользовательский интерфейс.
Get started with a template:
(0) blank (https://blank-template.reflex.run) - A blank Reflex app.
(1) ai - Generate a template using AI [Experimental]
(2) choose templates - Choose an existing template.
Which template would you like to use? (0): 0
"""Welcome to Reflex! This file outlines the steps to create a basic app."""
import reflex as rx
from rxconfig import config
class State(rx.State):
"""The app state."""
...
def index() -> rx.Component:
# Welcome Page (Index)
return rx.container(
rx.color_mode.button(position="top-right"),
rx.vstack(
rx.heading("Welcome to Reflex!", size="9"),
rx.text(
"Get started by editing ",
rx.code(f"{config.app_name}/{config.app_name}.py"),
size="5",
),
rx.link(
rx.button("Check out our docs!"),
href="https://reflex.dev/docs/getting-started/introduction/",
is_external=True,
),
spacing="5",
justify="center",
min_height="85vh",
),
rx.logo(),
)
app = rx.App()
app.add_page(index)
# rxconfig.py
import reflex as rx
config = rx.Config(
app_name="fitness_reflex",
db_url="sqlite:///reflex.db", # <-- Добавляем эту строку
)
─────────────────────────────────────────────────────────────────────── Starting Reflex App ───────────────────────────────────────────────────────────────────────
[13:05:26] Compiling: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 16/16 0:00:00
─────────────────────────────────────────────────────────────────────────── App Running ───────────────────────────────────────────────────────────────────────────
App running at: http://localhost:3000
Backend running at: http://0.0.0.0:8000
from datetime import datetime, timezone, timedelta
from sqlmodel import Field
import sqlalchemy
# Определяем модель данных для таблицы 'workout'
class Workout(rx.Model, table=True):
"""Модель для хранения записи о тренировке в БД."""
# Поле для хранения времени завершения тренировки
# Используем sqlalchemy.Column для установки значения по умолчанию на стороне БД
completed: datetime = Field(
sa_column=sqlalchemy.Column(
sqlalchemy.DateTime(timezone=True), # Тип данных в БД с таймзоной
server_default=sqlalchemy.func.now(), # Значение по умолчанию - текущее время БД
nullable=False # Поле не может быть пустым
)
)
# --- Остальной код (State, index, app) ниже ---
# 1. Инициализация Alembic
reflex db init
# 2. Создание файла миграции на основе изменений в моделях
reflex db makemigrations --message "Добавляем модель Workout"
# 3. Применение миграции к базе данных
reflex db migrate
# fitness_tracker/fitness_tracker.py
# ... (импорты и модель Workout)
WEEKLY_GOAL = 5 # Наша цель - 5 тренировок в неделю
class State(rx.State):
"""Состояние приложения (бэкенд-логика и данные)."""
workouts: list[str] = [] # Список строк с датами тренировок для отображения
target: int = WEEKLY_GOAL # Еженедельная цель
current_week_offset: int = 0 # Смещение текущей недели (0 - текущая, -1 - прошлая, 1 - следующая)
# --- Методы для загрузки данных ---
def load_workouts_for_week(self, week_offset: int):
"""Загружает тренировки для недели со смещением week_offset."""
today = datetime.now(timezone.utc)
# Вычисляем начало и конец нужной недели
start_of_current_week = today - timedelta(days=today.weekday())
start_date = start_of_current_week + timedelta(weeks=week_offset)
# Устанавливаем время на начало дня
start_date = start_date.replace(hour=0, minute=0, second=0, microsecond=0)
end_date = start_date + timedelta(days=7)
# Запрос к БД с использованием сессии Reflex
with rx.session() as session:
db_workouts = (
session.query(Workout)
.filter(
Workout.completed >= start_date,
Workout.completed < end_date,
)
.order_by(Workout.completed) # Сортируем для порядка
.all()
)
# Форматируем даты в строки для отображения (State не хранит сложные объекты)
self.workouts = [
w.completed.strftime("%Y-%m-%d %H:%M") for w in db_workouts
]
# --- Остальной код (index, app) ниже ---
# --- Вычисляемые свойства (Computed Properties) ---
# Эти свойства автоматически пересчитываются при изменении зависимых данных
@rx.var
def progress(self) -> int:
"""Количество выполненных тренировок на выбранной неделе."""
return len(self.workouts)
@rx.var
def progress_percentage(self) -> int:
"""Прогресс выполнения цели в процентах."""
if self.target == 0: # Избегаем деления на ноль
return 100 if self.progress > 0 else 0
return int(min(100, (self.progress / self.target) * 100)) # Ограничиваем 100%
@rx.var
def goal_reached(self) -> bool:
"""Достигнута ли еженедельная цель?"""
return self.progress >= self.target
@rx.var
def is_current_week_view(self) -> bool:
"""Просматривается ли текущая календарная неделя?"""
return self.current_week_offset == 0
@rx.var
def current_week_display(self) -> str:
"""Строка для отображения номера недели и года."""
# Рассчитываем дату внутри отображаемой недели
display_date = datetime.now(timezone.utc) + timedelta(weeks=self.current_week_offset)
# Используем isocalendar для получения номера недели по ISO
year, week, _ = display_date.isocalendar()
return f"{year} - Неделя {week:02}"
def load_current_week_workouts(self):
"""Загружает тренировки для текущей отображаемой недели."""
self.load_workouts_for_week(self.current_week_offset)
# --- Обработчики событий (Event Handlers) ---
def show_previous_week(self):
"""Переключиться на предыдущую неделю."""
self.current_week_offset -= 1
self.load_current_week_workouts() # Перезагружаем данные
def show_next_week(self):
"""Переключиться на следующую неделю."""
# Ограничим переход в будущее, если не хотим разрешать просмотр будущих недель
# if self.current_week_offset < 0: # Пример ограничения
self.current_week_offset += 1
self.load_current_week_workouts() # Перезагружаем данные
def log_workout(self):
"""Записать новую тренировку в БД."""
# Проверяем, что мы на текущей неделе, чтобы не логировать в прошлом/будущем
if not self.is_current_week_view:
return # Ничего не делаем, если пытаемся логировать не на текущей неделе
with rx.session() as session:
new_workout = Workout() # Создаем экземпляр, 'completed' заполнится автоматически
session.add(new_workout)
session.commit() # Сохраняем в БД
session.refresh(new_workout) # Обновляем объект из БД (опционально)
# Перезагружаем данные, чтобы увидеть новую тренировку
self.load_current_week_workouts()
def progress_display() -> rx.Component:
"""Отображает заголовок недели и прогресс-бар."""
return rx.vstack( # Вертикальный стек
rx.heading(State.current_week_display, size="5", margin_bottom="0.5em"), # Используем вычисляемое свойство
rx.progress(value=State.progress_percentage, width="100%", color_scheme="green", size="3"), # Используем вычисляемое свойство
rx.text(f"{State.progress} из {State.target} тренировок", size="2", margin_top="0.2em"), # Показываем числовой прогресс
align="center", # Центрируем элементы внутри vstack
width="80%", # Задаем ширину блока
margin_bottom="1.5em"
)
def week_navigation_buttons() -> rx.Component:
"""Кнопки для переключения недель."""
return rx.hstack( # Горизонтальный стек
rx.button("⬅️ Прошлая неделя", on_click=State.show_previous_week, size="2"), # Кнопка вызывает метод State
rx.spacer(), # Распорка для расталкивания кнопок
# Опционально: отключаем кнопку "Следующая", если мы на текущей неделе
rx.button(
"Следующая неделя ➡️",
on_click=State.show_next_week,
size="2",
disabled=State.is_current_week_view # Отключаем кнопку, если это текущая неделя
),
spacing="4", # Пространство между элементами
width="80%", # Задаем ширину блока
justify="center", # Центрируем содержимое
margin_bottom="1em"
)
def conditional_workout_logging_section() -> rx.Component:
"""Отображает кнопку 'Записать' или сообщение о достижении цели."""
return rx.vstack( # Используем vstack для центрирования
# Условное отображение с помощью rx.cond
rx.cond(
State.goal_reached, # Если цель достигнута...
rx.text("🎉 Отлично! Цель на неделю выполнена! 💪", size="4", color_scheme="green", weight="bold"),
# Иначе (цель не достигнута)...
rx.cond(
State.is_current_week_view, # Проверяем, текущая ли это неделя...
rx.button( # Если да, показываем кнопку
"✅ Записать тренировку",
on_click=State.log_workout, # Кнопка вызывает метод State
size="3",
color_scheme="grass", # Зеленая кнопка
),
# Иначе (не текущая неделя)...
rx.text("") # Ничего не показываем
)
),
align="center",
width="80%",
margin_top="1.5em",
min_height="3em" # Резервируем место, чтобы UI не прыгал
)
def workout_list() -> rx.Component:
"""Отображает список выполненных тренировок."""
return rx.vstack(
rx.heading("История тренировок", size="6", margin_bottom="0.5em"),
# Отображение списка с помощью rx.foreach
rx.cond( # Показываем сообщение, если список пуст
State.workouts, # Условие: есть ли элементы в списке?
rx.vstack( # Если список не пуст, отображаем его
rx.foreach(
State.workouts, # Итерируемся по списку строк-дат
lambda workout_date: rx.box( # Оборачиваем каждую строку в Box для стилизации
rx.text(f"✅ {workout_date}"),
border="1px solid #e2e8f0",
padding="0.5em",
border_radius="md",
width="100%"
)
),
spacing="2", # Пространство между записями
align="stretch", # Растягиваем элементы на всю ширину
),
rx.text("На этой неделе тренировок еще не было.", color_scheme="gray") # Сообщение, если список пуст
),
align="center",
width="80%",
margin_top="1em",
min_height="10em" # Минимальная высота, чтобы избежать скачков интерфейса
)
def index() -> rx.Component:
"""Главная страница приложения."""
return rx.container( # Используем контейнер для центрирования и ограничения ширины
rx.vstack(
rx.heading("🏃♂️ Фитнес-трекер", size="8", margin_bottom="1em"),
progress_display(), # Наш компонент прогресса
week_navigation_buttons(), # Наши кнопки навигации
workout_list(), # Наш список тренировок
conditional_workout_logging_section(), # Наша кнопка или сообщение о цели
# Общие стили для вертикального стека
spacing="5", # Расстояние между основными блоками
align="center", # Центрируем все блоки по горизонтали
padding_y="2em", # Вертикальные отступы сверху/снизу
),
max_width="600px", # Ограничиваем максимальную ширину контейнера
center_content=True, # Центрируем содержимое контейнера
min_height="100vh", # Минимальная высота на весь экран
)
# --- Конфигурация приложения ---
app = rx.App()
app.add_page(
index,
title="Фитнес-трекер | Reflex", # Заголовок вкладки браузера
# Загружаем данные при первой загрузке страницы
on_load=State.load_current_week_workouts
)