Перейти к содержанию

ЛР 4. Vue.js (Frontend)

Отчёт по лабораторной работе №4: Реализация клиентской части средствами Vue.js

1. Цель работы

Разработать клиентскую часть (frontend) для системы управления альпинистским клубом с использованием фреймворка Vue.js 3. Основные задачи: создание интерактивного SPA-приложения, настройка взаимодействия с ранее разработанным REST API, реализация интерфейсов авторизации, регистрации и смены пароля, а также подключение библиотеки компонентов Vuetify.


2. Стек технологий и структура проекта

Для реализации клиентской части использован следующий стек:

  • Vue.js — реактивный UI-фреймворк
  • Vuetify — библиотека Material Design компонентов
  • Vue Router — клиентская маршрутизация
  • Pinia — управление состоянием приложения
  • Axios — HTTP-клиент для взаимодействия с API
  • Vite — инструмент сборки

Структура проекта:

climbing-frontend/
├── src/
│   ├── api.js              # Axios instance + interceptors
│   ├── main.js             # Точка входа
│   ├── App.vue             # Корневой компонент + навбар
│   ├── assets/main.css     # Глобальные стили
│   ├── plugins/vuetify.js  # Настройка Vuetify
│   ├── router/index.js     # Маршруты
│   ├── stores/auth.js      # Pinia store авторизации
│   └── views/              # Страницы приложения
│       ├── LoginView.vue
│       ├── DashboardView.vue
│       ├── MountainsView.vue
│       ├── RoutesView.vue
│       ├── ClubsView.vue
│       ├── AlpinistsView.vue
│       ├── ClimbsView.vue
│       ├── CreateClimbView.vue
│       ├── ReportsView.vue
│       └── ChangePasswordView.vue


3. Запуск через Docker

Для развёртывания используется три контейнера: PostgreSQL, Django и Vue. Запускаются одной командой.

Dockerfile фронтенда:

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 5173
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]

Фрагмент docker-compose.yml:

frontend:
  build: ./climbing-frontend
  container_name: climbing_vue_cont
  ports:
    - "5173:5173"
  volumes:
    - ./climbing-frontend:/app
    - /app/node_modules
  depends_on:
    - web

Запуск всей системы:

docker-compose up --build

После запуска фронтенд доступен по адресу http://localhost:5173, бэкенд — http://localhost:8010.


4. Настройка CORS на бэкенде

Для обеспечения взаимодействия фронтенда с API необходимо было настроить CORS.

INSTALLED_APPS = [
    ...
    'corsheaders',
]

MIDDLEWARE = [
    ...
    'corsheaders.middleware.CorsMiddleware',
]

CORS_ALLOWED_ORIGINS = [
    "http://localhost:5173",
    "http://127.0.0.1:5173",
]

CORS_ALLOW_CREDENTIALS = True

5. Подключение Vuetify

Vuetify подключается как плагин Vue. Также импортируется шрифт иконок @mdi/font для отображения Material Design иконок.

// src/plugins/vuetify.js
import 'vuetify/styles'
import '@mdi/font/css/materialdesignicons.css'
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'

export default createVuetify({
  components,
  directives,
  theme: {
    defaultTheme: 'light',
  },
})
// src/main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import vuetify from './plugins/vuetify'
import './assets/main.css'

const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(vuetify)
app.mount('#app')

6. Авторизация, регистрация и смена пароля

Pinia store (stores/auth.js)

Состояние авторизации хранится в Pinia store. Токен и имя пользователя сохраняются в localStorage для сохранения сессии между перезагрузками страницы.

export const useAuthStore = defineStore('auth', {
  state: () => ({
    token:    localStorage.getItem('token')    || '',
    username: localStorage.getItem('username') || '',
  }),
  actions: {
    async login(username, password) {
      const response = await api.post('auth/token/login/', { username, password })
      this.token    = response.data.auth_token
      this.username = username
      localStorage.setItem('token',    this.token)
      localStorage.setItem('username', username)
      return true
    },
    logout() {
      this.token = ''
      this.username = ''
      localStorage.removeItem('token')
      localStorage.removeItem('username')
    }
  }
})

Axios interceptors (api.js)

Каждый запрос автоматически получает токен из localStorage. При ответе 401 или 403 пользователь автоматически разлогинивается.

api.interceptors.request.use(config => {
  const token = localStorage.getItem('token')
  if (token) config.headers.Authorization = `Token ${token}`
  return config
})

api.interceptors.response.use(
  response => response,
  error => {
    if (error.response?.status === 401 || error.response?.status === 403) {
      useAuthStore().logout()
      router.push('/login')
    }
    return Promise.reject(error)
  }
)

Страница входа и регистрации

Вход и регистрация совмещены на одной странице. Переключение между режимами реализовано через реактивный флаг isRegister. При смене режима форма и ошибки сбрасываются.

const isRegister = ref(false)

function switchMode() {
  isRegister.value = !isRegister.value
  error.value = ''
  form.value = { username: '', email: '', password: '', re_password: '' }
}

