3x-ui/docs/migrations/vue3-phase1-inventory.md
MHSanaei ebe57ef273
feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.

AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.

TableSortable.vue: the biggest Vue 3 rewrite of this phase.

  - $listeners is gone — replaced by inheritAttrs: false +
    explicit attrs forwarding
  - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
    slots object — just iterate Object.keys(this.slots) and forward
  - Vue 2 h(tag, { props, on, scopedSlots }, children) →
    Vue 3 h(tag, { ...props, ...on }, slotsObject)
  - 'a-table' string → resolveComponent('a-table') so app.use(Antd)
    registration is honored
  - inject: ['sortable'] (Options API) → inject('sortable', null)
    (Composition API) inside the trigger child
  - beforeDestroy → beforeUnmount
  - customRow's return shape flattened (no nested props/on/class)

Two intentional skips, documented in the migration doc:

  - aClientTable.html — slot fragments, not a component. Migrates
    inline with inbounds.html (new Phase 5f).
  - aPersianDatepicker.html — wraps a Persian-only third-party
    lib; defer until settings.html lands.

Build verified with vite 8.0.11.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 11:52:52 +02:00

15 KiB
Raw Blame History

Vue 3 + Ant Design Vue 4 Migration — Phase 1 Inventory

Branch: vue3-migration Source state: Vue 2 + Ant Design Vue 1.7.8, no build step, Go template-driven.

Scope

  • 69 HTML templates, ~17,650 total lines
  • Largest pages:
    • web/html/xray.html — 2,360 lines
    • web/html/index.html — ~1,700 lines
    • web/html/settings.html — 720 lines
    • web/html/settings/xray/outbounds.html — 263 lines

Vue 2 → Vue 3 breakage surface

Pattern Count Files Severity Notes
{{ x | filter }} 0 0 none Filters removed in Vue 3 — but we don't use any.
<template slot="X"> 233 36 medium Rewrite to <template #X>. Mechanical.
slot-scope="X" (incl. above) 275 total 40 medium Rewrite to v-slot:name="X". Mechanical.
scopedSlots: { ... } (in JS column defs) 49 4 medium Vue 3 removed scopedSlots. All slots are now scoped. Replace with slots: { ... } or rely on template slot binding only.
new Vue({...}) mounts ~30+ 30 medium Replace with createApp({...}).mount('#id'). Each page mounts its own Vue instance.
Vue.use / Vue.component / Vue.prototype <49 (after subtracting mounts) various medium Replace with app.use / app.component / app.config.globalProperties.
$listeners / $on / $off / $once / $children 4 4 low Mostly inside aTableSortable.html. $listeners merged into $attrs. Event-bus methods removed — need an explicit emitter (mitt, or component refs).
inline-template / functional 1 1 trivial One occurrence.
.sync modifier 0 0 none Removed in Vue 3, replaced by v-model:propName. We don't use it.
v-model 358 36 high Default v-model on custom components changed (valuemodelValue, input event → update:modelValue). Most of these target AD-Vue components, which AD-Vue 4 already adapts to internally — but components we wrote ourselves (e.g. aClientTable, aTableSortable) need updates.
Vue.set / Vue.delete / Vue.observable 0 0 none Replaced by reactive(). Not needed.
transition class names (-enter, -leave) 1 (in qrcode_modal.html) 1 low Renamed in Vue 3: *-enter*-enter-from, *-leave-to stays, etc.
Key modifiers using keyCode (@keyup.13) 0 0 none Number key codes removed in Vue 3. We don't use them.
ref="..." attrs 21 9 low Behavior unchanged for Options API — refs still work the same.
.native event modifier 0 0 none Removed in Vue 3 (events on components are no longer "fake" by default).

Ant Design Vue 1.x → 4.x breakage surface

Total <a-*> component instances: 3,145 across 63 files. This is the bulk of the migration cost.

