diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 00000000..4e97a76e
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,3 @@
+node_modules/
+.vite/
+*.log
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 00000000..8317472f
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,43 @@
+# 3x-ui frontend
+
+Vue 3 + Ant Design Vue 4 + Vite. Builds into `../web/dist/`, which the
+Go binary will embed via `embed.FS` once the migration reaches the page
+handlers (Phase 4+).
+
+This directory exists alongside the legacy `web/html/` Vue 2 templates
+during the migration. Pages will move over one at a time on the
+`vue3-migration` branch.
+
+## Dev
+
+```sh
+cd frontend
+npm install
+npm run dev
+```
+
+The dev server runs on `http://localhost:5173/` and proxies API calls to
+the Go panel at `http://localhost:2053/` — start the Go panel first
+(`go run main.go`), then start Vite.
+
+## Production build
+
+```sh
+npm run build
+```
+
+Outputs to `../web/dist/`. The Go binary picks it up at compile time via
+`embed.FS`.
+
+## Where things live
+
+- `src/main.js` — app entrypoint (createApp, install Antd, mount)
+- `src/App.vue` — root component (currently a smoke-test placeholder)
+- `vite.config.js` — build + dev-server config
+- `index.html` — Vite HTML template
+
+## Adding new pages
+
+For each legacy page being migrated, add an entry to
+`vite.config.js` `rollupOptions.input`. Each entry produces its own
+HTML file in `web/dist/`, which the Go panel route handler will serve.
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 00000000..68effba1
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ 3x-ui
+
+
+
+
+
+
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 00000000..063b7f1c
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "x-ui-frontend",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "description": "3x-ui panel frontend (Vue 3 + Ant Design Vue 4). Built with Vite into ../web/dist/ and embedded by the Go binary.",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview",
+ "lint": "eslint src --ext .js,.vue"
+ },
+ "dependencies": {
+ "ant-design-vue": "^4.2.6",
+ "@ant-design/icons-vue": "^7.0.1",
+ "axios": "^1.7.9",
+ "moment": "^2.30.1",
+ "qrious": "^4.0.2",
+ "vue": "^3.5.13",
+ "vue-i18n": "^10.0.5"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^5.2.1",
+ "eslint": "^9.17.0",
+ "eslint-plugin-vue": "^9.32.0",
+ "vite": "^6.0.7"
+ }
+}
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
new file mode 100644
index 00000000..9b83fa2a
--- /dev/null
+++ b/frontend/src/App.vue
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+ If you see this card with a styled button below, the toolchain works.
+
+ Clicked {{ count }} times
+ Reset
+
+
+
+
+
+
+
+
diff --git a/frontend/src/main.js b/frontend/src/main.js
new file mode 100644
index 00000000..f4920af9
--- /dev/null
+++ b/frontend/src/main.js
@@ -0,0 +1,9 @@
+import { createApp } from 'vue';
+import Antd from 'ant-design-vue';
+import 'ant-design-vue/dist/reset.css';
+
+import App from './App.vue';
+
+const app = createApp(App);
+app.use(Antd);
+app.mount('#app');
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
new file mode 100644
index 00000000..1fb94cde
--- /dev/null
+++ b/frontend/vite.config.js
@@ -0,0 +1,40 @@
+import { defineConfig } from 'vite';
+import vue from '@vitejs/plugin-vue';
+import path from 'node:path';
+
+// Output goes to web/dist/ at the repo root so the Go binary can embed it
+// via embed.FS without reaching outside the web/ tree.
+const outDir = path.resolve(__dirname, '../web/dist');
+
+export default defineConfig({
+ plugins: [vue()],
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, 'src'),
+ },
+ },
+ build: {
+ outDir,
+ emptyOutDir: true,
+ sourcemap: true,
+ target: 'es2020',
+ // Multiple HTML entries — one per legacy page we migrate.
+ // As pages get ported in later phases, add their entrypoints here.
+ rollupOptions: {
+ input: {
+ index: path.resolve(__dirname, 'index.html'),
+ },
+ },
+ },
+ server: {
+ port: 5173,
+ strictPort: true,
+ proxy: {
+ // Proxy API calls during `npm run dev` to the local Go panel.
+ '/panel': 'http://localhost:2053',
+ '/server': 'http://localhost:2053',
+ '/login': 'http://localhost:2053',
+ '/logout': 'http://localhost:2053',
+ },
+ },
+});