refactor: align frontend with FSD architecture
This commit is contained in:
123
docs/architecture.md
Normal file
123
docs/architecture.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# Архитектура проекта
|
||||
|
||||
Дата актуализации: 2026-06-22
|
||||
|
||||
## Базовый принцип
|
||||
|
||||
Проект следует Feature-Sliced Design в прагматичном варианте: слой добавляется только тогда,
|
||||
когда у него есть реальная ответственность. Главный рабочий поток сейчас построен вокруг
|
||||
страниц `auth` и `dashboard`, защищенного layout и общего shared-инструментария.
|
||||
|
||||
## Слои
|
||||
|
||||
```text
|
||||
src/
|
||||
app/ # инициализация приложения, router, providers, базовые стили
|
||||
pages/ # route-level страницы и их page-specific model/api/ui
|
||||
widgets/ # крупные композиционные блоки, собирающие entities/features/shared
|
||||
entities/ # доменные сущности
|
||||
shared/ # инфраструктура, API client, базовый UI, helpers, local UI-kit
|
||||
```
|
||||
|
||||
`features` сейчас не используется намеренно. Например, logout используется только внутри
|
||||
`widgets/protected-app-layout`, поэтому orchestration живет в widget model, а не в отдельном
|
||||
feature slice. Если действие начнет использоваться в нескольких независимых flows, его можно
|
||||
поднять в `features/logout-session`.
|
||||
|
||||
## Правила импортов
|
||||
|
||||
- Верхние слои могут импортировать нижние: `app -> pages -> widgets -> features -> entities -> shared`.
|
||||
- Нижние слои не импортируют верхние.
|
||||
- Между слайсами одного слоя не должно быть cross-imports.
|
||||
- Внутри одного слайса допускаются относительные импорты между сегментами.
|
||||
- Внешний код импортирует слайс через public API `index.ts`.
|
||||
- Generated API не импортируется напрямую из app/pages/widgets/entities. Используется facade `@/shared/api`.
|
||||
|
||||
## Текущие ключевые слайсы
|
||||
|
||||
### `pages/dashboard`
|
||||
|
||||
```text
|
||||
src/pages/dashboard/
|
||||
api/
|
||||
competenceMapAnalyticsApi.ts
|
||||
model/
|
||||
chartData.ts
|
||||
constants.ts
|
||||
dashboardStore.ts
|
||||
emptyState.ts
|
||||
error.ts
|
||||
filterDefinitions.ts
|
||||
filterDraft.ts
|
||||
filters.ts
|
||||
formatters.ts
|
||||
queryKeys.ts
|
||||
sorting.ts
|
||||
tooltip.ts
|
||||
types.ts
|
||||
ui/
|
||||
DashboardPage.vue
|
||||
ReportsView.vue
|
||||
CompetenceMapFilters.vue
|
||||
CompetenceMapToolbar.vue
|
||||
components/
|
||||
styles/
|
||||
```
|
||||
|
||||
Dashboard page владеет аналитическими фильтрами, вкладками, API wrappers, DTO-to-view helpers,
|
||||
табличной сортировкой, tooltip templates и page-specific компонентами. TanStack Query отвечает
|
||||
за server state cache; ручной response cache удален.
|
||||
|
||||
### `pages/auth`
|
||||
|
||||
```text
|
||||
src/pages/auth/
|
||||
model/
|
||||
loginSchema.ts
|
||||
redirect.ts
|
||||
useAuthLogin.ts
|
||||
ui/
|
||||
```
|
||||
|
||||
UI формы не содержит schema и redirect validation. Эти правила находятся в model-сегменте и
|
||||
покрыты unit-тестами.
|
||||
|
||||
### `widgets/protected-app-layout`
|
||||
|
||||
Widget собирает защищенную часть приложения: header, sidebar, page content, footer и scroll-up.
|
||||
Здесь живут current-user composition, logout orchestration, sidebar route matching, route preload
|
||||
и persisted expanded groups. `shared/ui/AppSidebar` остается pure UI и не знает про Vue Router.
|
||||
|
||||
### `shared/api`
|
||||
|
||||
Содержит API client, auth token primitives, refresh/session infrastructure, error normalization,
|
||||
generated API и public facade. Прикладной код импортирует endpoint functions и DTO-типы через
|
||||
`@/shared/api`, а не из `generated-api`.
|
||||
|
||||
## Generated и vendor-зоны
|
||||
|
||||
Эти зоны исключены из FSD/formatting-оценки как generated/vendor:
|
||||
|
||||
- `src/shared/api/generated-api`
|
||||
- `src/shared/model/generated-zod`
|
||||
- `src/shared/ui-kit`
|
||||
|
||||
Ручные wrappers, adapters и page models не должны попадать в эти исключения.
|
||||
|
||||
## Проверки архитектуры
|
||||
|
||||
Основная команда:
|
||||
|
||||
```bash
|
||||
bun run check:full
|
||||
```
|
||||
|
||||
Она запускает ESLint, Steiger, unit tests, typecheck/build и Playwright E2E. Для быстрой проверки
|
||||
FSD-границ:
|
||||
|
||||
```bash
|
||||
bun run lint:fsd
|
||||
```
|
||||
|
||||
Перед передачей изменений заказчику `lint:fsd` должен проходить без ошибок или иметь явно
|
||||
задокументированное точечное исключение.
|
||||
82
docs/generated-api.md
Normal file
82
docs/generated-api.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Generated API
|
||||
|
||||
Дата актуализации: 2026-06-22
|
||||
|
||||
## Назначение
|
||||
|
||||
Проект использует Orval для генерации TypeScript API-клиентов и Zod-моделей из `openapi.json`.
|
||||
Generated-код не редактируется вручную.
|
||||
|
||||
## Команда генерации
|
||||
|
||||
```bash
|
||||
bun run apigen
|
||||
```
|
||||
|
||||
Orval config находится в `orval.config.ts`.
|
||||
|
||||
## Входной файл
|
||||
|
||||
```text
|
||||
openapi.json
|
||||
```
|
||||
|
||||
В config есть нормализация русскоязычных тегов OpenAPI в стабильные англоязычные имена файлов:
|
||||
|
||||
- `Аутентификация` -> `authentication`
|
||||
- `Карта компетенций: аналитика` -> `competence-map-analytics`
|
||||
- `Мониторинг` -> `monitoring`
|
||||
- `Пользователь` -> `user`
|
||||
- `Управление пользователями` -> `user-management`
|
||||
|
||||
Config также умеет читать первый валидный JSON-документ, если исходный OpenAPI-файл содержит
|
||||
лишние данные после первого JSON.
|
||||
|
||||
## Выходные директории
|
||||
|
||||
```text
|
||||
src/shared/api/generated-api/
|
||||
src/shared/model/generated-zod/
|
||||
```
|
||||
|
||||
`generated-api` использует custom mutator:
|
||||
|
||||
```text
|
||||
src/shared/api/axiosInstance.ts
|
||||
```
|
||||
|
||||
Это сохраняет единое поведение API-клиента: base URL, credentials, refresh flow, error handling
|
||||
и abort signal.
|
||||
|
||||
## Public facade
|
||||
|
||||
Прикладной код не должен импортировать generated-файлы напрямую. Правильный путь:
|
||||
|
||||
```ts
|
||||
import { getCompetenceMapAnalytics, type Login } from '@/shared/api'
|
||||
```
|
||||
|
||||
Неправильный путь:
|
||||
|
||||
```ts
|
||||
import { api_v1_competence_map_analytics_list } from '@/shared/api/generated-api/competence-map-analytics'
|
||||
```
|
||||
|
||||
Facade `src/shared/api/index.ts` сейчас экспортирует только используемые приложением endpoint
|
||||
functions, token primitives, error helpers и DTO-типы.
|
||||
|
||||
## Правила изменений
|
||||
|
||||
- Не редактировать файлы в `generated-api` и `generated-zod` вручную.
|
||||
- При изменении backend контракта обновить `openapi.json` и выполнить `bun run apigen`.
|
||||
- После генерации выполнить `bun run check:full`.
|
||||
- Если новому экрану нужен endpoint, сначала добавить осознанный export в `src/shared/api/index.ts`
|
||||
или отдельный wrapper, а не импортировать generated-файл напрямую.
|
||||
- Если DTO требует page-specific адаптации, mapper должен жить рядом с owner-страницей или рядом
|
||||
с API wrapper, а не в generated-зоне.
|
||||
|
||||
## Tooling exclusions
|
||||
|
||||
Generated-директории исключены из Steiger и Prettier через `steiger.config.ts` и `.prettierignore`.
|
||||
Это не разрешение писать ручной код внутри generated-зон. Исключение нужно только для стабильной
|
||||
работы tooling после Orval generation.
|
||||
@@ -576,12 +576,12 @@ Query params:
|
||||
Страница имеет четыре серверных состояния. Они отличаются составом данных, но используют один и тот же
|
||||
endpoint `/api/v1/competence-map/analytics/`.
|
||||
|
||||
| `association_type` | KPI | `main_block.type` | `region_ranking` | `top_industries.mode` | `trend_block` | `employee_stats` |
|
||||
|---|---|---|---|---|---|---|
|
||||
| `all` | 4 карточки по всем объединениям | `all_associations_composition` | 3 метрики по типам объединений | `stacked_by_association_type` | `opk_participation_by_year` | `null` |
|
||||
| `sez` | 4 карточки по ОЭЗ | `sez_types_and_priority_directions` | ОЭЗ + организации ОПК | `single_association_type` | `null` | сотрудники ОПК-резидентов ОЭЗ и сотрудники ОЭЗ |
|
||||
| `technoparks` | 4 карточки по технопаркам | `technopark_area_distribution` | технопарки + организации ОПК | `single_association_type` | `null` | сотрудники ОПК-резидентов технопарков и их доля |
|
||||
| `clusters` | 4 карточки по кластерам | `cluster_specializations` | кластеры + организации ОПК | `single_association_type` | `cluster_creation_dynamics` | `null` |
|
||||
| `association_type` | KPI | `main_block.type` | `region_ranking` | `top_industries.mode` | `trend_block` | `employee_stats` |
|
||||
| ------------------ | ------------------------------- | ----------------------------------- | ------------------------------ | ----------------------------- | --------------------------- | ----------------------------------------------- |
|
||||
| `all` | 4 карточки по всем объединениям | `all_associations_composition` | 3 метрики по типам объединений | `stacked_by_association_type` | `opk_participation_by_year` | `null` |
|
||||
| `sez` | 4 карточки по ОЭЗ | `sez_types_and_priority_directions` | ОЭЗ + организации ОПК | `single_association_type` | `null` | сотрудники ОПК-резидентов ОЭЗ и сотрудники ОЭЗ |
|
||||
| `technoparks` | 4 карточки по технопаркам | `technopark_area_distribution` | технопарки + организации ОПК | `single_association_type` | `null` | сотрудники ОПК-резидентов технопарков и их доля |
|
||||
| `clusters` | 4 карточки по кластерам | `cluster_specializations` | кластеры + организации ОПК | `single_association_type` | `cluster_creation_dynamics` | `null` |
|
||||
|
||||
Требования:
|
||||
|
||||
@@ -2287,16 +2287,16 @@ Backend должен гарантировать:
|
||||
|
||||
### Status code matrix
|
||||
|
||||
| Status | Когда возвращается |
|
||||
|---|---|
|
||||
| `200 OK` | Успешный ответ, включая валидные фильтры без данных. |
|
||||
| `304 Not Modified` | Conditional GET по `If-None-Match` или `If-Modified-Since`, если представление не изменилось. |
|
||||
| `400 Bad Request` | Некорректный query param, неизвестный query param, повтор параметра, неверный формат значения. |
|
||||
| `401 Unauthorized` | Пользователь не аутентифицирован. |
|
||||
| `403 Forbidden` | Пользователь аутентифицирован, но не имеет доступа к странице или разделу. |
|
||||
| `429 Too Many Requests` | Превышен rate limit. |
|
||||
| `500 Internal Server Error` | Непредвиденная ошибка backend-а. |
|
||||
| `503 Service Unavailable` | Временная недоступность сервиса или источника аналитических данных. |
|
||||
| Status | Когда возвращается |
|
||||
| --------------------------- | ---------------------------------------------------------------------------------------------- |
|
||||
| `200 OK` | Успешный ответ, включая валидные фильтры без данных. |
|
||||
| `304 Not Modified` | Conditional GET по `If-None-Match` или `If-Modified-Since`, если представление не изменилось. |
|
||||
| `400 Bad Request` | Некорректный query param, неизвестный query param, повтор параметра, неверный формат значения. |
|
||||
| `401 Unauthorized` | Пользователь не аутентифицирован. |
|
||||
| `403 Forbidden` | Пользователь аутентифицирован, но не имеет доступа к странице или разделу. |
|
||||
| `429 Too Many Requests` | Превышен rate limit. |
|
||||
| `500 Internal Server Error` | Непредвиденная ошибка backend-а. |
|
||||
| `503 Service Unavailable` | Временная недоступность сервиса или источника аналитических данных. |
|
||||
|
||||
Для collection/report endpoint-ов не использовать `404 Not Found`, если path существует, но по валидным фильтрам
|
||||
нет данных. В этом случае нужен `200 OK` с пустыми массивами.
|
||||
|
||||
@@ -320,10 +320,10 @@ GET /api/v1/competence-map/analytics/?association_type=technoparks
|
||||
|
||||
Обязательные метрики для ОЭЗ:
|
||||
|
||||
| `code` | Назначение | `value_type` |
|
||||
|---|---|---|
|
||||
| `opk_resident_employees_count` | численность сотрудников организаций ОПК-резидентов ОЭЗ | `integer` |
|
||||
| `sez_employees_count` | общая численность сотрудников ОЭЗ | `integer` |
|
||||
| `code` | Назначение | `value_type` |
|
||||
| ------------------------------ | ------------------------------------------------------ | ------------ |
|
||||
| `opk_resident_employees_count` | численность сотрудников организаций ОПК-резидентов ОЭЗ | `integer` |
|
||||
| `sez_employees_count` | общая численность сотрудников ОЭЗ | `integer` |
|
||||
|
||||
### Технопарки. Требуемые поля
|
||||
|
||||
@@ -357,10 +357,10 @@ GET /api/v1/competence-map/analytics/?association_type=technoparks
|
||||
|
||||
Обязательные метрики для технопарков:
|
||||
|
||||
| `code` | Назначение | `value_type` |
|
||||
|---|---|---|
|
||||
| `opk_resident_employees_count` | численность сотрудников организаций ОПК-резидентов технопарков | `integer` |
|
||||
| `opk_resident_employees_share_percent` | доля сотрудников организаций ОПК-резидентов технопарков | `percent` |
|
||||
| `code` | Назначение | `value_type` |
|
||||
| -------------------------------------- | -------------------------------------------------------------- | ------------ |
|
||||
| `opk_resident_employees_count` | численность сотрудников организаций ОПК-резидентов технопарков | `integer` |
|
||||
| `opk_resident_employees_share_percent` | доля сотрудников организаций ОПК-резидентов технопарков | `percent` |
|
||||
|
||||
### Требования
|
||||
|
||||
@@ -421,13 +421,13 @@ GET /api/v1/competence-map/analytics/?association_type=technoparks
|
||||
|
||||
## Сводка исправлений по backend response
|
||||
|
||||
| Проблема | Endpoint | Поля | Что нужно сделать |
|
||||
|---|---|---|---|
|
||||
| Placeholder субъектов РФ | `/analytics/` для всех `association_type` | `region_ranking.rows[].region_name` | возвращать реальные названия субъектов РФ |
|
||||
| Placeholder отраслей | `/analytics/` для всех `association_type` | `top_industries.rows[].industry_name` | возвращать реальные названия отраслей |
|
||||
| Пустая таблица направлений ОЭЗ | `/analytics/?association_type=sez` | `main_block.priority_directions.rows[]` | вернуть строки направлений ОЭЗ |
|
||||
| Нет карточек сотрудников ОЭЗ | `/analytics/?association_type=sez` | `employee_stats.items[]` | вернуть две KPI-метрики сотрудников |
|
||||
| Нет карточек сотрудников технопарков | `/analytics/?association_type=technoparks` | `employee_stats.items[]` | вернуть две KPI-метрики сотрудников |
|
||||
| Проблема | Endpoint | Поля | Что нужно сделать |
|
||||
| ------------------------------------ | ------------------------------------------ | --------------------------------------- | ----------------------------------------- |
|
||||
| Placeholder субъектов РФ | `/analytics/` для всех `association_type` | `region_ranking.rows[].region_name` | возвращать реальные названия субъектов РФ |
|
||||
| Placeholder отраслей | `/analytics/` для всех `association_type` | `top_industries.rows[].industry_name` | возвращать реальные названия отраслей |
|
||||
| Пустая таблица направлений ОЭЗ | `/analytics/?association_type=sez` | `main_block.priority_directions.rows[]` | вернуть строки направлений ОЭЗ |
|
||||
| Нет карточек сотрудников ОЭЗ | `/analytics/?association_type=sez` | `employee_stats.items[]` | вернуть две KPI-метрики сотрудников |
|
||||
| Нет карточек сотрудников технопарков | `/analytics/?association_type=technoparks` | `employee_stats.items[]` | вернуть две KPI-метрики сотрудников |
|
||||
|
||||
---
|
||||
|
||||
|
||||
1278
docs/review.md
Normal file
1278
docs/review.md
Normal file
File diff suppressed because it is too large
Load Diff
99
docs/testing.md
Normal file
99
docs/testing.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# Testing Policy
|
||||
|
||||
Дата актуализации: 2026-06-22
|
||||
|
||||
## Обязательные проверки перед сдачей
|
||||
|
||||
```bash
|
||||
bun run format:check
|
||||
bun run check:full
|
||||
```
|
||||
|
||||
`check:full` запускает:
|
||||
|
||||
- ESLint;
|
||||
- Steiger FSD lint;
|
||||
- Vitest unit tests;
|
||||
- TypeScript typecheck;
|
||||
- production build;
|
||||
- Playwright E2E.
|
||||
|
||||
## Текущее покрытие
|
||||
|
||||
Unit:
|
||||
|
||||
- dashboard filter normalization;
|
||||
- dashboard API params;
|
||||
- structured query keys;
|
||||
- dashboard empty state;
|
||||
- chart adapters;
|
||||
- sorting;
|
||||
- formatting/pluralization;
|
||||
- tooltip escaping;
|
||||
- dashboard filter draft mapping;
|
||||
- dashboard filter definitions;
|
||||
- `CompetenceMapFilterField` render/v-model contract;
|
||||
- auth login schema;
|
||||
- auth safe redirect helper;
|
||||
- UI-kit entrypoint smoke.
|
||||
|
||||
E2E:
|
||||
|
||||
- redirect protected route `/dashboard` to `/auth` without auth session;
|
||||
- auth form empty validation.
|
||||
|
||||
## Что нужно покрыть следующим этапом
|
||||
|
||||
Component tests:
|
||||
|
||||
- `CompetenceMapFilters.vue`: open/sync draft, apply, reset, cancel, disabled/loading state;
|
||||
- `DashboardTabs.vue`: tab selection and region rating branch;
|
||||
- `RegionRankingCard.vue`: sort controls;
|
||||
- `TopIndustriesCard.vue`: empty and filled states;
|
||||
- `ChartTooltipPortal.vue`: visibility and positioning contract.
|
||||
|
||||
E2E with mocked backend:
|
||||
|
||||
- authorized dashboard load;
|
||||
- all/sez/technoparks/clusters tabs;
|
||||
- filters dialog apply/reset/cancel;
|
||||
- ranking table sort;
|
||||
- region rating tab;
|
||||
- mobile layout smoke.
|
||||
|
||||
Visual regression:
|
||||
|
||||
- desktop auth page;
|
||||
- desktop dashboard all/sez/technoparks/clusters;
|
||||
- filters dialog;
|
||||
- mobile dashboard top section.
|
||||
|
||||
## Правила написания тестов
|
||||
|
||||
- Pure helpers тестировать unit-тестами без Vue mount.
|
||||
- Page-specific UI тестировать рядом с owner slice.
|
||||
- UI-kit internals не тестировать в page tests; для сложных компонентов использовать stubs или E2E.
|
||||
- API-зависимые E2E должны использовать route mocks, а не реальный backend.
|
||||
- Для security-sensitive строковых HTML templates обязательно оставлять tests на escaping.
|
||||
- После изменения FSD-границ всегда запускать `bun run lint:fsd`.
|
||||
|
||||
## Известные build warnings
|
||||
|
||||
Сборка проходит, но Vite/Rolldown предупреждает:
|
||||
|
||||
- `INVALID_ANNOTATION` в `node_modules/@vueuse/core/dist/index.js`;
|
||||
- главный chunk больше 500 kB.
|
||||
|
||||
Это не runtime failures. Bundle size требует отдельного performance-pass с анализатором чанков.
|
||||
|
||||
## Известные test warnings
|
||||
|
||||
Vitest component test для Vue SFC может печатать jsdom warning:
|
||||
|
||||
```text
|
||||
Could not parse CSS stylesheet
|
||||
```
|
||||
|
||||
Тесты при этом проходят. Это связано с парсингом runtime-injected SFC/UI-kit CSS в jsdom, а не с
|
||||
ошибкой production CSS. Если warning начнет мешать CI logs, стоит добавить test setup с точечной
|
||||
фильтрацией этого сообщения или вынести component tests на browser-based runner.
|
||||
Reference in New Issue
Block a user