From 7838df211b6cbf5cb4676a741cc4f26ae41cbe70 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Fri, 8 May 2026 13:04:06 +0200 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20Phase=205d-i=20=E2=80=94=20se?= =?UTF-8?q?ttings=20page=20shell=20+=20dirty=20tracking?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 --- frontend/settings.html | 13 + frontend/src/models/setting.js | 7 +- frontend/src/pages/settings/SettingsPage.vue | 297 +++++++++++++++++++ frontend/src/pages/settings/useAllSetting.js | 80 +++++ frontend/src/settings.js | 18 ++ frontend/vite.config.js | 1 + 6 files changed, 415 insertions(+), 1 deletion(-) create mode 100644 frontend/settings.html create mode 100644 frontend/src/pages/settings/SettingsPage.vue create mode 100644 frontend/src/pages/settings/useAllSetting.js create mode 100644 frontend/src/settings.js diff --git a/frontend/settings.html b/frontend/settings.html new file mode 100644 index 00000000..455deb22 --- /dev/null +++ b/frontend/settings.html @@ -0,0 +1,13 @@ + + + + + + 3x-ui · Settings + + +
+
+ + + diff --git a/frontend/src/models/setting.js b/frontend/src/models/setting.js index fdca27ec..7efc9e7d 100644 --- a/frontend/src/models/setting.js +++ b/frontend/src/models/setting.js @@ -1,4 +1,9 @@ -import { ObjectUtil } from '../utils/legacy.js'; +// Mirrors web/assets/js/model/setting.js — every field on this class is +// round-tripped through `/panel/setting/all` and `/panel/setting/update`, +// so adding a field here without a matching Go-side change will silently +// drop it on save. Defaults match the legacy panel. + +import { ObjectUtil } from '@/utils'; export class AllSetting { diff --git a/frontend/src/pages/settings/SettingsPage.vue b/frontend/src/pages/settings/SettingsPage.vue new file mode 100644 index 00000000..c376d2ff --- /dev/null +++ b/frontend/src/pages/settings/SettingsPage.vue @@ -0,0 +1,297 @@ + + + + + diff --git a/frontend/src/pages/settings/useAllSetting.js b/frontend/src/pages/settings/useAllSetting.js new file mode 100644 index 00000000..8b09d00d --- /dev/null +++ b/frontend/src/pages/settings/useAllSetting.js @@ -0,0 +1,80 @@ +// Centralizes the AllSetting fetch/save lifecycle the legacy panel +// scattered across data() + methods + a busy-loop dirty checker. +// +// The dirty flag is recomputed once per second (matching the legacy +// `while (true) sleep(1000)` poll) — we don't deep-watch because the +// settings tree has many nested fields and a poll is cheap enough. + +import { onMounted, onUnmounted, reactive, ref } from 'vue'; +import { HttpUtil } from '@/utils'; +import { AllSetting } from '@/models/setting.js'; + +const DIRTY_POLL_MS = 1000; + +export function useAllSetting() { + const fetched = ref(false); + const spinning = ref(false); + const saveDisabled = ref(true); + + // Two reactive snapshots: the last server-side state and the one the + // user is editing. `equals` compares enumerable props field-by-field. + const oldAllSetting = reactive(new AllSetting()); + const allSetting = reactive(new AllSetting()); + + function applyServerState(obj) { + const fresh = new AllSetting(obj); + Object.assign(oldAllSetting, fresh); + Object.assign(allSetting, fresh); + saveDisabled.value = true; + } + + async function fetchAll() { + const msg = await HttpUtil.post('/panel/setting/all'); + if (msg?.success) { + fetched.value = true; + applyServerState(msg.obj); + } + } + + async function saveAll() { + spinning.value = true; + try { + const msg = await HttpUtil.post('/panel/setting/update', allSetting); + if (msg?.success) await fetchAll(); + } finally { + spinning.value = false; + } + } + + let timer = null; + function startDirtyPoll() { + if (timer != null) return; + timer = setInterval(() => { + // ObjectUtil.equals walks own enumerable props; reactive proxies + // expose them transparently so this works without cloning. + saveDisabled.value = oldAllSetting.equals(allSetting); + }, DIRTY_POLL_MS); + } + function stopDirtyPoll() { + if (timer != null) { + clearInterval(timer); + timer = null; + } + } + + onMounted(() => { + fetchAll(); + startDirtyPoll(); + }); + onUnmounted(stopDirtyPoll); + + return { + fetched, + spinning, + saveDisabled, + oldAllSetting, + allSetting, + fetchAll, + saveAll, + }; +} diff --git a/frontend/src/settings.js b/frontend/src/settings.js new file mode 100644 index 00000000..83f9ec32 --- /dev/null +++ b/frontend/src/settings.js @@ -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 / before Vue mounts. +import '@/composables/useTheme.js'; +import SettingsPage from '@/pages/settings/SettingsPage.vue'; + +setupAxios(); + +const messageContainer = document.getElementById('message'); +if (messageContainer) { + message.config({ getContainer: () => messageContainer }); +} + +createApp(SettingsPage).use(Antd).mount('#app'); diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 6dd77531..87aa8416 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -45,6 +45,7 @@ export default defineConfig({ input: { index: path.resolve(__dirname, 'index.html'), login: path.resolve(__dirname, 'login.html'), + settings: path.resolve(__dirname, 'settings.html'), }, }, },