feat(frontend): Phase 5c-i — index.html dashboard shell

Replaces the smoke-test App.vue with a real IndexPage shell so the
/index.html route now boots the actual dashboard layout in Vue 3:

- a-config-provider drives AD-Vue 4's dark algorithm from useTheme
  (same pattern as LoginPage)
- AppSidebar (Phase 5b component) is wired in with basePath +
  requestUri props
- a-spin loading state with placeholder card while we build out the
  rest of the page
- Page palette mirrors the legacy: light #f0f2f5, dark #0a1222
  (--dark-color-background), ultra-dark #21242a

The 1,805-line legacy index.html is too big for one commit. Split
into five sub-phases on the todo list: ii) status cards + /server/status
polling, iii) xray status card, iv) logs/backup/panel-update modals,
v) custom-geo section.

frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold)
are removed — both purposes now served by IndexPage and index.js.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
MHSanaei 2026-05-08 12:26:51 +02:00
parent 4a98280519
commit e24e70dde2
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
5 changed files with 115 additions and 80 deletions

View file

@ -6,7 +6,8 @@
<title>3x-ui</title> <title>3x-ui</title>
</head> </head>
<body> <body>
<div id="message"></div>
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/index.js"></script>
</body> </body>
</html> </html>

View file

@ -1,70 +0,0 @@
<script setup>
import { ref, computed } from 'vue';
import { SizeFormatter, RandomUtil, Wireguard } from '@/utils';
import { useMediaQuery } from '@/composables/useMediaQuery.js';
const message = ref('Vue 3 + Ant Design Vue 4 scaffold is alive');
const count = ref(0);
const { isMobile } = useMediaQuery();
const fakeBytes = ref(1234567890);
const formatted = computed(() => SizeFormatter.sizeFormat(fakeBytes.value));
const uuid = ref(RandomUtil.randomUUID());
const keypair = ref(Wireguard.generateKeypair());
</script>
<template>
<a-layout class="layout">
<a-layout-header class="header">
<h1>3x-ui (vue3-migration scaffold)</h1>
<a-tag color="blue">isMobile: {{ isMobile }}</a-tag>
</a-layout-header>
<a-layout-content class="content">
<a-space direction="vertical" :size="16" style="width: 100%">
<a-alert :message="message" type="success" show-icon />
<a-card title="Smoke test — toolchain">
<a-space>
<a-button type="primary" @click="count++">Clicked {{ count }} times</a-button>
<a-button @click="count = 0">Reset</a-button>
</a-space>
</a-card>
<a-card title="Smoke test — utility imports">
<p><strong>SizeFormatter:</strong> {{ formatted }}</p>
<p><strong>RandomUtil.randomUUID:</strong> <code>{{ uuid }}</code></p>
<p><strong>Wireguard public key:</strong> <code>{{ keypair.publicKey }}</code></p>
<a-button @click="uuid = RandomUtil.randomUUID()">Regenerate UUID</a-button>
</a-card>
</a-space>
</a-layout-content>
</a-layout>
</template>
<style>
.layout {
min-height: 100vh;
}
.header {
background: #001529;
color: #fff;
display: flex;
align-items: center;
gap: 16px;
padding: 0 24px;
}
.header h1 {
color: #fff;
margin: 0;
font-size: 18px;
}
.content {
padding: 24px;
background: #f0f2f5;
}
code {
background: rgba(0, 0, 0, 0.06);
padding: 1px 6px;
border-radius: 3px;
font-size: 12px;
}
</style>

18
frontend/src/index.js Normal file
View file

@ -0,0 +1,18 @@
import { createApp } from 'vue';
import Antd, { message } from 'ant-design-vue';
import 'ant-design-vue/dist/reset.css';
import { setupAxios } from '@/api/axios-init.js';
// Importing useTheme triggers the boot side-effect that applies the
// stored theme to <body>/<html> before Vue mounts.
import '@/composables/useTheme.js';
import IndexPage from '@/pages/index/IndexPage.vue';
setupAxios();
const messageContainer = document.getElementById('message');
if (messageContainer) {
message.config({ getContainer: () => messageContainer });
}
createApp(IndexPage).use(Antd).mount('#app');

View file

@ -1,9 +0,0 @@
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');

View file

@ -0,0 +1,95 @@
<script setup>
import { computed, ref } from 'vue';
import { theme as antdTheme } from 'ant-design-vue';
import { theme as themeState } from '@/composables/useTheme.js';
import AppSidebar from '@/components/AppSidebar.vue';
// Drive AD-Vue 4's built-in dark algorithm from our reactive theme.
const antdThemeConfig = computed(() => ({
algorithm: themeState.isDark ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm,
}));
// Phase 5c-i ships the page shell only sidebar, layout, theme.
// Real content (CPU/mem/swap/disk cards, Xray status card, panel
// update modal, logs, custom-geo section) follows in 5c-ii through
// 5c-iv. Loading state is currently a placeholder true so the shell
// renders; it will be wired to the real /server/status fetch later.
const fetched = ref(true);
// In production the Go panel injects basePath + requestUri into the
// served HTML; during `npm run dev` we infer them from window.location.
const basePath = window.__X_UI_BASE_PATH__ || '';
const requestUri = window.location.pathname;
</script>
<template>
<a-config-provider :theme="antdThemeConfig">
<a-layout class="index-page" :class="{ 'is-dark': themeState.isDark, 'is-ultra': themeState.isUltra }">
<AppSidebar :base-path="basePath" :request-uri="requestUri" />
<a-layout class="content-shell">
<a-layout-content class="content-area">
<a-spin :spinning="!fetched" :delay="200" size="large">
<div v-if="!fetched" class="loading-spacer" />
<div v-else class="page-body">
<a-card hoverable>
<a-space direction="vertical" :size="12" style="width: 100%">
<h2 style="margin: 0">Dashboard (vue3-migration shell)</h2>
<p style="margin: 0; opacity: 0.7">
Phase 5c-i: layout, sidebar, and theme switching wired up.
Status cards, xray controls, and custom-geo arrive in
follow-up commits.
</p>
</a-space>
</a-card>
</div>
</a-spin>
</a-layout-content>
</a-layout>
</a-layout>
</a-config-provider>
</template>
<style scoped>
.index-page {
/* Same legacy palette source as the login page. */
--bg-page: #f0f2f5;
--bg-card: #ffffff;
min-height: 100vh;
background: var(--bg-page);
}
.index-page.is-dark {
--bg-page: #0a1222; /* legacy --dark-color-background */
--bg-card: #151f31; /* legacy --dark-color-surface-100 */
}
.index-page.is-dark.is-ultra {
--bg-page: #21242a; /* legacy ultra --dark-color-background */
--bg-card: #0c0e12; /* legacy ultra surface-100 */
}
.index-page :deep(.ant-layout),
.index-page :deep(.ant-layout-content) {
background: transparent;
}
.content-shell {
background: transparent;
}
.content-area {
padding: 24px;
}
.loading-spacer {
min-height: calc(100vh - 120px);
}
.page-body :deep(.ant-card) {
background: var(--bg-card);
}
</style>