Страница входа


7. Маршрутизация и защита роутов

Все маршруты, кроме /login, защищены navigation guard — неавторизованный пользователь перенаправляется на страницу входа. Авторизованный пользователь не может попасть обратно на /login.

router.beforeEach((to, from, next) => {
  const auth = useAuthStore()
  if (to.meta.requiresAuth && !auth.token) {
    next('/login')
  } else if (to.name === 'login' && auth.token) {
    next('/dashboard')
  } else {
    next()
  }
})

Список маршрутов приложения:

Путь Компонент Описание
/login LoginView Вход и регистрация
/dashboard DashboardView Главная панель
/mountains MountainsView Справочник вершин
/routes RoutesView Справочник маршрутов
/clubs ClubsView Справочник клубов
/alpinists AlpinistsView Справочник альпинистов
/climbs ClimbsView Список восхождений
/climbs/create CreateClimbView Создание восхождения
/reports ReportsView Аналитика и отчёты
/change-password ChangePasswordView Смена пароля

8. Доработка бэкенда: пагинация и поиск

Для поддержки пагинации и поиска по таблицам на фронтенде был создан класс FlexiblePageNumberPagination. Он поддерживает параметр ?page_size=all для отключения пагинации при необходимости загрузить все данные сразу (например, в выпадающих списках при создании восхождения).

# climbing_app/pagination.py
class FlexiblePageNumberPagination(PageNumberPagination):
    page_size = 10
    page_size_query_param = 'page_size'
    max_page_size = 1000

    def paginate_queryset(self, queryset, request, view=None):
        if request.query_params.get(self.page_size_query_param) == 'all':
            return None   # отключаем пагинацию — вернём весь список
        return super().paginate_queryset(queryset, request, view)

Класс подключён глобально через settings.py:

REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'climbing_app.pagination.FlexiblePageNumberPagination',
    'PAGE_SIZE': 10,
}

Во все основные представления добавлены фильтры поиска через SearchFilter:

class MountainListCreateView(generics.ListCreateAPIView):
    queryset = Mountain.objects.all()
    serializer_class = MountainSerializer
    filter_backends = [OrderingFilter, SearchFilter]
    ordering_fields = ['id', 'name', 'height', 'country', 'region']
    search_fields = ['name', 'country', 'region']

class ClimbListView(generics.ListAPIView):
    serializer_class = ClimbSerializer
    filter_backends = [SearchFilter]
    search_fields = ['group_name', 'route__mountain__name']

    def get_queryset(self):
        return Climb.objects.select_related(
            'route__mountain'
        ).prefetch_related(
            'details__alpinist'
        ).order_by('-id')

На фронтенде поиск реализован с задержкой debounce 350 мс — запрос отправляется не при каждом нажатии клавиши, а только после паузы в наборе:

let searchTimer = null
watch(search, () => {
  clearTimeout(searchTimer)
  searchTimer = setTimeout(() => {
    page.value = 1
    fetchItems()
  }, 350)
})

async function fetchItems() {
  const params = new URLSearchParams({ page: page.value })
  if (search.value) params.set('search', search.value)
  const res = await api.get(`api/mountains/?${params}`)
  const data = res.data
  items.value = Array.isArray(data) ? data : (data.results ?? [])
  totalCount.value = Array.isArray(data) ? data.length : (data.count ?? 0)
}

9. Реализованные интерфейсы

Дашборд

Главная страница отображает сводную статистику (количество гор, клубов, альпинистов, восхождений), таблицу 10 последних восхождений, топ вершин по числу альпинистов и список непокорённых вершин. Данные загружаются параллельно через Promise.all.

Дашборд

Таблицы справочников

Все справочники (вершины, маршруты, клубы, альпинисты) реализованы по единому паттерну: - таблица с серверной пагинацией (v-pagination) - поле поиска с debounce - диалоговое окно создания новой записи (v-dialog) - snackbar с подтверждением

Вершины

Восхождения

Страница восхождений содержит таблицу групп. По клику на строку или кнопку открывается детальный диалог с полной информацией: гора, маршрут, плановые и фактические даты, примечания и состав группы с цветными чипами статуса каждого участника.

function openDetail(climb) {
  selected.value = climb
  detailDialog.value = true
}

Отчёты

Страница отчётов реализована через вкладки v-tabs. Каждая вкладка — отдельный аналитический запрос к API:

Вкладка Запрос
По датам GET /api/query/alpinists-by-date/
Восхождения GET /api/query/climbs-by-period/
Статистика гор GET /api/query/mountain-stats/
Без восхождений GET /api/query/unclimbed/
Альпинист × гора GET /api/query/alpinist-mountain-stats/
Итоговый отчёт GET /api/query/final-report/

Отчёты


10. Выводы

В результате выполнения работы реализована полноценная клиентская часть приложения для управления альпинистским клубом. Настроено взаимодействие с Django REST Framework API через Axios с автоматической токенизацией запросов. Реализованы интерфейсы авторизации, регистрации и смены пароля средствами Djoser.