diff options
| author | Shishkevich D. <135337715+shishkevichd@users.noreply.github.com> | 2025-04-06 12:40:33 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-04-06 12:40:33 +0300 |
| commit | bea19a263db88fef44b4356082b199fbfcc39a25 (patch) | |
| tree | a111e9328c6273ad9721118238c40cf3004f72a9 /web/html/settings.html | |
| parent | 878e0d02cd01a045f4f32464124c59e24f98aedd (diff) | |
Code refactoring (#2865)
* refactor: use vue inline styles in entire application
* refactor: setting row in dashboard page
* refactor: use blob for download file in text modal
* refactor: move all html templates in `web/html` folder
* refactor: `DeviceUtils` -> `MediaQueryMixin`
The transition to mixins has been made, as they can update themselves.
* chore: pretty right buttons in `outbounds` tab in xray settings
* refactor: add translations for system status
* refactor: adjust gutter spacing in setting list item
* refactor: use native `a-input-password` for password field
* chore: return old system status
with new translations
* chore: add missing translation
Diffstat (limited to 'web/html/settings.html')
| -rw-r--r-- | web/html/settings.html | 542 |
1 files changed, 542 insertions, 0 deletions
diff --git a/web/html/settings.html b/web/html/settings.html new file mode 100644 index 00000000..c8bfabad --- /dev/null +++ b/web/html/settings.html @@ -0,0 +1,542 @@ +<!DOCTYPE html> +<html lang="en"> +{{template "head" .}} +<style> + @media (min-width: 769px) { + .ant-layout-content { + margin: 24px 16px; + } + } + @media (max-width: 768px) { + .ant-tabs-nav .ant-tabs-tab { + margin: 0; + padding: 12px .5rem; + } + } + .ant-tabs-bar { + margin: 0; + } + .ant-list-item { + display: block; + } + .alert-msg { + color: rgb(194, 117, 18); + font-weight: normal; + font-size: 16px; + padding: .5rem 1rem; + text-align: center; + background: rgb(255 145 0 / 15%); + margin: 1.5rem 2.5rem 0rem; + border-radius: .5rem; + transition: all 0.5s; + animation: signal 3s cubic-bezier(0.18, 0.89, 0.32, 1.28) infinite; + } + .alert-msg:hover { + cursor: default; + transition-duration: .3s; + animation: signal 0.9s ease infinite; + } + @keyframes signal { + 0% { + box-shadow: 0 0 0 0 rgba(194, 118, 18, 0.5); + } + + 50% { + box-shadow: 0 0 0 6px rgba(0, 0, 0, 0); + } + + 100% { + box-shadow: 0 0 0 6px rgba(0, 0, 0, 0); + } + } + .alert-msg>i { + color: inherit; + font-size: 24px; + } + .dark .ant-input-password-icon { + color: var(--dark-color-text-primary); + } +</style> +<body> + <a-layout id="app" v-cloak :class="themeSwitcher.currentTheme"> + <a-sidebar></a-sidebar> + <a-layout id="content-layout"> + <a-layout-content> + <a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'> + <transition name="list" appear> + <a-alert type="error" v-if="confAlerts.length>0" :style="{ marginBottom: '10px' }" + message='{{ i18n "secAlertTitle" }}' + color="red" + show-icon closable> + <template slot="description"> + <b>{{ i18n "secAlertConf" }}</b> + <ul><li v-for="a in confAlerts">[[ a ]]</li></ul> + </template> + </a-alert> + </transition> + <a-space direction="vertical"> + <a-card hoverable :style="{ marginBottom: '.5rem', overflowX: 'hidden' }"> + <a-row :style="{ display: 'flex', flexWrap: 'wrap', alignItems: 'center' }"> + <a-col :xs="24" :sm="10" :style="{ padding: '4px' }"> + <a-space direction="horizontal"> + <a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.settings.save" }}</a-button> + <a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button> + </a-space> + </a-col> + <a-col :xs="24" :sm="14"> + <template> + <div> + <a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200"></a-back-top> + <a-alert type="warning" :style="{ float: 'right', width: 'fit-content' }" + message='{{ i18n "pages.settings.infoDesc" }}' + show-icon> + </a-alert> + </div> + </template> + </a-col> + </a-row> + </a-card> + <a-tabs default-active-key="1"> + <a-tab-pane key="1" tab='{{ i18n "pages.settings.panelSettings" }}' :style="{ paddingTop: '20px' }"> + {{ template "settings/panel/general" . }} + </a-tab-pane> + <a-tab-pane key="2" tab='{{ i18n "pages.settings.securitySettings" }}' :style="{ paddingTop: '20px' }"> + {{ template "settings/panel/security" . }} + </a-tab-pane> + <a-tab-pane key="3" tab='{{ i18n "pages.settings.TGBotSettings" }}' :style="{ paddingTop: '20px' }"> + {{ template "settings/panel/telegram" . }} + </a-tab-pane> + <a-tab-pane key="4" tab='{{ i18n "pages.settings.subSettings" }}' :style="{ paddingTop: '20px' }"> + {{ template "settings/panel/subscription/general" . }} + </a-tab-pane> + <a-tab-pane key="5" tab='{{ i18n "pages.settings.subSettings" }} Json' v-if="allSetting.subEnable" :style="{ paddingTop: '20px' }"> + {{ template "settings/panel/subscription/json" . }} + </a-tab-pane> + </a-tabs> + </a-space> + </a-spin> + </a-layout-content> + </a-layout> + </a-layout> +{{template "js" .}} +<script src="{{ .base_path }}assets/js/model/setting.js?{{ .cur_ver }}"></script> +{{template "component/aSidebar" .}} +{{template "component/aThemeSwitch" .}} +{{template "component/aSettingListItem" .}} +<script> + const app = new Vue({ + delimiters: ['[[', ']]'], + el: '#app', + data: { + themeSwitcher, + spinning: false, + changeSecret: false, + oldAllSetting: new AllSetting(), + allSetting: new AllSetting(), + saveBtnDisable: true, + user: {}, + lang: LanguageManager.getLanguage(), + remarkModels: { i: 'Inbound', e: 'Email', o: 'Other' }, + remarkSeparators: [' ', '-', '_', '@', ':', '~', '|', ',', '.', '/'], + datepickerList: [{ name: 'Gregorian (Standard)', value: 'gregorian' }, { name: 'Jalalian (شمسی)', value: 'jalalian' }], + remarkSample: '', + defaultFragment: { + tag: "fragment", + protocol: "freedom", + settings: { + domainStrategy: "AsIs", + fragment: { + packets: "tlshello", + length: "100-200", + interval: "10-20" + } + }, + streamSettings: { + sockopt: { + tcpKeepAliveIdle: 100, + tcpMptcp: true, + penetrate: true + } + } + }, + defaultNoises: { + tag: "noises", + protocol: "freedom", + settings: { + domainStrategy: "AsIs", + noises: [ + { type: "rand", packet: "10-20", delay: "10-16" }, + ], + }, + }, + defaultMux: { + enabled: true, + concurrency: 8, + xudpConcurrency: 16, + xudpProxyUDP443: "reject" + }, + defaultRules: [ + { + type: "field", + outboundTag: "direct", + domain: [ + "geosite:category-ir" + ] + }, + { + type: "field", + outboundTag: "direct", + ip: [ + "geoip:private", + "geoip:ir" + ] + }, + ], + directIPsOptions: [ + { label: 'Private IP', value: 'geoip:private' }, + { label: '🇮🇷 Iran', value: 'geoip:ir' }, + { label: '🇨🇳 China', value: 'geoip:cn' }, + { label: '🇷🇺 Russia', value: 'geoip:ru' }, + { label: '🇻🇳 Vietnam', value: 'geoip:vn' }, + { label: '🇪🇸 Spain', value: 'geoip:es' }, + { label: '🇮🇩 Indonesia', value: 'geoip:id' }, + { label: '🇺🇦 Ukraine', value: 'geoip:ua' }, + { label: '🇹🇷 Türkiye', value: 'geoip:tr' }, + { label: '🇧🇷 Brazil', value: 'geoip:br' }, + ], + diretDomainsOptions: [ + { label: 'Private DNS', value: 'geosite:private' }, + { label: '🇮🇷 Iran', value: 'geosite:category-ir' }, + { label: '🇨🇳 China', value: 'geosite:cn' }, + { label: '🇷🇺 Russia', value: 'geosite:category-ru' }, + { label: 'Apple', value: 'geosite:apple' }, + { label: 'Meta', value: 'geosite:meta' }, + { label: 'Google', value: 'geosite:google' }, + ], + get remarkModel() { + rm = this.allSetting.remarkModel; + return rm.length > 1 ? rm.substring(1).split('') : []; + }, + set remarkModel(value) { + rs = this.allSetting.remarkModel[0]; + this.allSetting.remarkModel = rs + value.join(''); + this.changeRemarkSample(); + }, + get remarkSeparator() { + return this.allSetting.remarkModel.length > 1 ? this.allSetting.remarkModel.charAt(0) : '-'; + }, + set remarkSeparator(value) { + this.allSetting.remarkModel = value + this.allSetting.remarkModel.substring(1); + this.changeRemarkSample(); + }, + get datepicker() { + return this.allSetting.datepicker ? this.allSetting.datepicker : 'gregorian'; + }, + set datepicker(value) { + this.allSetting.datepicker = value; + }, + changeRemarkSample() { + sample = [] + this.remarkModel.forEach(r => sample.push(this.remarkModels[r])); + this.remarkSample = sample.length == 0 ? '' : sample.join(this.remarkSeparator); + } + }, + methods: { + loading(spinning = true) { + this.spinning = spinning; + }, + async getAllSetting() { + this.loading(true); + const msg = await HttpUtil.post("/panel/setting/all"); + this.loading(false); + if (msg.success) { + this.oldAllSetting = new AllSetting(msg.obj); + this.allSetting = new AllSetting(msg.obj); + app.changeRemarkSample(); + this.saveBtnDisable = true; + } + await this.fetchUserSecret(); + }, + async updateAllSetting() { + this.loading(true); + const msg = await HttpUtil.post("/panel/setting/update", this.allSetting); + this.loading(false); + if (msg.success) { + await this.getAllSetting(); + } + }, + async updateUser() { + this.loading(true); + const msg = await HttpUtil.post("/panel/setting/updateUser", this.user); + this.loading(false); + if (msg.success) { + this.user = {}; + window.location.replace(basePath + "logout"); + } + }, + async restartPanel() { + await new Promise(resolve => { + this.$confirm({ + title: '{{ i18n "pages.settings.restartPanel" }}', + content: '{{ i18n "pages.settings.restartPanelDesc" }}', + class: themeSwitcher.currentTheme, + okText: '{{ i18n "sure" }}', + cancelText: '{{ i18n "cancel" }}', + onOk: () => resolve(), + }); + }); + this.loading(true); + const msg = await HttpUtil.post("/panel/setting/restartPanel"); + this.loading(false); + if (msg.success) { + this.loading(true); + await PromiseUtil.sleep(5000); + var { webCertFile, webKeyFile, webDomain: host, webPort: port, webBasePath: base } = this.allSetting; + if (host == this.oldAllSetting.webDomain) host = null; + if (port == this.oldAllSetting.webPort) port = null; + const isTLS = webCertFile !== "" || webKeyFile !== ""; + const url = URLBuilder.buildURL({ host, port, isTLS, base, path: "panel/settings" }); + window.location.replace(url); + } + }, + async fetchUserSecret() { + this.loading(true); + const userMessage = await HttpUtil.post("/panel/setting/getUserSecret", this.user); + if (userMessage.success) { + this.user = userMessage.obj; + } + this.loading(false); + }, + async updateSecret() { + this.loading(true); + const msg = await HttpUtil.post("/panel/setting/updateUserSecret", this.user); + if (msg && msg.obj) { + this.user = msg.obj; + } + this.loading(false); + await this.updateAllSetting(); + }, + generateRandomString(length) { + var chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; + let randomString = ""; + for (let i = 0; i < length; i++) { + randomString += chars[Math.floor(Math.random() * chars.length)]; + } + return randomString; + }, + async getNewSecret() { + if (!this.changeSecret) { + this.changeSecret = true; + this.user.loginSecret = ''; + const newSecret = this.generateRandomString(64); + await PromiseUtil.sleep(1000); + this.user.loginSecret = newSecret; + this.changeSecret = false; + } + }, + async toggleToken(value) { + if (value) { + await this.getNewSecret(); + } else { + this.user.loginSecret = ""; + } + }, + addNoise() { + const newNoise = { type: "rand", packet: "10-20", delay: "10-16" }; + this.noisesArray = [...this.noisesArray, newNoise]; + }, + removeNoise(index) { + const newNoises = [...this.noisesArray]; + newNoises.splice(index, 1); + this.noisesArray = newNoises; + }, + updateNoiseType(index, value) { + const updatedNoises = [...this.noisesArray]; + updatedNoises[index] = { ...updatedNoises[index], type: value }; + this.noisesArray = updatedNoises; + }, + updateNoisePacket(index, value) { + const updatedNoises = [...this.noisesArray]; + updatedNoises[index] = { ...updatedNoises[index], packet: value }; + this.noisesArray = updatedNoises; + }, + updateNoiseDelay(index, value) { + const updatedNoises = [...this.noisesArray]; + updatedNoises[index] = { ...updatedNoises[index], delay: value }; + this.noisesArray = updatedNoises; + }, + }, + computed: { + fragment: { + get: function () { return this.allSetting?.subJsonFragment != ""; }, + set: function (v) { + this.allSetting.subJsonFragment = v ? JSON.stringify(this.defaultFragment) : ""; + } + }, + fragmentPackets: { + get: function () { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).settings.fragment.packets : ""; }, + set: function (v) { + if (v != "") { + newFragment = JSON.parse(this.allSetting.subJsonFragment); + newFragment.settings.fragment.packets = v; + this.allSetting.subJsonFragment = JSON.stringify(newFragment); + } + } + }, + fragmentLength: { + get: function () { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).settings.fragment.length : ""; }, + set: function (v) { + if (v != "") { + newFragment = JSON.parse(this.allSetting.subJsonFragment); + newFragment.settings.fragment.length = v; + this.allSetting.subJsonFragment = JSON.stringify(newFragment); + } + } + }, + fragmentInterval: { + get: function () { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).settings.fragment.interval : ""; }, + set: function (v) { + if (v != "") { + newFragment = JSON.parse(this.allSetting.subJsonFragment); + newFragment.settings.fragment.interval = v; + this.allSetting.subJsonFragment = JSON.stringify(newFragment); + } + } + }, + noises: { + get() { + return this.allSetting?.subJsonNoises != ""; + }, + set(v) { + if (v) { + this.allSetting.subJsonNoises = JSON.stringify(this.defaultNoises); + } else { + this.allSetting.subJsonNoises = ""; + } + } + }, + noisesArray: { + get() { + return this.noises ? JSON.parse(this.allSetting.subJsonNoises).settings.noises : []; + }, + set(value) { + if (this.noises) { + const newNoises = JSON.parse(this.allSetting.subJsonNoises); + newNoises.settings.noises = value; + this.allSetting.subJsonNoises = JSON.stringify(newNoises); + } + } + }, + enableMux: { + get: function () { return this.allSetting?.subJsonMux != ""; }, + set: function (v) { + this.allSetting.subJsonMux = v ? JSON.stringify(this.defaultMux) : ""; + } + }, + muxConcurrency: { + get: function () { return this.enableMux ? JSON.parse(this.allSetting.subJsonMux).concurrency : -1; }, + set: function (v) { + newMux = JSON.parse(this.allSetting.subJsonMux); + newMux.concurrency = v; + this.allSetting.subJsonMux = JSON.stringify(newMux); + } + }, + muxXudpConcurrency: { + get: function () { return this.enableMux ? JSON.parse(this.allSetting.subJsonMux).xudpConcurrency : -1; }, + set: function (v) { + newMux = JSON.parse(this.allSetting.subJsonMux); + newMux.xudpConcurrency = v; + this.allSetting.subJsonMux = JSON.stringify(newMux); + } + }, + muxXudpProxyUDP443: { + get: function () { return this.enableMux ? JSON.parse(this.allSetting.subJsonMux).xudpProxyUDP443 : "reject"; }, + set: function (v) { + newMux = JSON.parse(this.allSetting.subJsonMux); + newMux.xudpProxyUDP443 = v; + this.allSetting.subJsonMux = JSON.stringify(newMux); + } + }, + enableDirect: { + get: function () { return this.allSetting?.subJsonRules != ""; }, + set: function (v) { + this.allSetting.subJsonRules = v ? JSON.stringify(this.defaultRules) : ""; + } + }, + directIPs: { + get: function () { + if (!this.enableDirect) return []; + const rules = JSON.parse(this.allSetting.subJsonRules); + if (!Array.isArray(rules)) return []; + const ipRule = rules.find(r => r.ip); + return ipRule?.ip ?? []; + }, + set: function (v) { + let rules = JSON.parse(this.allSetting.subJsonRules); + if (!Array.isArray(rules)) return; + + if (v.length == 0) { + rules = rules.filter(r => !r.ip); + } else { + let ruleIndex = rules.findIndex(r => r.ip); + if (ruleIndex == -1) ruleIndex = rules.push(this.defaultRules[1]) - 1; + + rules[ruleIndex].ip = []; + v.forEach(d => { + rules[ruleIndex].ip.push(d); + }); + } + this.allSetting.subJsonRules = JSON.stringify(rules); + } + }, + directDomains: { + get: function () { + if (!this.enableDirect) return []; + const rules = JSON.parse(this.allSetting.subJsonRules); + if (!Array.isArray(rules)) return []; + const domainRule = rules.find(r => r.domain); + return domainRule?.domain ?? []; + }, + set: function (v) { + let rules = JSON.parse(this.allSetting.subJsonRules); + if (!Array.isArray(rules)) return; + if (v.length == 0) { + rules = rules.filter(r => !r.domain); + } else { + let ruleIndex = rules.findIndex(r => r.domain); + if (ruleIndex == -1) ruleIndex = rules.push(this.defaultRules[0]) - 1; + + rules[ruleIndex].domain = v; + } + this.allSetting.subJsonRules = JSON.stringify(rules); + } + }, + confAlerts: { + get: function () { + if (!this.allSetting) return []; + var alerts = [] + if (window.location.protocol !== "https:") alerts.push('{{ i18n "secAlertSSL" }}'); + if (this.allSetting.webPort == 54321) alerts.push('{{ i18n "secAlertPanelPort" }}'); + panelPath = window.location.pathname.split('/').length < 4 + if (panelPath && this.allSetting.webBasePath == '/') alerts.push('{{ i18n "secAlertPanelURI" }}'); + if (this.allSetting.subEnable) { + subPath = this.allSetting.subURI.length > 0 ? new URL(this.allSetting.subURI).pathname : this.allSetting.subPath; + if (subPath == '/sub/') alerts.push('{{ i18n "secAlertSubURI" }}'); + subJsonPath = this.allSetting.subJsonURI.length > 0 ? new URL(this.allSetting.subJsonURI).pathname : this.allSetting.subJsonPath; + if (subJsonPath == '/json/') alerts.push('{{ i18n "secAlertSubJsonURI" }}'); + } + return alerts + } + } + }, + async mounted() { + await this.getAllSetting(); + while (true) { + await PromiseUtil.sleep(1000); + this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting); + } + } + }); +</script> +</body> +</html>
\ No newline at end of file |
