Skip to content

Kiến trúc kỹ thuật

Tổng thể

compass.conan1.com          api.conan1.com              D1
Cloudflare Pages            Cloudflare Worker           conan-compass
─────────────────           ──────────────────          ─────────────
Vite 8 + React 19           Hono                        capabilities
Tailwind v4                 SQL thuần                   brands
shadcn + radix-ui           Access JWT verify           brand_capabilities
Heroicons                                               processes / activities
Motion                                                  applications
React Router 7                                          data_entities
                                                        technologies
                                                        value_streams
                                                        initiatives / kpis
                                                        users / audit_log

Vì sao tải toàn bộ mô hình một lần

Mô hình chỉ có vài chục năng lực và một nắm Brand — tổng cộng vài chục KB JSON. Khi khởi động, CompassProvider tải:

  1. /v1/me
  2. /v1/capabilities
  3. /v1/brands
  4. /v1/architecture — toàn bộ tầng thực thi và chuyển đổi trong một request
  5. /v1/brands/:code/capabilities cho từng Brand

Sau đó mọi màn hình phân tích được tính ngay tại client. Cross-Brand Comparison, Heatmap, Gaps, Risk Map và các thẻ trên Overview không cần thêm một round-trip nào, và không cần một endpoint riêng cho từng biểu đồ.

Sau mỗi lần ghi, ứng dụng gọi reload() để tải lại — đơn giản và luôn nhất quán với server. Nếu sau này mô hình phình lên hàng nghìn năng lực, đây là chỗ đầu tiên cần đổi.

Cấu trúc mã nguồn

compass.conan1.com/
  src/
    lib/
      api.ts        Client gọi api.conan1.com
      store.tsx     CompassProvider — nguồn dữ liệu duy nhất
      scales.ts     Thang điểm, công thức risk và gap
      types.ts
    i18n/
      dictionary.ts Từ điển Việt – Anh
      index.tsx     I18nProvider, useI18n
    components/
      ui/           shadcn
      AppShell.tsx  Sidebar, topbar, chuyển trang
      Score.tsx     ScoreChip, RiskChip, GapChip
      *Dialog.tsx   Form thêm/sửa
    pages/          Mỗi route một file

Nguồn dữ liệu duy nhất

useCompass() trả về mô hình đã chuẩn hoá cùng vài chỉ mục dựng sẵn:

TrườngÝ nghĩa
byIdMap<capability_id, Capability>
childrenOfMap<parent_id | null, Capability[]> đã sắp xếp
pathOf(id)Đường dẫn từ L1 tới năng lực đó, dùng cho breadcrumb
maps[brandCode][capabilityId]Đánh giá của một Brand
archTầng thực thi: process, application, data, technology, value stream, initiative, KPI
canEditModel, canEditBrand(code), canEditScopeQuyền, tính từ vai trò và phạm vi đang chọn

Các hook phụ trợ trả về dữ liệu đã lọc theo bộ chọn phạm vi, nên mỗi trang chỉ cần lo phần trình bày:

HookTrả về
useScopedBrands()Brand trong phạm vi
useScopedRows()Mọi đánh giá năng lực trong phạm vi, đã làm phẳng
useScopeBrandId()brand_id hiện tại, hoặc null ở Enterprise
inScope(rows, brandId)Lọc bản ghi của Brand cộng bản ghi dùng chung
indexLinks(links, from, to)Gom bảng nối thành Map<a, b[]>

Animation

AppShell bọc nội dung trong AnimatePresence mode="wait" với keylocation.pathname: fade + dịch 6px, 180ms. Danh sách và thẻ fade-in so le, độ trễ tối đa 0.24s. prefers-reduced-motion được tôn trọng ở tầng CSS.

Toàn bộ cấu hình animation nằm trong src/lib/motion.ts (enter()pageTransition()), không rải rác trong từng trang.

Tab ẩn

Animation chạy trên requestAnimationFrame, mà trình duyệt bóp nghẹt rAF ở tab đang ẩn. Một phần tử bắt đầu ở opacity: 0 sẽ đứng yên vô hình cho tới khi người dùng chuyển sang tab đó — nghĩa là trang mở ở tab nền hiện ra trắng trơn.

Vì vậy enter()pageTransition() kiểm tra document.visibilityState khi mount: nếu tài liệu đang ẩn thì bỏ animation và render thẳng trạng thái cuối.