12 KiB
Daiting — Frontend
Vue 3 + Vite + Tauri v2. Работает как PWA в браузере и как нативное десктопное приложение (Windows / macOS / Linux).
Стек
| Слой | Технология |
|---|---|
| UI framework | Vue 3 (Composition API, <script setup>) |
| Build tool | Vite 6 |
| Desktop shell | Tauri v2 |
| State management | Pinia |
| Routing | Vue Router 4 |
| HTTP client | Axios (сгенерированный клиент src/api/api.ts) |
| Форм-валидация | Vuelidate |
| Анимации | GSAP 3 |
| Карты | Leaflet |
| Утилиты | VueUse |
| CSS | SCSS + Tailwind v4 (reset-only) |
| Package manager | pnpm |
| TypeScript | строгий режим, no any |
Быстрый старт
Зависимости
- Node.js ≥ 20
- pnpm ≥ 9 (
npm i -g pnpm) - Rust + cargo (для Tauri: rustup.rs)
- Tauri CLI устанавливается через pnpm как dev-зависимость
# Клонировать репо
git clone <repo-url>
cd dating-app-frontend
# Установить зависимости
pnpm install
# Запустить в браузере (порт 1420)
pnpm dev
# Запустить с Tauri (нативное окно)
pnpm tauri dev
Переменные окружения
Создайте .env.local в корне:
VITE_API_BASE_URL=http://localhost:3000
По умолчанию фоллбэк — http://localhost:3000.
Структура проекта
dating-app-frontend/
├── src/
│ ├── api/
│ │ ├── api.ts # Сгенерированный клиент (не трогать)
│ │ └── client.ts # Axios-инстанс + интерцептор refresh-токена
│ ├── assets/
│ │ └── grain.svg # SVG-текстура шума для фона
│ ├── components/
│ │ ├── common/ # AppButton, AppInput, AppModal, AppDrawer,
│ │ │ # AppToast, LoadingSpinner, EmptyState
│ │ ├── layout/ # AppShell, SideNav, BottomNav, TauriTitlebar
│ │ ├── feed/ # FeedCard, FeedCardStack, FeedFilters, ProfileBadge
│ │ ├── chat/ # ChatBubble, ChatInput, VoiceRecorder, MediaMessage
│ │ ├── profile/ # ProfileEditor, MediaGallery
│ │ ├── dates/ # MapPicker, DateProposalForm
│ │ └── reports/ # ReportModal
│ ├── composables/ # useAuth, useFeed, useChat, useMedia, useGeolocation
│ ├── router/
│ │ └── index.ts # Маршруты + guards + восстановление сессии
│ ├── stores/
│ │ ├── auth.store.ts # user, accessToken (in-memory), profiles
│ │ ├── feed.store.ts # cards, filters, pagination
│ │ ├── chat.store.ts # chats, messages, polling
│ │ ├── profile.store.ts
│ │ └── ui.store.ts # toasts, sidebar state, reference data (tags/cities)
│ ├── styles/
│ │ ├── _variables.scss # CSS custom properties + SCSS breakpoint-миксины
│ │ ├── _typography.scss
│ │ ├── _animations.scss
│ │ ├── main.scss # Глобальные стили (без Tailwind)
│ │ └── tailwind.css # @import "tailwindcss" — отдельно от SCSS
│ ├── views/ # LoginView, RegisterView, ProfileSetupView,
│ │ # FeedView, MatchesView, ChatsListView, ChatRoomView,
│ │ # DatesView, MyProfileView, ProfileDetailView,
│ │ # ReportsView (admin)
│ ├── App.vue
│ ├── main.ts
│ └── vite-env.d.ts
├── src-tauri/
│ ├── src/
│ │ ├── lib.rs # Tauri plugins init
│ │ └── main.rs # Windows entry (no console window)
│ ├── capabilities/
│ │ └── default.json # Tauri permissions
│ ├── Cargo.toml
│ ├── build.rs
│ └── tauri.conf.json
├── PRODUCT.md # Стратегический документ для AI-ассистентов
├── vite.config.ts
├── tsconfig.json
└── package.json
Маршрутизация
| Путь | Компонент | Guard |
|---|---|---|
/ |
redirect → /feed |
— |
/login |
LoginView | guest only |
/register |
RegisterView | guest only |
/setup |
ProfileSetupView | auth |
/feed |
FeedView | auth |
/matches |
MatchesView | auth |
/chats |
ChatsListView | auth |
/chats/:chatId |
ChatRoomView | auth |
/dates |
DatesView | auth |
/profile/me |
MyProfileView | auth |
/profile/:profileId |
ProfileDetailView | auth |
/admin/reports |
ReportsView | auth + admin |
Логика guard:
- При первой навигации пытается восстановить сессию через
refreshTokenиз localStorage. - Если пользователь авторизован, но профилей нет — редирект на
/setup. - Если токен истёк — silent refresh через
POST /api/v1/auth/refresh.
Работа с токенами
Access token → хранится только в памяти (Pinia store / переменная модуля)
Refresh token → localStorage ('refreshToken')
src/api/client.ts содержит Axios response interceptor: при получении 401 пытается один раз обновить токен. Если не удаётся — чистит сессию и редиректит на /login. Параллельные запросы ставятся в очередь и резолвятся с новым токеном.
Управление состоянием
auth.store
state: { user, accessToken (in-memory ref), activeProfileId }
actions: login, logout, register, fetchMe, setActiveProfile,
addProfile, updateProfile, removeProfile
feed.store
state: { cards[], filters, page, hasMore, searchPaused, loading }
actions: fetchNextPage(profileId), applyFilters(filters), removeCard(profileId)
chat.store
state: { chats[], activeChat, messages[], pollingTimer }
actions: fetchChats, fetchMessages, sendMessage, openChat, closeChat,
startPolling, stopPolling
// Polling интервал: 2000 мс. Место замены на WebSocket помечено комментарием.
ui.store
state: { toasts[], sidebarExpanded, tags[], cities[], districts{}, greetings[] }
actions: addToast(message, type, duration), removeToast(id), setTags/Cities/...
SCSS-архитектура
Tailwind v4 импортируется отдельно от SCSS:
// main.ts — порядок важен
import '@/styles/tailwind.css' // @import "tailwindcss"
import '@/styles/main.scss' // custom styles
Каждый SCSS-парциал (_typography.scss, _animations.scss) включает в начале:
@use 'variables' as *;
Это нужно, так как Sass module system (@use) имеет пофайловую область видимости — миксины не наследуются транзитивно.
Компоненты Vue используют <style scoped lang="scss">. Vite через additionalData в vite.config.ts автоматически инжектирует @use "@/styles/_variables.scss" as *; во все <style lang="scss"> блоки компонентов (кроме самих файлов стилей, чтобы избежать цикличности).
Дизайн-токены (CSS custom properties)
--color-base: #0d0d0d // фон приложения
--color-surface: #161614 // поверхность карточек
--color-surface-2: #1e1e1b // инпуты, вторичные поверхности
--color-cream: #f0ebe0 // основной текст
--color-muted: #6b6860 // второстепенный текст
--color-signal: #c45c3a // акцент (CTA, лайк, активный стейт)
--font-display: 'Instrument Serif', serif
--font-mono: 'DM Mono', monospace
Шрифты загружаются из Google Fonts в index.html.
Tauri
Обнаружение среды
const isTauri = typeof window !== 'undefined' && !!window.__TAURI__;
Кастомный тайтлбар
TauriTitlebar.vue рендерится только при isTauri === true. Контейнер AppShell.vue использует data-tauri-drag-region для нативного drag окна.
Файловый диалог
В ChatInput.vue и MediaGallery.vue при isTauri используется @tauri-apps/plugin-dialog (open()), иначе — стандартный <input type="file">.
Конфигурация окна
src-tauri/tauri.conf.json:
decorations: false— убирает системный тайтлбарminWidth: 375— соответствует мобильному брейкпойнту- Permissions:
dialog:allow-open,dialog:allow-save,shell:allow-open
API-клиент
src/api/api.ts — сгенерированный файл (swagger-typescript-api). Не редактировать вручную.
src/api/client.ts — обёртка:
import { apiClient } from '@/api/client'
// Пример вызова
const feed = await apiClient.api.feedControllerGetFeed({
profileId: 'xxx',
page: 1,
limit: 20,
})
Все методы возвращают Promise<void> по типам (ограничение генератора) — кастуйте через as unknown as YourType.
Скрипты
pnpm dev # Vite dev server, порт 1420
pnpm build # vue-tsc + vite build → dist/
pnpm preview # Превью prod-сборки
pnpm tauri dev # Tauri dev window (запускает pnpm dev внутри)
pnpm tauri build # Нативный инсталлятор → src-tauri/target/release/bundle/
Разработка компонентов
Новый view
- Создать файл в
src/views/<section>/. - Добавить маршрут в
src/router/index.tsс нужнымиmeta(auth,admin). - Лениво импортировать:
component: () => import('@/views/...').
Новый компонент
<script setup lang="ts">— обязательно.<style scoped lang="scss">— изолированные стили.- Никаких
any— типы изsrc/api/api.tsили кастомные интерфейсы в сторе/компоненте. - Ошибки API — через
uiStore.addToast(message, 'error'), неconsole.error. - Скелетоны вместо спиннеров для контента с историей (лента, чат, список).
Анимации
GSAP используется в FeedCard.vue для drag-механики. Все GSAP-вызовы оборачивайте в проверку:
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
if (!prefersReducedMotion) {
gsap.to(el, { ... })
}
Известные ограничения
- Типы API: генератор возвращает
Promise<void>для всех методов. Реальные типы ответов приходится кастовать черезas unknown as T. При регенерации клиента это сохраняется. - Polling: чат поллит каждые 2 секунды. Место замены на WebSocket отмечено комментарием
// TODO: replace with WebSocket subscriptionвchat.store.ts. - FCM:
POST /api/v1/auth/fcm-tokenвызывается как заглушка. Реальная интеграция требует Tauri-плагина для push-уведомлений. - Upload:
mediaControllerUploadвMediaGallery.vueотправляет запрос без тела — нужно доработать под реальный multipart/form-data с файлом.