The most-used components (rough estimate from grep):

  • a-input, a-select, a-button — heavily used; props mostly stable, but <a-select-option> slot syntax changed
  • a-form + a-form-item — Form API was substantially redesigned in AD-Vue 3+. We use it lightly (layout="vertical", label-col, wrapper-col); migration is mechanical but every form needs touching.
  • a-table — column definitions with scopedSlots (49 instances) need to become slots: { customRender: 'name' } or use template slot binding directly.
  • a-modalv-model on a-modal changed: v-model="visible"v-model:open="visible" (or v-model:visible depending on version). Every modal needs updating.
  • a-iconremoved as a generic component in AD-Vue 4. Each icon must be imported individually from @ant-design/icons-vue. We use ~233 a-icon references — mostly via type="..." attribute. Likely the single highest-friction change.
  • a-tooltip, a-popover, a-drawer — slot-based titles (<template slot="title"><template #title>).
  • a-collapse + a-collapse-panel — header slot syntax changes.
  • a-tabs + a-tab-panetab slot rewrite, possibly renamed.
  • a-space — exists in AD-Vue 4, props stable.
  • a-tag — stable.

Key custom code

  • web/assets/js/util/index.js — utilities (HttpUtil, ObjectUtil, ClipboardManager, SizeFormatter, etc.). Framework-agnostic. Trivial migration (no Vue dependency).
  • web/assets/js/axios-init.js — axios setup. Trivial migration.
  • web/assets/js/websocket.js — websocket client. Trivial migration (we recently refactored this).
  • web/assets/js/model/{inbound,outbound,dbinbound,setting,reality_targets}.js — domain model classes. Plain JS. Trivial migration.
  • web/assets/js/subscription.js — subscription page logic.
  • Custom components in web/html/component/:
    • aClientTable.html — non-trivial; uses scoped slots and v-model
    • aSidebar.html — sidebar navigation
    • aThemeSwitch.html — theme picker
    • aPersianDatepicker.html — wraps a third-party datepicker
    • aTableSortable.html — uses $listeners, $on — needs explicit refactor
    • aSettingListItem.html, aCustomStatistic.html — small wrappers
    • These are the only places using the deprecated event-bus APIs (4 occurrences).

Server-side coupling

The Go layer interpolates translations directly into templates via {{ i18n "key" }}. After migration we have two options:

  • Keep Go-side i18n. Vite builds .html partials that still get processed by Go's template package. Means <script type="module"> entrypoints reference build artifacts but markup is still server-rendered. Pro: smallest change. Con: every page change forces a Vite rebuild and a Go restart.
  • Move i18n to client side. Export the translation TOML files as JSON, ship as static assets, use vue-i18n. Pro: cleaner client/server split, hot reload during dev. Con: more change, every i18n key reference in templates must be rewritten.

We will defer this decision to Phase 7. For Phases 24 we keep the Go-side approach.

Risk-ranked migration order

Order chosen so that breakage is contained and we always have a working panel:

  1. Phase 2 — Toolchain. Vite scaffold; Go binary embeds dist/ via embed.FS. New build runs in CI alongside existing static assets; legacy continues to work.
  2. Phase 3 — Utils. Migrate framework-agnostic JS first. Zero Vue dependency, zero risk.
  3. Phase 4 — login.html. Smallest page with state. Hits every Vue 2→3 syntax change and every AD-Vue 1→4 component change at small scale. Becomes the template the rest follow.
  4. Phase 5 — Medium pages and modals. index.html, settings.html, all modals. ~30 templates of 200-1000 lines each.
  5. Phase 6 — xray.html. The 2,360-line page with the inbound/outbound editors. Highest regression risk — will likely break and need fixing in the QA pass.
  6. Phase 7 — i18n decision.
  7. Phase 8 — Regression pass + delete legacy templates + cut release.

Numbers to remember

  • 68 weeks of focused work, single developer
  • 63 HTML files to touch
  • 3,145 AD-Vue component instances to validate
  • One assumption that needs confirming with the user: build step OK (yes — confirmed by choice of Vite)

Confirmed user decisions

  • Migrate to Vue 3 + Ant Design Vue 4
  • Introduce Vite build step (npm acceptable)
  • Work on a long-running vue3-migration branch
  • ⏸ i18n strategy — to be decided in Phase 7

Phase progress

  • Phase 1 — inventory (this document)
  • Phase 2 — Vite + Vue 3 + AD-Vue 4 scaffold under frontend/
  • Phase 3 — utils + models + websocket ported as ES modules
  • Phase 4 — first real page (login.html) ported
  • Phase 5 — medium pages + modals
    • 5a — theme system (composable + ThemeSwitch / ThemeSwitchLogin); wired into login
    • 5b — CustomStatistic, SettingListItem, AppSidebar, TableSortable
    • 5c — index.html dashboard

Phase 5a notes

  • aThemeSwitch.html had two near-identical components (full menu version + login popover version) plus a themeSwitcher global object. Refactored into:
    • composables/useTheme.js — single reactive theme state, toggleTheme/toggleUltra, and a pauseAnimationsUntilLeave helper. Boot side-effects (apply stored theme + persist on change) run via watchEffect. Importing the module is enough to apply the right theme before mount.
    • components/ThemeSwitch.vue — full menu version (used in the main panel sidebar).
    • components/ThemeSwitchLogin.vue — login popover version.
  • AD-Vue 4 dropped <a-icon>. The BulbFilled / BulbOutlined swap is done via <component :is="BulbIcon">.
  • Vue.component('a-theme-switch', { ... }) global registration → per-page imports.
  • this.$message.config(...) (Vue 2 instance method) → message.config(...) from ant-design-vue, called once at app boot in login.js.
  • vue-i18n bumped 10 → 11.1.4 (npm warned that v9/v10 are EOL).
  • Vite 8.0.11 build verified — npm run build succeeds, outputs web/dist/login.html + chunked JS/CSS. AD-Vue with app.use(Antd) produces a 1.5 MB chunk; we'll switch to per-component imports in a later cleanup pass.

Known gap: the legacy web/assets/css/custom.min.css styles body.dark / body.light / [data-theme="ultra-dark"]. The new login page doesn't import that CSS, so toggling theme switches AD-Vue's own components but not the panel chrome (e.g. card backgrounds). The composable still toggles the body class so behavior is correct — visual fidelity is restored when we either port custom.css to the new build or import it directly.

Phase 5b notes

Migrated four components into frontend/src/components/:

  • CustomStatistic.vue — wraps <a-statistic> with prefix/suffix slots. Trivial port; only change is Vue.component(...) → SFC.
  • SettingListItem.vue — wraps <a-list-item> with title/description/control slots and a paddings prop. Trivial port.
  • AppSidebar.vue — main panel sidebar. Surfaces tabs (Dashboard / Inbounds / Settings / Xray / Logout) with icons and the theme switcher. Two key changes from the legacy:
    • AD-Vue 4 dropped <a-icon :type="dynamic">. Replaced with a name→component map ({ dashboard: DashboardOutlined, ... }) rendered via <component :is="...">.
    • <a-drawer> slot="handle" (a non-standard prop in legacy AD-Vue 1) was replaced with a fixed-position toggle button rendered as a sibling. The drawer's :visible was renamed to :open per AD-Vue 4 conventions.
    • Tab key paths and requestUri are passed in as props (parent page knows the basePath); the legacy embedded {{ .base_path }} directly via Go templating.
  • TableSortable.vue — drag-to-reorder a-table wrapper. The biggest single Vue 3 / AD-Vue 4 rewrite in this phase:
    • $listeners (Vue 2) is gone — replaced by inheritAttrs: false + v-bind="$attrs" style forwarding via the attrs setup return.
    • scopedSlots: this.$scopedSlots → Vue 3 unifies all slots; just iterate Object.keys(this.slots) and forward.
    • Render-function shape changed: Vue 2's h(tag, { props, on, scopedSlots }, children) → Vue 3's h(tag, { ...props, ...on }, slotsObject) where slot fns are passed as the third arg.
    • 'a-table' as a plain string → resolveComponent('a-table') so app.use(Antd) registration is honored.
    • inject: ['sortable'] (Options API) inside child component swapped for inject('sortable', null) (Composition API) since the trigger now uses setup().
    • beforeDestroybeforeUnmount (Vue 3 lifecycle rename).
    • customRow's return shape flattened: Vue 2 nested attrs/on/class is now a flat object of attrs + listeners + class.

Skipped in 5b:

  • aClientTable.htmlnot actually a component. It's a fragment of <template slot="X" slot-scope="..."> blocks pulled into pages that use it. It depends on outer-scope variables (record, app, themeSwitcher, etc.) and only makes sense inlined into its consumer. Will migrate as part of inbounds.html.
  • aPersianDatepicker.html — wraps a Persian-only third-party datepicker that isn't in the critical path. Defer until settings.html migrates and we know whether to keep the legacy lib, replace with a Vue 3 wrapper, or fall back to a native HTML5 date input.

Phase 4 notes

  • Vite 6 → Vite 8.0.11 (released March 2026). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 which lists vite ^8 as a peer.
  • Multi-page Vite: each migrated page = its own entry. frontend/login.html registered alongside frontend/index.html in rollupOptions.input. Same approach for the rest of the migration.
  • New page lives at frontend/src/pages/login/LoginPage.vue with a thin entrypoint at frontend/src/login.js that calls setupAxios(), installs Antd, and mounts.
  • Three legacy features deferred to keep Phase 4 small:
    • i18n — strings hardcoded in English. Phase 7 wires up vue-i18n with the existing TOML files.
    • Theme switcher<a-theme-switch-login> was a custom component referencing a global themeSwitcher. Deferred to Phase 5 with the rest of the shared components.
    • Headline word-cycling animation — purely aesthetic, not migrated.
    • Password-manager DOM observer — the pm_* script that strips inline styles from autofilled inputs. Niche workaround, easy to add back if needed.
  • Vue 3 / AD-Vue 4 syntax changes applied:
    • <template slot="X"><template #X> (Vue 3 slot syntax)
    • <a-icon slot="prefix" type="user"><template #prefix><UserOutlined /></template> with explicit imports from @ant-design/icons-vue. AD-Vue 4 removed the generic <a-icon> — every icon must be imported individually.
    • v-model.trim on a-inputv-model:value (AD-Vue 4 uses named v-model on inputs). The .trim modifier is dropped; trim manually if needed.
    • new Vue({ el: '#app', delimiters, data, methods })createApp(LoginPage).use(Antd).mount('#app') with <script setup> and Composition API.
    • mounted()onMounted().
    • el: '#app', delimiters: ['[[', ']]'] is gone — SFCs use the standard {{ }}.

Phase 3 notes

  • web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js. All classes prefixed with export. Barrel frontend/src/utils/index.js re-exports for cleaner consumer imports.
  • Vue.prototype.$message[...] inside HttpUtil._handleMsg was replaced with a direct import { message } from 'ant-design-vue'. Vue 3 has no Vue.prototype.
  • RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, which would create a circular import. Replaced with the literal string default '2022-blake3-aes-256-gcm'.
  • MediaQueryMixin removed from utils. Replaced by frontend/src/composables/useMediaQuery.js, a Vue 3 composable returning a reactive isMobile.
  • web/assets/js/axios-init.js was an imperative side-effect script. Wrapped as setupAxios() which the app calls once at startup. Qs global → import qs from 'qs'.
  • web/assets/js/websocket.js exported as WebSocketClient. The bottom window.wsClient = ... line was removed; pages instantiate the client themselves with the basePath they need.
  • Models (inbound, outbound, dbinbound, setting, reality_targets) copied verbatim with export added and the imports they need from utils/legacy.js wired up.
  • subscription.js deferred to Phase 5 — it's a Vue 2 mount, not a util.
  • Smoke test added to frontend/src/App.vue exercising SizeFormatter, RandomUtil, Wireguard, and useMediaQuery. If npm run dev renders it correctly, Phase 3 is verified.