66 KiB
Ревью проекта и план FSD/Vue рефакторинга
Дата: 2026-06-22
Проект: analytical-panel
Цель документа: зафиксировать техническое ревью, источники best practices, проблемы текущей реализации и пошаговый план рефакторинга без потери визуального или логического поведения приложения.
1. Главный принцип рефакторинга
Рефакторинг должен идти маленькими проверяемыми шагами. На каждом шаге нужно сохранять:
- тот же пользовательский сценарий авторизации;
- тот же redirect guard для защищенных страниц;
- те же запросы к backend и параметры фильтров;
- те же состояния загрузки, ошибки и пустых данных;
- те же вкладки аналитики:
all,sez,technoparks,clusters, режим рейтинга регионов; - ту же сортировку таблиц;
- те же тултипы графиков и интерактивные элементы;
- ту же адаптивную верстку;
- те же публичные UI-kit импорты, пока не будет отдельного шага по оптимизации bundle.
Визуальный редизайн не входит в первый этап. Сначала нужно привести код к поддерживаемой архитектуре, не меняя внешний вид.
2. Исследованные источники и применимые выводы
2.1. Feature-Sliced Design
Официальные источники:
- FSD Overview: https://fsd.how/docs/get-started/overview/
- FSD Layers: https://feature-sliced.design/docs/reference/layers
- FSD Slices and segments: https://feature-sliced.design/docs/reference/slices-segments
- FSD Public API: https://feature-sliced.design/docs/reference/public-api
- FSD Migration from custom architecture: https://feature-sliced.design/docs/guides/migration/from-custom
- FSD Migration from v2.0 to v2.1: https://feature-sliced.design/docs/guides/migration/from-v2-0
- FSD Handling API Requests: https://feature-sliced.design/docs/guides/examples/api-requests
- FSD Types: https://feature-sliced.design/docs/guides/examples/types
- FSD Authentication: https://feature-sliced.design/docs/guides/examples/auth
- FSD Page layouts: https://feature-sliced.design/docs/guides/examples/page-layout
- FSD Desegmentation smell: https://feature-sliced.design/docs/guides/issues/desegmented
- FSD Cross-import smell: https://feature-sliced.design/docs/guides/issues/cross-imports
- FSD Routing smell: https://feature-sliced.design/docs/guides/issues/routes
Ключевые выводы для проекта:
- FSD не требует использовать все слои. Минимально нормальная структура может состоять из
app,pages,shared;widgets,features,entitiesдобавляются только когда дают реальную пользу. - Актуальная ментальная модель FSD v2.1:
pages first. Сначала страница становится понятным слайсом, затем действительно переиспользуемая логика выносится ниже. - Основное правило импортов: код внутри слайса может импортировать только слайсы слоев ниже. Исключения:
appиshared, потому что в них нет бизнес-слайсов. - Каждый слайс и каждый сегмент без слайсов должен иметь public API. Внешний код не должен импортировать внутренние файлы напрямую.
- Внутри одного слайса лучше использовать относительные импорты напрямую к нужному файлу, а не импортировать из собственного
index.ts, чтобы не создавать цикл через barrel-файл. shared/uiиshared/libне должны превращаться в одну большую бочку экспортов, если это ухудшает tree-shaking и размер bundle. Для крупных UI-наборов допустимы отдельные entrypoints.- DTO и generated API допустимо держать в
shared/api, особенно если они связаны между доменами. Мапперы стоит держать рядом с DTO/API-оберткой или рядом со слайсом, если маппер специфичен для страницы. - Логика auth token storage и refresh может жить в
shared/api, если она является частью общего API-клиента. UI-действия вроде logout лучше не прятать вshared/ui. - Layout с бизнес-виджетами можно держать в
app/layoutsлибо строить через slots/render props, чтобы не нарушать направление зависимостей. - Cross-import между слайсами одного слоя является code smell и должен быть либо устранен, либо явно документирован.
- Hardcoded routing/URLs на нижних слоях размазывает ответственность. Роутинг должен концентрироваться в
app/pages, а нижним слоям лучше передавать callbacks, props или route descriptors.
2.2. Vue 3
Официальные источники:
- Vue Style Guide, Essential: https://vuejs.org/style-guide/rules-essential
- Vue Style Guide, Strongly Recommended: https://vuejs.org/style-guide/rules-strongly-recommended.html
- Vue
<script setup>: https://vuejs.org/api/sfc-script-setup - Vue TypeScript with Composition API: https://vuejs.org/guide/typescript/composition-api
- Vue Composables: https://vuejs.org/guide/reusability/composables
- Vue Performance: https://vuejs.org/guide/best-practices/performance
- Vue Production Deployment: https://vuejs.org/guide/best-practices/production-deployment
- Vue Security: https://vuejs.org/guide/best-practices/security
- Vue Tooling: https://vuejs.org/guide/scaling-up/tooling
- Vue Router Lazy Loading: https://router.vuejs.org/guide/advanced/lazy-loading.html
Ключевые выводы для проекта:
<script setup lang="ts">является рекомендуемым синтаксисом для Composition API в SFC.- Для props и emits желательно использовать типизированные объявления. В TypeScript-проектах предпочтительны
defineProps<Props>(),defineEmits<...>(),defineModel<...>()по ситуации. - Компоненты должны быть multi-word, за исключением корневого
App. Отключениеvue/multi-word-component-namesдопустимо только временно и с понятной причиной. v-forдолжен иметь стабильныйkey;v-ifиv-forна одном элементе нужно избегать.- Стили компонентов приложения должны быть scoped или изолированы соглашением имен. Глобальные стили допустимы для root/layout/reset/design tokens, но не для всей страницы на 1900 строк.
- Composables нужны для переиспользуемой чистой логики; если переиспользуется и логика, и layout, это уже компонент.
- Composables должны возвращать refs/plain object так, чтобы потребитель не терял реактивность при destructuring.
- Для производительности нужно измерять bundle size, избегать лишних абстракций, применять route-level code splitting и не тянуть тяжелые части UI-kit в главный chunk без необходимости.
- Vue Router рекомендует dynamic imports для route components; проект это уже делает.
- ESLint и Prettier должны быть частью проверок перед поставкой; желательно иметь единый
check/CI script. - В security-контексте нельзя строить templates из недоверенного контента. В текущем коде есть HTML tooltip templates, поэтому нужно оставить явный escaping и покрыть это тестами.
2.3. Pinia, TanStack Query, Orval
Официальные источники:
- Pinia Defining a Store: https://pinia.vuejs.org/core-concepts/
- TanStack Query Vue Query Keys: https://tanstack.com/query/v5/docs/framework/vue/guides/query-keys
- TanStack Query Vue Dependent Queries: https://tanstack.com/query/v5/docs/framework/vue/guides/dependent-queries
- Orval Vue Query: https://orval.dev/docs/guides/vue-query/
- Orval Custom HTTP Client: https://orval.dev/docs/guides/custom-client/
Ключевые выводы для проекта:
- Pinia setup stores могут использовать composables и watchers, но store лучше держать сфокусированным и в отдельном файле.
- TanStack Query уже решает server state: кэш, stale time, retry, query keys. Ручной кэш поверх Vue Query должен быть оправдан поведением UI, иначе он усложняет модель.
- Query keys должны включать все переменные, от которых зависит query function. Строковый ключ через
join('|')работает, но структурированный ключ с объектом фильтров проще проверять и масштабировать. - Orval может генерировать Vue Query composables и type-safe helpers. В проекте Orval уже настроен на
vue-queryи custom mutator, но потребители обходят public API generated-модулей.
3. Текущее состояние проекта
3.1. Стек
Из package.json:
- Vue
^3.5.34; - Vite
^8.0.12; - TypeScript
~6.0.2; - Vue Router
^5.1.0; - Pinia
^3.0.4; - TanStack Vue Query
^5.101.0; - TanStack Vue Table
^8.21.3; - Orval
^8.18.0; - Zod
3.25.76; - Vee Validate
^4.15.1; - Tailwind CSS
^4.1.18; - shadcn/reka-based local UI-kit;
- Vitest and Playwright.
3.2. Baseline проверок
На момент ревью:
bun run typecheckпроходит;bun run lintпроходит;bun run test:unitпроходит: 4 test files, 18 tests;bun run buildпроходит.
Предупреждения build:
- основной
dist/assets/index-*.jsпосле минификации около725.42 kB, gzip около234.10 kB; - Vite/Rolldown выводит предупреждения
INVALID_ANNOTATIONпо#__PURE__вnode_modules/@vueuse/core/dist/index.js; - Vite предупреждает о chunk больше
500 kB.
3.3. Структура, важная для FSD
Сейчас есть:
src/app/layouts,src/app/providers;src/app/router;src/app/styles;src/pages/auth,src/pages/dashboard;src/widgets/protected-app-layout;src/entities/user;src/shared/api,src/shared/lib,src/shared/model,src/shared/ui,src/shared/ui-kit;- root entrypoints вне слоев:
src/main.ts,src/App.vue,src/style.css.
Крупные файлы:
src/pages/dashboard/ui/DashboardPage.vue: около 487 строк;src/pages/dashboard/model/dashboardStore.ts: около 205 строк;src/pages/dashboard/ui/CompetenceMapFilters.vue: около 284 строк;src/pages/dashboard/ui/components/CompetenceMapFilterField.vue: около 98 строк;src/pages/dashboard/ui/components/TrendCard.vue: около 261 строк;src/pages/dashboard/ui/components/TopIndustriesCard.vue: около 197 строк;src/style.css: 2 строки, только CSS entrypoint;src/pages/dashboard/ui/styles/sections/layout.css: около 495 строк;src/pages/dashboard/ui/styles/sections/tables.css: около 415 строк;src/pages/dashboard/ui/styles/sections/composition.css: около 367 строк;src/pages/dashboard/ui/styles/sections/charts-and-industries.css: около 324 строк;src/pages/dashboard/ui/ReportsView.vue: около 243 строк.
Практические пороги размера файлов для этого проекта:
- page component: целевой диапазон 150-250 строк, после 300 строк нужен план декомпозиции, после 500 строк это архитектурный smell;
- widget component: целевой диапазон 150-300 строк, после 350 строк нужно проверить, не смешаны ли UI, model и адаптеры данных;
- feature/entity component: целевой диапазон 100-250 строк, после 300 строк нужно выносить подкомпоненты или composable;
- shared/ui component: целевой диапазон 80-180 строк, после 250 строк компонент обычно становится слишком специализированным для
shared; - composable: целевой диапазон 80-180 строк, после 220 строк нужно разделять orchestration, derived state и side effects;
- store/model file: целевой диапазон 150-300 строк, после 350 строк нужно отделять query keys, API adapters, pure mappers и UI state;
- pure helper/mapper: целевой диапазон 50-150 строк, после 200 строк нужно проверить cohesion и тестируемые единицы;
- CSS-файл компонента или feature: целевой диапазон 150-300 строк, после 400 строк нужно дробить по зонам ответственности;
- глобальный CSS entrypoint: только reset, tokens, base layers и импорты; страничные стили не должны жить в root
style.css; - generated, vendor, icon packs и локальный
shared/ui-kit, если он осознанно хранится как design system/vendor layer, не оцениваются по этим порогам.
Это не жесткий лимит ради числа строк. Файл может быть длиннее, если он механически сгенерирован или содержит однотипную таблицу конфигурации. Для ручного прикладного кода превышение порога означает обязательное ревью ответственности файла.
3.4. Диагностика Steiger
Команда первичного аудита: bunx steiger src
Результат: 12 errors, 1 warning, без автофиксов.
Для дальнейшей работы добавлена воспроизводимая команда bun run lint:fsd и конфиг steiger.config.ts. В конфиге осознанно исключены generated/vendor-зоны:
src/shared/api/generated-api/**;src/shared/model/generated-zod/**;src/shared/ui-kit/**.
Эти исключения не скрывают архитектурные проблемы прикладного кода. Они отделяют ручной FSD-код от generated API, generated zod-моделей и локального UI-kit, который сейчас фактически живет как отдельный design system.
Текущий результат bun run lint:fsd: проходит без ошибок.
Точечное архитектурное исключение:
src/entities/user: правилоfsd/insignificant-sliceотключено только для этого слайса, потому чтоuserявляется доменной entity, даже если в текущем приложении используется только вwidgets/protected-app-layout.
Уже устранено:
src/pages/dashboardбольше не является segmentless slice: real page component перенесен вsrc/pages/dashboard/ui/DashboardPage.vue;src/widgets/competence-map-filtersиsrc/widgets/competence-map-toolbarслиты вsrc/pages/dashboard/ui, потому что сейчас они используются только dashboard-страницей;src/stores/dashboard.tsперенесен вsrc/pages/dashboard/model/dashboardStore.ts,src/storesудален;src/viewsудален, потому что самостоятельных потребителей не осталось;src/routerперенесен вsrc/app/routerи разделен наroutes.ts,guards.ts,index.ts;- прямые импорты generated API из прикладного кода заменены на public facade
@/shared/api; src/style.cssпревращен в entrypoint, а базовые и dashboard-стили разнесены поsrc/app/stylesиsrc/pages/dashboard/ui/styles.
Найденные категории:
src/entities/user: слайс используется только вwidgets/protected-app-layout; сохранен осознанно как доменная entity с точечным исключениемfsd/insignificant-slice;- direct imports из
@/shared/api/generated-api/...устранены в ручном app/pages/widgets/entities коде, но это нужно закрепить lint-правилом; src/shared/model/generated-zod/**исключен как generated area;- внутренности
src/shared/ui-kit/lib/...конфликтуют с зарезервированными именами FSD (lib,model,ui), что для локального design system нужно либо исключить осознанно, либо переупаковать.
4. Главные проблемы текущего кода
P0. Архитектурные нарушения, влияющие на поддержку
-
dashboardStore.tsуже разделен и больше не содержит ручной response cache.Выполнено: типы, константы, фильтры, empty state, error resolving, chart data, форматтеры, сортировка, query keys и tooltip helpers вынесены в отдельные model-файлы. Store стал orchestration layer примерно на 205 строк. Ручные
analyticsResponseCache/filtersResponseCacheудалены; server state теперь опирается на TanStack Query cache со структурированными query keys. -
DashboardPage.vueстал тоньше, но все еще выше целевого диапазона.Выполнено: KPI, state blocks, tabs, composition panels, ranking, industries, trend, employee stats и tooltip portal вынесены в компоненты
src/pages/dashboard/ui/components. Файл уменьшен примерно с 1712 до 487 строк. Это уже не критический монолит, но он все еще выше целевого диапазона page component 150-250 строк и требует второго прохода после декомпозиции фильтров/toolbar. -
CompetenceMapFilters.vueбольше не является крупным монолитом, но требует полного component-test flow.Выполнено: draft mapping вынесен в
model/filterDraft.ts, option definitions вынесены вmodel/filterDefinitions.ts, select field вынесен вui/components/CompetenceMapFilterField.vue.CompetenceMapFilters.vueуменьшен примерно с 470 до 284 строк. Добавлен component test дляCompetenceMapFilterField. Оставшийся риск: нет component tests на полный apply/reset/cancel flow и disabled/loading states диалога. -
CSS больше не лежит целиком в root, но dashboard CSS пока разделен крупными section-файлами.
Выполнено:
src/style.cssтеперь только импортируетsrc/app/styles/base.cssиsrc/pages/dashboard/ui/styles/dashboard.css. Оставшийся риск:layout.css,tables.css,composition.css,charts-and-industries.cssвсе еще крупные и используют общие class selectors. Их нужно дробить рядом с компонентами только после стабилизации UI-декомпозиции. -
shared/uiочищен от прямой app/router logic в header и sidebar.Примеры:
- выполнено:
AppHeaderActions.vueбольше не импортируетuseLogout, а эмититlogout; - выполнено:
AppHeaderBranding.vueбольше не использует router/smoothScroll напрямую, а получает props и эмититmainPageClick; - выполнено:
ProtectedAppLayout.vueстал owner для logout redirect, route state и smooth scroll; - выполнено:
AppSidebarбольше не импортирует router/preload/localStorage logic; - выполнено: route matching, navigation, route preload и persisted expanded groups перенесены в
widgets/protected-app-layout/model/useProtectedAppSidebarNavigation.ts.
Выполнено: logout orchestration больше не живет в
shared/api; оно перенесено вwidgets/protected-app-layout/model/useProtectedLogout.ts. Вshared/apiостались low-level endpoint facade и token primitives. - выполнено:
-
Public API generated API исправлен на уровне импортов, но нужна защита от регрессий.
В
src/shared/api/index.tsдобавлен facade для используемых endpoint functions и типов. Ручной прикладной код больше не импортирует@/shared/api/generated-api/...напрямую. Следующий шаг: закрепить это ESLint/Steiger convention и добавить README для generated area. -
Auth page приведен ближе к FSD/Vue best practices.
Выполнено:
AuthLoginForm.vueимпортируетuseAuthLoginиз локального model-сегмента, а не из@/pages/auth; login schema вынесена вpages/auth/model/loginSchema.ts; safe redirect helper вынесен вpages/auth/model/redirect.tsи покрыт unit-тестом.
P1. Риски логики и тестируемости
-
Тестовое покрытие недостаточно для безопасного рефакторинга dashboard.
Сейчас есть:
- unit-тесты для
normalizeAnalyticsFilters, fallback association type, цветов,toApiParams, structured query keys, empty analytics response; - unit-тесты для dashboard filter draft mapping и filter definitions;
- unit-тесты для chart adapters, сортировки, sort direction, pluralization/growth formatting и
escapeHtml; - unit-тест entrypoint UI-kit;
- E2E только для redirect на auth и локальной валидации login form.
Все еще нет тестов на:
- component behavior для dashboard cards/tables;
- component-level draft/apply/reset/cancel flow фильтров;
- tab switching;
- ranking report;
- mocked authorized dashboard load;
- визуальные состояния dashboard.
- unit-тесты для
-
Loading/placeholder UX после смены фильтров не закреплен тестами.
Ручной cache layer удален в пользу TanStack Query cache. Следующий риск теперь не архитектурный cache, а UX: нужно компонентно или E2E проверить, что при смене фильтров loading/empty/fetching states остаются ожидаемыми.
-
Query key переведен со строки на структурированный объект.
Выполнено: добавлен model/queryKeys.ts, query keys строятся как ['competence-map-analytics', normalizeAnalyticsFilters(filters)] и ['competence-map-analytics-filters', normalizeAnalyticsFilters(filters)].
- Error handling dashboard дублирует общую error-модель.
resolveDashboardApiErrorMessage вынесен из store в src/pages/dashboard/model/error.ts, но все еще не переиспользует общую error-модель из shared/api/error. Следующий шаг: унифицировать, если сообщения и контракты ошибок совпадают.
- Tooltip HTML templates строятся строками.
Сейчас есть escapeHtml, это правильно. Но безопасность и корректность нужно закрепить unit-тестами, потому что при будущих правках легко случайно убрать escaping.
src/shared/model/generated-zodостается generated area без ручного public API.
Steiger исключает src/shared/model/generated-zod/**, но для передачи заказчику нужно явно задокументировать, что это generated-код и он не редактируется вручную.
P2. Качество сборки и tooling
buildне запускает ESLint, но добавлен отдельныйcheck.
build по-прежнему делает typecheck && vite build, что нормально для runtime-сборки. Для сдачи заказчику добавлен check, который запускает lint, lint:fsd, typecheck, unit tests и build. В CI отдельно стоит запускать test:e2e или check:full.
vue/multi-word-component-namesполностью выключен.
Это может скрывать реальные нарушения style guide. Нужно либо включить правило и исправить компоненты, либо оставить точечные overrides для App.vue/generated/vendor.
- TypeScript ослаблен.
В tsconfig.app.json:
noUnusedLocals: false;noUnusedParameters: false;erasableSyntaxOnly: false.
Включать все сразу рискованно, но в плане нужно постепенное усиление.
shared/ui-kitисключен из ESLint и tsconfig.
Это может быть осознанно, если UI-kit является импортированным/vendor-кодом. Но перед сдачей нужно описать это в README или проверить отдельно.
- Bundle size требует анализа.
Главный chunk 725 KB minified. Вероятные причины:
- общий UI-kit entrypoint;
- heavy chart components;
- barrel exports;
- dashboard импортирует много компонентов и данных в одном route chunk.
P3. Навигация, naming и документация
- Report-tab сейчас является частью dashboard.
ReportsView.vue перенесен в src/pages/dashboard/ui. Если позже появится самостоятельный route /reports, его нужно выделять в отдельный pages/reports слайс, а не держать как скрытую вкладку dashboard.
docs/components/heder.cssсодержит опечатку в имениheder.
Если эти файлы являются reference layout docs, нужно оставить как есть или переименовать с учетом ссылок.
- Sidebar localStorage key мигрирован с legacy namespace.
Выполнено: новый ключ analytical-panel:app-sidebar:expanded-group-ids живет в widgets/protected-app-layout/model/useProtectedAppSidebarNavigation.ts; старый mostovik:app-sidebar:expanded-group-ids читается только как migration fallback.
- README слишком короткий для передачи заказчику.
Нужны разделы:
- архитектура;
- команды;
- env;
- generated API;
- тестирование;
- known limitations;
- FSD rules.
5. Целевая FSD-структура
Цель не в том, чтобы механически создать все возможные слои, а в том, чтобы у каждой папки была понятная ответственность.
src/
app/
layouts/
protected-route/
ui/
index.ts
providers/
query/
store/
router/
router/
routes.ts
guards.ts
index.ts
styles/
global.css
tokens.css
index.ts
pages/
auth/
model/
loginSchema.ts
useAuthLogin.ts
ui/
AuthPage/
AuthLoginForm/
index.ts
dashboard/
api/
competenceMapAnalyticsApi.ts
model/
filters.ts
queryKeys.ts
types.ts
useDashboardAnalytics.ts
useDashboardFilters.ts
useDashboardTabs.ts
sortRows.ts
formatters.ts
tooltip.ts
adapters.ts
ui/
DashboardPage.vue
DashboardToolbar.vue
DashboardTabs.vue
DashboardKpiGrid.vue
DashboardStatePanel.vue
DashboardSkeletonGrid.vue
DashboardAnalyticsGrid.vue
AssociationCompositionCard.vue
SezPriorityDirectionsCard.vue
TechnoparkAreaDistributionCard.vue
ClusterSpecializationsCard.vue
EmployeeStatsCard.vue
TrendCard.vue
RegionRankingCard.vue
TopIndustriesCard.vue
RegionRatingReport.vue
CompetenceMapFiltersDialog.vue
ChartTooltipPortal.vue
index.ts
widgets/
protected-app-layout/
model/
ui/
index.ts
features/
logout-session/
model/
ui/ # только если появится самостоятельная кнопка/меню
index.ts
entities/
user/
model/
index.ts
shared/
api/
client/
auth/
competence-map-analytics/
generated-api/
index.ts
config/
routes.ts
env.ts
lib/
number/
text/
navigation/
model/
generated-zod/
index.ts
ui/
app-header/ # только pure UI или перенести в widget
app-sidebar/ # только pure UI или перенести в widget
breadcrumb/
logo/
index.ts
ui-kit/
README.md
index.ts
Возможные корректировки после анализа переиспользования:
features/logout-sessionможно не создавать, если logout останется только детальюprotected-app-layout.entities/userможно оставить как доменную сущность, несмотря на warning Steiger, если ожидаются роли, профиль, права доступа и дальнейшее использование. Если это не планируется,useCurrentUserможно перенести ближе кprotected-app-layout.widgets/competence-map-filtersиwidgets/competence-map-toolbarуже слиты вpages/dashboard/ui, потому что сейчас используются только dashboard-страницей.src/shared/ui-kitлучше трактовать как локальный design-system/vendor package. Его не нужно ломать ради буквального FSD, но нужно документировать исключение.
6. Детальный пошаговый план рефакторинга
Этап 0. Зафиксировать baseline и защиту от регрессий
-
Зафиксировать текущий список проверок:
bun run lint bun run typecheck bun run test:unit bun run build bun run test:e2e -
Проверить
bun run test:e2eотдельно и зафиксировать, проходит ли он на текущей машине. -
Добавить временный документ
docs/refactoring-regression-checklist.mdили секцию в этом файле с ручными сценариями:- переход
/dashboardбез токена ведет на/auth; - login form показывает ошибки пустых полей;
- успешный login редиректит на dashboard;
- dashboard all state;
- вкладка
ОЭЗ; - вкладка
Технопарки; - вкладка
Кластеры; - фильтры: открыть, выбрать, отменить, сбросить, применить;
- сортировка ranking;
- сортировка detail tables;
- region rating tab;
- loading state;
- error state;
- mobile viewport.
- переход
-
Добавить Playwright route mocks для analytics endpoints, чтобы E2E dashboard не зависел от внешнего backend.
-
Снять baseline screenshots для desktop и mobile:
- auth page;
- dashboard all;
- dashboard sez;
- dashboard technoparks;
- dashboard clusters;
- filters dialog;
- report/rating table.
-
Зафиксировать bundle baseline:
- размер
index-*.js; - размер route chunk
dashboard-*.js; - CSS sizes;
- build warnings.
- размер
Этап 1. Настроить архитектурные проверки
Статус: выполнено для текущего объема.
-
Выполнено: Steiger добавлен в dev tooling:
{ "scripts": { "lint:fsd": "steiger src" } } -
Выполнено: добавлен
steiger.config.ts:- исключить
src/shared/ui-kit/**как локальный vendor/design-system; - исключить generated files, но не исключать public API wrappers;
- оставить strict checks для
pages,widgets,features,entities,shared/api,shared/ui.
- исключить
-
Выполнено: добавлены
checkиcheck:full:{ "scripts": { "check": "bun run lint && bun run lint:fsd && bun run typecheck && bun run test:unit && bun run build" } } -
Выполнено:
lint:fsdвключен вcheck, потому что текущие FSD-нарушения ручного прикладного кода устранены. -
Оставшийся план по усилению ESLint:
- убрать глобальное отключение
vue/multi-word-component-names; - заменить его точечными exceptions;
- добавить правила для import boundaries, если Steiger не покрывает нужные Vue-сценарии;
- добавить правило против прямых импортов из
shared/api/generated-apiвнеshared/api.
- убрать глобальное отключение
Этап 2. Привести app слой в порядок
Статус: выполнено.
-
Выполнено:
src/router/index.tsперенесен вsrc/app/router/index.ts. -
Выполнено: router разделен:
routes.ts: route records и lazy imports;guards.ts: auth guard и title guard;index.ts: создание router.
-
Выполнено: импорт в
src/main.tsобновлен на./app/router. -
Выполнено: compatibility re-export не оставлялся, потому что импорт был единственный и безопасно обновлен.
-
Выполнено: route-level lazy loading сохранен в
routes.ts. -
Выполнено: app-level styles перенесены из
src/style.cssвsrc/app/styles/base.css:- reset;
- html/body/#app;
- tokens;
- global font-family;
- минимальные общие helpers.
-
Выполнено:
src/style.cssоставлен entrypoint, который импортируетapp/styles/base.cssи dashboard CSS.
Этап 3. Сформировать настоящий pages/dashboard слайс
Статус: базовое перемещение выполнено.
-
Создать сегменты:
src/pages/dashboard/ui;src/pages/dashboard/modelна следующем этапе;src/pages/dashboard/apiпри выделении dashboard API wrappers;src/pages/dashboard/libпри выделении pure helpers.
-
Выполнено:
src/views/DashboardView.vueперенесен вsrc/pages/dashboard/ui/DashboardPage.vue. -
Выполнено:
src/views/ReportsView.vueперенесен вsrc/pages/dashboard/ui/ReportsView.vue, потому что это вкладка dashboard, а не самостоятельная route page. -
Выполнено:
src/pages/dashboard/index.tsобновлен:export { DashboardPage } from './ui' -
Выполнено: зависимость
pages/dashboardотsrc/viewsудалена. -
Выполнено:
src/viewsудален, потому что других потребителей не осталось. -
Выполнено: на этом шаге логика не дробилась, были только механические переносы и обновление импортов.
-
Проверки после шага:
bun run lint bun run typecheck bun run test:unit bun run build
Этап 4. Перенести и разделить dashboard model
Статус: выполнено для первого безопасного прохода.
-
Выполнено:
src/stores/dashboard.tsперенесен вsrc/pages/dashboard/model/dashboardStore.ts. -
Выполнено: API
useDashboardStoreсохранен, чтобы не менять всю страницу одновременно. -
Выполнено: импорты обновлены:
DashboardPage.vue;ReportsView.vue;CompetenceMapFilters.vue;- tests.
-
Выполнено:
src/storesудален. -
Выполнено:
dashboardStore.tsразделен на отдельные model-файлы:types.ts: domain/view types;constants.ts: цветовые карты и константы;filters.ts:defaultAnalyticsFilters,normalizeAnalyticsFilters,toApiParams, cache key;emptyState.ts: empty response factories;error.ts: dashboard error resolving;chartData.ts: chart/donut adapters;formatters.ts: number, metric, area, growth, plural forms;sorting.ts: sorting helpers;tooltip.ts: tooltip builders and escaping.
-
Выполнено: dashboard API wrappers вынесены в
src/pages/dashboard/api/competenceMapAnalyticsApi.ts. -
Выполнено: unit tests перенесены рядом с model и расширены до 15 dashboard-кейсов:
- filter normalization;
- filter draft mapping;
- filter definitions;
- API params;
- cache key;
- empty state;
- chart adapters;
- sorting;
- formatting/pluralization;
- tooltip escaping.
-
Оставшийся подшаг: проверить, можно ли отказаться от Pinia для dashboard.
Предпочтительный target:
- server state: TanStack Query;
- page UI state: composables/ref внутри
pages/dashboard/model; - Pinia оставить только если состояние нужно глобально между routes.
-
Ручной cache layer уже удален; если отказ от Pinia слишком рискованный, оставить setup store на один этап и покрыть page state/component behavior тестами.
Этап 5. Убрать direct imports generated API
Статус: выполнен минимальный безопасный facade, осталась документация и возможная детализация entrypoints.
-
Выполнено: создан public facade
src/shared/api/index.ts, который экспортирует используемые endpoint functions и типы. -
Возможный следующий уровень: создать более явные public API wrappers:
src/shared/api/authentication/ index.ts src/shared/api/user/ index.ts src/shared/api/competence-map-analytics/ index.ts src/shared/api/generated/ README.md -
В wrappers экспортировать только нужные функции и типы:
- login mutation;
- token refresh URL/handler, если нужен;
- current user endpoint;
- analytics endpoints;
- endpoint schemas/types, которые реально нужны приложению.
-
Выполнено: потребители переведены на public facade:
entities/user/model/useCurrentUser.ts;pages/auth/model/useAuthLogin.ts;pages/auth/ui/AuthPage/AuthLoginForm/AuthLoginForm.vue;- dashboard model.
-
Запретить прямые импорты из
shared/api/generated-apiвнеshared/apiчерез ESLint/Steiger convention. -
Добавить README в
shared/api/generated-api:- generated by Orval;
- do not edit manually;
- run
bun run apigen; - public wrappers live one level above.
Этап 6. Разобрать DashboardPage.vue на компоненты
Статус: выполнен первый безопасный проход. DashboardPage.vue уменьшен примерно с 1712 до 487 строк без изменения визуального поведения.
Порядок дробления шел от простого к сложному.
-
Выполнено частично: shell оставлен в
DashboardPage.vue, tabs вынесены:DashboardTabs.vue.
-
Выполнено: KPI вынесены:
DashboardKpiGrid.vue;- отдельный
DashboardKpiCard.vueпока не создавался, чтобы не дробить простую разметку без необходимости.
-
Выполнено: state blocks вынесены:
DashboardStatePanel.vue;DashboardSkeletonGrid.vue;- отдельные error/empty components пока не нужны.
-
Выполнено: primary cards/panels вынесены:
CompositionCard.vue;AssociationsCompositionPanel.vue;SezCompositionPanel.vue;TechnoparkCompositionPanel.vue;ClusterSpecializationsPanel.vue.
-
Выполнено: secondary cards вынесены:
RegionRankingCard.vue;TopIndustriesCard.vue.
-
Выполнено: trend вынесен:
TrendCard.vue;- отдельные
ClusterCreationTrendChart.vueиOpkParticipationTrendChart.vueпока не создавались, потому что общийTrendCard.vueсохраняет связную ответственность.
-
Выполнено: employee stats вынесены:
EmployeeStatsCard.vue.
-
Выполнено: tooltip portal вынесен:
ChartTooltipPortal.vue;- tooltip builders и escaping вынесены в
model/tooltip.ts.
-
Оставшийся возможный шаг: вынести table primitives только если повторяются:
SortableHeaderButton.vue;AnalyticsTable.vue;RankingTable.vue.
-
Не создавать слишком мелкие компоненты ради количества файлов. Компонент должен появляться, если:
- у него отдельная ответственность;
- его template читается независимо;
- его props API естественный;
- он уменьшает размер родителя;
- он покрывается тестом или визуально проверяется.
- После каждого выноса:
- проверить props typing;
- не потерять
aria-*; - не потерять
key; - не изменить CSS class names до отдельного CSS шага;
- прогнать проверки.
Этап 7. Перенести фильтры и toolbar в правильное место
Статус: ownership исправлен, первый проход внутреннего разбиения фильтров выполнен.
-
Выполнено: ownership решен в пользу dashboard page:
widgets/competence-map-filtersперенесен вpages/dashboard/ui/CompetenceMapFilters.vue;widgets/competence-map-toolbarперенесен вpages/dashboard/ui/CompetenceMapToolbar.vue.
-
Обоснование: фильтры и toolbar сейчас завязаны на конкретную dashboard page.
Причина: фильтры завязаны на конкретные
AnalyticsFilters,dashboard.filtersResponse,dashboard.applyFilters. -
Выполнено: логика фильтров разделена:
filterDraft.ts: draft mapping и nullable conversion;filterDefinitions.ts: option definitions;CompetenceMapFilterField.vue: select field UI.
-
Выполнено:
competence-map-toolbarперенесен в dashboard page, потому что он только про breadcrumb этой страницы. -
Если toolbar станет общим, сделать pure shared component:
- props:
breadcrumbItems; - no dashboard imports;
- no route assumptions.
- props:
Этап 8. Очистить shared/ui от app/business logic
Статус: AppHeader и AppSidebar очищены в первом проходе.
-
Выполнено:
AppHeaderразделен по ответственности:shared/ui/app-header: только layout, slots, display props;widgets/protected-app-layout/ui: current user, logout, router callbacks.
-
Выполнено: logout orchestration перенесен из
shared/api:Возможные варианты:
- текущий выбранный вариант:
widgets/protected-app-layout/model/useProtectedLogout; features/logout-sessionне создавался, потому что Steiger корректно считает отдельный feature slice преждевременным при одном потребителе;- token primitives и low-level logout endpoint facade остались в
shared/api.
Если logout начнет переиспользоваться несколькими flows, тогда его можно поднять в
features/logout-session. - текущий выбранный вариант:
-
Оставить token primitives в
shared/api, потому что API client и refresh interceptor используют их глобально. -
Выполнено:
AppSidebarсделан более чистым:- route-specific preload убран из shared component;
- callbacks на click/hover передаются из widget owner;
- route matching/navigation строятся в widget model.
-
Выполнено: localStorage key перенесен из shared UI в widget model.
-
Выполнено:
mostovik:app-sidebar:expanded-group-idsисправлен миграционно:- новый ключ:
analytical-panel:app-sidebar:expanded-group-ids; - при чтении есть fallback на старый ключ, чтобы не ломать существующее состояние.
- новый ключ:
Этап 9. Привести auth page к FSD и Vue best practices
Статус: выполнено для первого безопасного прохода.
-
Выполнено: schema перенесена из
AuthLoginForm.vueвpages/auth/model/loginSchema.ts. -
Выполнено: self-import убран:
- было:
import { useAuthLogin } from '@/pages/auth'; - стало: импорт из локального model-сегмента.
- было:
-
Выполнено: тип
Loginимпортируется через public facade@/shared/api, а не generated path. -
Выполнено: redirect safety вынесен в
pages/auth/model/redirect.ts:- текущая проверка
/и не//правильная; - добавлен unit test на внешний/protocol-relative redirect.
- текущая проверка
-
Улучшить submit flow:
- оставить защиту от double submit;
- проверить disabled state для username input тоже, если UX требует;
- оставить accessible error messages.
-
Выполнено частично:
- empty validation;
- whitespace validation;
- safe redirect.
-
Оставшийся подшаг:
- API error message;
- full submit flow component/E2E tests.
Этап 10. Перенести и изолировать CSS
Статус: первый безопасный перенос выполнен, компонентная изоляция осталась.
-
Выполнено:
src/style.cssразделен:src/app/styles/base.css;src/pages/dashboard/ui/styles/dashboard.css;src/pages/dashboard/ui/styles/sections/layout.css;src/pages/dashboard/ui/styles/sections/composition.css;src/pages/dashboard/ui/styles/sections/tables.css;src/pages/dashboard/ui/styles/sections/charts-and-industries.css;src/pages/dashboard/ui/styles/sections/responsive.css.
-
Выполнено: dashboard-specific CSS перенесен без переписывания selectors.
-
Следующий шаг: распределить CSS по компонентам:
- tabs styles рядом с
DashboardTabs.vue; - KPI styles рядом с
DashboardKpiGrid.vue; - card styles рядом с конкретными cards;
- table styles рядом с table components;
- tooltip styles рядом с
ChartTooltipPortal.vue; - reports styles рядом с
RegionRatingReport.vue.
- tabs styles рядом с
-
Избегать изменения class names одновременно с переносом template.
-
Если используется
:deep, оставить комментарий только там, где это действительно override UI-kit internals. -
Проверить mobile breakpoints после каждого крупного переноса.
Этап 11. Улучшить server state и dashboard data model
Статус: первый проход выполнен.
-
Выполнено: query key factories вынесены:
export const competenceMapAnalyticsQueryKeys = { all: ['competence-map-analytics'] as const, list: (filters: AnalyticsFilters) => [...competenceMapAnalyticsQueryKeys.all, normalizeAnalyticsFilters(filters)] as const, filters: (filters: AnalyticsFilters) => ['competence-map-analytics-filters', normalizeAnalyticsFilters(filters)] as const, } -
Проверить сериализуемость фильтров и отсутствие
undefined. -
Выполнено:
createAnalyticsCacheKeyудален, строковый key больше не используется. -
Выполнено: ручной response cache удален, server state опирается на Vue Query cache.
-
Оставшийся подшаг: решить, как показывать прошлые данные при смене фильтров:
- использовать Vue Query cache;
- использовать
placeholderData, если нужно; - явно покрыть loading UX тестами.
-
Перенести
resolveApiErrorMessagedashboard на общийshared/api/error. -
Вынести DTO-to-view adapters:
toCompositionDonutData;toTrendChartData;toRegionReportRows;toEmployeeStatItems.
-
Добавить unit tests на все adapters.
Этап 12. Оптимизировать public APIs и barrel exports
-
Проверить все
index.ts:- public API должен экспортировать только нужный контракт;
- не использовать
export *для всего подряд, если это открывает internals; - внутри слайса не импортировать из собственного public API.
-
Для
shared/uiрассмотреть subpath imports:@/shared/ui/breadcrumb;@/shared/ui/logo;@/shared/ui/app-header;@/shared/ui/app-sidebar.
-
Для
shared/ui-kitоценить:- оставить единый
@gisp/ui-kit-shadcn-vue, если tree-shaking нормальный; - либо добавить component-level entrypoints, если bundle analysis подтвердит проблему.
- оставить единый
-
Добавить architecture notes в
src/shared/ui-kit/lib/README.mdилиsrc/shared/ui-kit/README.md:- почему папка исключена из FSD lint;
- как импортировать компоненты;
- как добавлять новые компоненты;
- как не ломать public API.
Этап 13. Усилить тесты
-
Unit tests:
normalizeAnalyticsFilters;toApiParams;- query keys;
- sortRows with null/string/number/tie by rank;
- formatNumber;
- formatMetricValue;
- getRuPluralForm;
- formatOrganizationsText;
- escapeHtml;
- tooltip builders;
- dashboard adapters;
- auth schema;
- safe redirect helper.
-
Component tests:
- DashboardTabs emits/apply behavior;
- CompetenceMapFiltersDialog draft/apply/reset/cancel;
- RegionRankingCard sorting UI;
- TopIndustriesCard empty and filled states;
- ChartTooltipPortal visibility.
-
E2E tests:
- auth redirect;
- login validation;
- mocked authorized dashboard load;
- tab switch all/sez/technoparks/clusters;
- filter dialog;
- sort buttons;
- region rating tab;
- mobile layout smoke.
-
Visual regression:
- either Playwright screenshots with controlled mock data;
- or at minimum saved screenshots before/after refactor during manual validation.
-
Accessibility checks:
- role/aria для tables;
- focusable chart segments;
- keyboard focus in dialog;
aria-busy;- tooltips not blocking focus.
Этап 14. Bundle/performance pass
-
Добавить bundle analyzer или Vite visualizer.
-
Сравнить:
- before/after main chunk;
- dashboard route chunk;
- UI-kit chunk;
- chart chunk.
-
Проверить, почему
index-*.js725 KB:- общий import UI-kit;
- route preload;
- shared barrel;
- chart components;
- TanStack table/chart libraries.
-
Возможные оптимизации:
- лениво грузить тяжелые chart cards;
- разделить dashboard report tab;
- component-level imports из UI-kit;
- manual chunks для UI-kit/charts;
- отложенная загрузка report table.
-
Не применять оптимизации до архитектурного переноса, чтобы не смешивать причины регрессий.
Этап 15. Документация для передачи заказчику
-
Расширить README:
- стек;
- требования к Node/Bun;
- env variables;
- запуск dev;
- build/preview;
- tests;
- generated API;
- FSD structure;
- deployment notes;
- known warnings.
-
Добавить
docs/architecture.md:- слои;
- правила импортов;
- public API;
- где хранить API;
- где хранить UI;
- как добавлять новую страницу;
- как добавлять новый analytics block.
-
Добавить
docs/testing.md:- unit;
- e2e;
- visual checks;
- mock data policy.
-
Добавить
docs/generated-api.md:- Orval config;
- OpenAPI source;
- команда
bun run apigen; - что нельзя редактировать руками.
7. Предлагаемый порядок выполнения PR/коммитов
-
Tooling and baseline:
- выполнено: добавить
lint:fsd/Steiger config; - выполнено: добавить
checkиcheck:full; - добавить dashboard E2E mocks skeleton;
- выполнено: runtime не менялся на этом шаге.
- выполнено: добавить
-
App/router cleanup:
- выполнено: перенести router в app;
- выполнено: разделить routes/guards;
- выполнено: проверки.
-
Dashboard page relocation:
- выполнено:
views/DashboardView.vue->pages/dashboard/ui/DashboardPage.vue; - выполнено:
views/ReportsView.vue->pages/dashboard/ui/ReportsView.vue; - выполнено: dashboard-only widgets перенесены в
pages/dashboard/ui; - выполнено:
src/viewsудален; - выполнено: проверки.
- выполнено:
-
Dashboard model relocation:
- выполнено:
stores/dashboard.ts->pages/dashboard/model/dashboardStore.ts; - выполнено: model split на filters/constants/types/chartData/formatters/sorting/tooltip/error/emptyState;
- выполнено: API wrappers в
pages/dashboard/api; - выполнено: временно сохранен
useDashboardStoreAPI; - выполнено: удален
src/stores; - выполнено: dashboard unit-тесты расширены до 15 кейсов;
- выполнено: проверки.
- выполнено:
-
Public API wrappers:
- выполнено: shared API facade;
- выполнено: убрать generated direct imports из ручного прикладного кода;
- выполнено: исправить Steiger public-api issues в текущем объеме;
- выполнено: проверки.
-
Auth cleanup:
- выполнено: schema в model;
- выполнено: убрать self-import;
- выполнено: тесты redirect/schema;
- выполнено: проверки.
-
Dashboard pure model extraction:
- выполнено: filters/sort/formatters/tooltip/adapters;
- выполнено: queryKeys заменены на структурированные ключи;
- выполнено: unit tests;
- выполнено: проверки.
-
Dashboard UI decomposition:
- выполнено: карточки, панели, tooltip portal, tabs, states;
- выполнено: фильтры, draft helpers и select field;
- выполнено: проверки после блока.
-
Shared UI cleanup:
- выполнено частично: AppHeader стал ближе к pure UI;
- выполнено: AppSidebar стал pure UI;
- выполнено: logout orchestration перенесен в widget model;
- выполнено: protected layout composes header behavior;
- выполнено: проверки.
-
CSS relocation:
- выполнено: global split;
- выполнено: dashboard CSS split;
- screenshots;
- выполнено: проверки.
-
Bundle/performance:
- analyzer;
- chunk strategy;
- UI-kit import strategy;
- проверки.
-
Final docs and handoff:
- README;
- architecture docs;
- generated API docs;
- final full check.
8. Definition of Done
Рефакторинг можно считать завершенным, когда:
src/viewsудален;src/storesудален или обоснован как отдельный слой не нужен;src/routerперенесен вapp;pages/dashboardимеет реальные сегментыui,model,api;DashboardPage.vueстал тонкой композицией, а не файлом на 1700 строк;- dashboard pure logic покрыта unit-тестами;
- generated API не импортируется напрямую из app/pages/widgets/entities;
shared/uiне содержит бизнес-логики logout/current user/router-specific behavior;src/style.cssбольше не содержит весь dashboard;- Steiger либо проходит, либо имеет задокументированные исключения для generated/vendor UI-kit;
bun run lintпроходит;bun run typecheckпроходит;bun run test:unitпроходит;bun run buildпроходит;bun run test:e2eпроходит;- визуальные screenshots ключевых dashboard states совпадают или изменения явно согласованы;
- README и архитектурная документация достаточны для передачи заказчику.
9. Что не делать на первом проходе
- Не менять дизайн, размеры, цвета и сетку без отдельного согласования.
- Не менять backend contract и названия query params.
- Не удалять Pinia одномоментно, если это смешает слишком много причин регрессий.
- Не переписывать UI-kit внутренности до bundle/performance этапа.
- Не добавлять
featuresради формального наличия слоя. - Не делать массовое переименование CSS classes одновременно с переносом компонентов.
- Не включать все strict TS flags одним коммитом.
10. Статус первого большого refactor-pass
Защитная инфраструктура, FSD-перенос и первая декомпозиция выполнены:
- Добавлен Steiger script/config с осознанными исключениями для generated/vendor-зон.
- Добавлены
checkиcheck:full. - Добавлены file-size thresholds и FSD policy в
docs/review.md. - Router перенесен в
src/app/routerи разделен наroutes.ts,guards.ts,index.ts. - Dashboard page перенесен из
src/viewsвsrc/pages/dashboard/ui. ReportsViewперенесен в dashboard page slice как вкладка, а не самостоятельная route page.- Dashboard-only widgets перенесены из
src/widgetsвsrc/pages/dashboard/ui. - Dashboard store перенесен из
src/storesвsrc/pages/dashboard/model. - Dashboard model разделен на типы, константы, фильтры, empty state, error, chart data, форматтеры, сортировку и tooltip helpers.
- Dashboard API wrappers вынесены в
src/pages/dashboard/api. - Dashboard UI разделен на композиционные компоненты в
src/pages/dashboard/ui/components. src/style.cssпревращен в entrypoint, dashboard CSS разнесен по section-файлам.- Generated API imports закрыты public facade
@/shared/api. AppHeaderочищен от прямого router/logout ownership, поведение перенесено вProtectedAppLayout.- Фильтры dashboard разделены на
filterDraft.ts,filterDefinitions.tsиCompetenceMapFilterField.vue. AppSidebarочищен от router/preload/localStorage ownership; поведение перенесено вuseProtectedAppSidebarNavigation.- Sidebar storage key мигрирован на
analytical-panel:app-sidebar:expanded-group-idsс fallback на legacy key. - Dashboard server state переведен на структурированные Vue Query keys.
- Ручной
analyticsResponseCache/filtersResponseCacheудален. - Auth schema и safe redirect helper вынесены в
pages/auth/model. - Logout orchestration перенесен из
shared/apiвwidgets/protected-app-layout/model/useProtectedLogout.ts. - Handoff-документация добавлена:
docs/architecture.md,docs/generated-api.md,docs/testing.md, расширенREADME.md. - Unit/component tests расширены до 18 кейсов.
bun run lint:fsdпроходит без ошибок.
Следующий безопасный этап:
-
Добавить полный component-test flow для
CompetenceMapFilters.vue:- open/sync draft;
- apply;
- reset;
- cancel;
- disabled/loading state.
-
Закрепить dashboard loading/placeholder UX тестами после удаления ручного cache layer.
-
Продолжить CSS-изоляцию:
- разнести
layout.css,tables.css,composition.css,charts-and-industries.cssближе к компонентам; - не менять class names одновременно с переносом.
- разнести
-
Добавить dashboard E2E с mocked authorized state и визуальные screenshots для desktop/mobile.