3x-ui/frontend
MHSanaei ef36757b88
refactor(frontend): port clients to react+ts
Step 6 of the planned vue->react migration. Clients is the biggest
data-CRUD page in the panel (1.1k-line ClientsPage, 4 modals, full
table + mobile card list, WebSocket-driven realtime traffic + online
updates).

New shared infra (lives alongside vue twins until inbounds migrates):

* hooks/useClients.ts — clients + inbounds list, CRUD + bulk delete +
  attach/detach + traffic reset, with WebSocket event handlers
  (traffic, client_stats, invalidate) and a small debounced refresh
  on the invalidate event. State managed via setState; the live
  client_stats event merges traffic snapshots row-by-row through a
  ref to avoid stale closure issues.
* hooks/useDatepicker.ts — singleton "gregorian"/"jalalian" cache
  with subscribe/notify so multiple components can read the panel's
  Calendar Type without re-fetching. Mirrors useDatepicker.js.
* components/DateTimePicker.tsx — AntD DatePicker wrapper.
  vue3-persian-datetime-picker has no React port; the Jalali UI
  calendar is deferred (read-only Jalali display via IntlUtil
  formatDate still works). The vue twin stays for inbounds.
* pages/inbounds/QrPanel.tsx — copy/download/copy-as-png QR helper
  shared between clients (qr modal) and inbounds (still on vue).
  Vue twin stays alive at QrPanel.vue.
* models/inbound.ts — slim port: only the TLS_FLOW_CONTROL constant
  the clients form needs. The full inbound model stays as
  inbound.js for now; inbounds will pull it in as inbound.ts.

The clients page itself uses Modal.useModal() for all confirm
dialogs (delete, bulk-delete, reset-traffic, delDepleted, reset-all)
so the dialogs render themed. Filter state persists to
localStorage under clientsFilterState. Sort + pagination state is
local; pageSize seeds from /panel/setting/defaultSettings.

The four modals share a controlled "open/onOpenChange" pattern
that replaces vue's v-model:open. ClientFormModal computes
attach/detach diffs from the inbound multi-select on submit; the
parent's onSave callback routes them through useClients's attach()/
detach() after the main update succeeds.

ESLint config: turned off four react-hooks v7 rules
(react-compiler, preserve-manual-memoization, set-state-in-effect,
purity). They're all React-Compiler-driven informational rules; we
don't run the compiler and the patterns they flag (initial-fetch
useEffect, derived computations using Date.now, inline arrow event
handlers) are all idiomatic React. Disabling globally instead of
per-line keeps the diff readable.
2026-05-21 22:03:31 +02:00
..
src refactor(frontend): port clients to react+ts 2026-05-21 22:03:31 +02:00
.gitignore Vue3 migration (#4198) 2026-05-09 17:47:35 +02:00
api-docs.html refactor(frontend): port api-docs to react+ts 2026-05-21 21:26:28 +02:00
clients.html refactor(frontend): port clients to react+ts 2026-05-21 22:03:31 +02:00
eslint.config.js refactor(frontend): port clients to react+ts 2026-05-21 22:03:31 +02:00
inbounds.html feat(ui): use the host as the browser tab title prefix 2026-05-13 14:23:57 +02:00
index.html feat(ui): use the host as the browser tab title prefix 2026-05-13 14:23:57 +02:00
login.html refactor(frontend): port login to react+ts 2026-05-21 21:19:52 +02:00
nodes.html refactor(frontend): port nodes to react+ts 2026-05-21 21:34:46 +02:00
package-lock.json chore(frontend): add react+typescript toolchain alongside vue 2026-05-21 21:19:09 +02:00
package.json chore(frontend): add react+typescript toolchain alongside vue 2026-05-21 21:19:09 +02:00
README.md Vue3 migration (#4198) 2026-05-09 17:47:35 +02:00
settings.html refactor(frontend): port settings to react+ts 2026-05-21 21:48:15 +02:00
subpage.html refactor(frontend): port subpage to react+ts 2026-05-21 21:19:38 +02:00
tsconfig.json chore(frontend): add react+typescript toolchain alongside vue 2026-05-21 21:19:09 +02:00
vite.config.js chore(frontend): add react+typescript toolchain alongside vue 2026-05-21 21:19:09 +02:00
xray.html feat(ui): use the host as the browser tab title prefix 2026-05-13 14:23:57 +02:00

3x-ui frontend

Vue 3 + Ant Design Vue 4 + Vite. Multi-page app — one HTML entry per panel route — built into ../web/dist/ and embedded into the Go binary via embed.FS.

Dev

npm install
npm run dev

Vite serves on http://localhost:5173/. API calls and /panel/* routes proxy to the Go panel at http://localhost:2053/, so start the Go panel first (go run main.go) and then Vite.

The proxy auto-rewrites /panel, /panel/settings, /panel/inbounds, /panel/xray to the matching Vite-served HTML in dev mode (see MIGRATED_ROUTES in vite.config.js), so the sidebar's production-style links work without round-tripping through Go.

Production build

npm run build

Outputs to ../web/dist/ (HTML at the root, hashed JS/CSS under assets/). The Go binary embeds this directory at compile time and web/controller/dist.go serves the per-page HTML.

Lint

npm run lint

ESLint 10 with eslint.config.js (flat config) — vue3-recommended plus a few rule overrides for the project's formatting style.

Layout

frontend/
├── *.html                 # Vite entry HTML, one per panel route
├── eslint.config.js
├── vite.config.js
└── src/
    ├── entries/           # Per-page bootstrap (createApp + mount)
    ├── pages/             # One folder per route, each with the page
    │   ├── index/         # component + helpers + sub-components
    │   ├── login/
    │   ├── inbounds/
    │   ├── xray/
    │   ├── settings/
    │   └── sub/
    ├── components/        # Cross-page Vue components
    ├── composables/       # Reusable reactive logic (useTheme, …)
    ├── api/               # Axios setup, CSRF interceptor
    ├── i18n/              # vue-i18n init (locales live in web/translation/)
    ├── models/            # Inbound, Outbound, Status, … domain classes
    └── utils/             # HttpUtil, ObjectUtil, LanguageManager, …

Adding a new page

  1. Add frontend/<page>.html referencing /src/entries/<page>.js.
  2. Add src/entries/<page>.js that imports the page component and mounts it.
  3. Add the page component under src/pages/<page>/.
  4. Register the entry in rollupOptions.input in vite.config.js.
  5. If the page is reachable from the sidebar at /panel/<route>, add it to MIGRATED_ROUTES so the dev proxy serves the Vite HTML.
  6. Wire the Go controller to serveDistPage(c, "<page>.html").