mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-13 09:36:05 +00:00
feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
772e778aa0
commit
138696cf36
9 changed files with 850 additions and 747 deletions
|
|
@ -107,6 +107,22 @@ Order chosen so that breakage is contained and we always have a working panel:
|
|||
- ✅ 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 — remaining custom components
|
||||
|
||||
### 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 4 notes
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
<title>3x-ui — Sign in</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="message"></div>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/login.js"></script>
|
||||
</body>
|
||||
|
|
|
|||
1407
frontend/package-lock.json
generated
1407
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -18,7 +18,7 @@
|
|||
"qrious": "^4.0.2",
|
||||
"qs": "^6.13.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^10.0.5"
|
||||
"vue-i18n": "^11.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.6",
|
||||
|
|
|
|||
46
frontend/src/components/ThemeSwitch.vue
Normal file
46
frontend/src/components/ThemeSwitch.vue
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { BulbFilled, BulbOutlined } from '@ant-design/icons-vue';
|
||||
import { theme, currentTheme, toggleTheme, toggleUltra, pauseAnimationsUntilLeave } from '@/composables/useTheme.js';
|
||||
|
||||
const BulbIcon = computed(() => (theme.isDark ? BulbFilled : BulbOutlined));
|
||||
|
||||
function onDarkChange() {
|
||||
pauseAnimationsUntilLeave('change-theme');
|
||||
toggleTheme();
|
||||
}
|
||||
|
||||
function onUltraClick() {
|
||||
pauseAnimationsUntilLeave('change-theme-ultra');
|
||||
toggleUltra();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-menu :theme="currentTheme" mode="inline" :selected-keys="[]">
|
||||
<a-sub-menu>
|
||||
<template #title>
|
||||
<span>
|
||||
<component :is="BulbIcon" />
|
||||
<span class="theme-label">Theme</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<a-menu-item id="change-theme" class="ant-menu-theme-switch">
|
||||
<span>Dark</span>
|
||||
<a-switch :style="{ marginLeft: '2px' }" size="small" :checked="theme.isDark" @change="onDarkChange" />
|
||||
</a-menu-item>
|
||||
|
||||
<a-menu-item v-if="theme.isDark" id="change-theme-ultra" class="ant-menu-theme-switch">
|
||||
<span>Ultra dark</span>
|
||||
<a-checkbox :style="{ marginLeft: '2px' }" :checked="theme.isUltra" @click="onUltraClick" />
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
</a-menu>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.theme-label {
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
25
frontend/src/components/ThemeSwitchLogin.vue
Normal file
25
frontend/src/components/ThemeSwitchLogin.vue
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<script setup>
|
||||
import { theme, toggleTheme, toggleUltra, pauseAnimationsUntilLeave } from '@/composables/useTheme.js';
|
||||
|
||||
function onDarkChange() {
|
||||
pauseAnimationsUntilLeave('change-theme');
|
||||
toggleTheme();
|
||||
}
|
||||
|
||||
function onUltraClick() {
|
||||
toggleUltra();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-space id="change-theme" direction="vertical" :size="10" :style="{ width: '100%' }">
|
||||
<a-space direction="horizontal" size="small">
|
||||
<a-switch size="small" :checked="theme.isDark" @change="onDarkChange" />
|
||||
<span>Dark</span>
|
||||
</a-space>
|
||||
<a-space v-if="theme.isDark" direction="horizontal" size="small">
|
||||
<a-checkbox :checked="theme.isUltra" @click="onUltraClick" />
|
||||
<span>Ultra dark</span>
|
||||
</a-space>
|
||||
</a-space>
|
||||
</template>
|
||||
67
frontend/src/composables/useTheme.js
Normal file
67
frontend/src/composables/useTheme.js
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { reactive, computed, watchEffect } from 'vue';
|
||||
|
||||
// Single shared theme state. `import { theme } from '@/composables/useTheme.js'`
|
||||
// from any component to read/toggle. Boot side-effects (apply current
|
||||
// theme to <body>/<html>) run once at module load so the page is in the
|
||||
// right theme before Vue mounts.
|
||||
|
||||
const STORAGE_DARK = 'dark-mode';
|
||||
const STORAGE_ULTRA = 'isUltraDarkThemeEnabled';
|
||||
|
||||
function readBool(key, fallback) {
|
||||
const raw = localStorage.getItem(key);
|
||||
if (raw === null) return fallback;
|
||||
return raw === 'true';
|
||||
}
|
||||
|
||||
const isDark = readBool(STORAGE_DARK, true);
|
||||
const isUltra = readBool(STORAGE_ULTRA, false);
|
||||
|
||||
export const theme = reactive({
|
||||
isDark,
|
||||
isUltra,
|
||||
});
|
||||
|
||||
export const currentTheme = computed(() => (theme.isDark ? 'dark' : 'light'));
|
||||
|
||||
export function toggleTheme() {
|
||||
theme.isDark = !theme.isDark;
|
||||
}
|
||||
|
||||
export function toggleUltra() {
|
||||
theme.isUltra = !theme.isUltra;
|
||||
}
|
||||
|
||||
// Briefly disable theme transition animations while a toggle is in
|
||||
// flight, then re-enable on mouseleave. Mirrors the legacy panel's
|
||||
// behavior of preventing flicker when hovering the theme menu.
|
||||
export function pauseAnimationsUntilLeave(elementId) {
|
||||
document.documentElement.setAttribute('data-theme-animations', 'off');
|
||||
const el = document.getElementById(elementId);
|
||||
if (!el) return;
|
||||
const restore = () => {
|
||||
document.documentElement.removeAttribute('data-theme-animations');
|
||||
el.removeEventListener('mouseleave', restore);
|
||||
el.removeEventListener('touchend', restore);
|
||||
};
|
||||
el.addEventListener('mouseleave', restore);
|
||||
el.addEventListener('touchend', restore);
|
||||
}
|
||||
|
||||
// Apply theme to DOM and persist whenever it changes.
|
||||
watchEffect(() => {
|
||||
document.body.setAttribute('class', theme.isDark ? 'dark' : 'light');
|
||||
localStorage.setItem(STORAGE_DARK, String(theme.isDark));
|
||||
|
||||
if (theme.isUltra) {
|
||||
document.documentElement.setAttribute('data-theme', 'ultra-dark');
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
}
|
||||
localStorage.setItem(STORAGE_ULTRA, String(theme.isUltra));
|
||||
|
||||
// Keep the global #message container's class in sync so AD-Vue toasts
|
||||
// pick up the right styling.
|
||||
const msg = document.getElementById('message');
|
||||
if (msg) msg.className = theme.isDark ? 'dark' : 'light';
|
||||
});
|
||||
|
|
@ -1,10 +1,20 @@
|
|||
import { createApp } from 'vue';
|
||||
import Antd from 'ant-design-vue';
|
||||
import Antd, { message } from 'ant-design-vue';
|
||||
import 'ant-design-vue/dist/reset.css';
|
||||
|
||||
import { setupAxios } from '@/api/axios-init.js';
|
||||
// Importing this module triggers the boot side-effect that applies the
|
||||
// stored theme to <body>/<html> before Vue renders anything.
|
||||
import '@/composables/useTheme.js';
|
||||
import LoginPage from '@/pages/login/LoginPage.vue';
|
||||
|
||||
setupAxios();
|
||||
|
||||
// Toasts attach to a #message div the page provides — keeps theme
|
||||
// styling in sync with the rest of the panel.
|
||||
const messageContainer = document.getElementById('message');
|
||||
if (messageContainer) {
|
||||
message.config({ getContainer: () => messageContainer });
|
||||
}
|
||||
|
||||
createApp(LoginPage).use(Antd).mount('#app');
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
<script setup>
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { UserOutlined, LockOutlined, KeyOutlined } from '@ant-design/icons-vue';
|
||||
import { UserOutlined, LockOutlined, KeyOutlined, SettingOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
import { HttpUtil } from '@/utils';
|
||||
import { currentTheme } from '@/composables/useTheme.js';
|
||||
import ThemeSwitchLogin from '@/components/ThemeSwitchLogin.vue';
|
||||
|
||||
// Phase 4 ships this page in English only. Translations come back in
|
||||
// Phase 7 (vue-i18n) once we decide how the new build pipeline reads
|
||||
|
|
@ -69,6 +71,17 @@ async function login() {
|
|||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="login-settings">
|
||||
<a-popover :overlay-class-name="currentTheme" title="Settings" placement="bottomRight" trigger="click">
|
||||
<template #content>
|
||||
<ThemeSwitchLogin />
|
||||
</template>
|
||||
<a-button shape="circle">
|
||||
<template #icon><SettingOutlined /></template>
|
||||
</a-button>
|
||||
</a-popover>
|
||||
</div>
|
||||
|
||||
<a-row justify="center">
|
||||
<a-col :span="24">
|
||||
<h2 class="login-title">Welcome to 3x-ui</h2>
|
||||
|
|
@ -134,6 +147,12 @@ async function login() {
|
|||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
.login-settings {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.login-content {
|
||||
position: relative;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue