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'), }, }, },