ЛР 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.