Files
x-panel/web/html/settings.html
2026-05-03 11:34:48 +08:00

613 lines
22 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{ template "page/head_start" .}}
<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);
}
.ant-collapse-content-box .ant-alert {
margin-block-end: 12px;
}
</style>
{{ template "page/head_end" .}}
{{ template "page/body_start" .}}
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
<a-sidebar></a-sidebar>
<a-layout id="content-layout">
<a-layout-content>
<a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}'>
<transition name="list" appear>
<a-alert type="error" v-if="confAlerts.length>0 && loadingStates.fetched" :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>
<transition name="list" appear>
<template>
<a-row v-if="!loadingStates.fetched">
<a-card :style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
<a-spin tip='{{ i18n "loading" }}'></a-spin>
</a-card>
</a-row>
<a-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-else>
<a-col>
<a-card hoverable>
<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-col>
<a-col>
<a-tabs v-model="activeTab">
<a-tab-pane key="1" :style="{ paddingTop: '20px' }">
<template #tab>
<a-icon type="setting"></a-icon>
<span>{{ i18n "pages.settings.panelSettings" }}</span>
</template>
{{ template "settings/panel/general" . }}
</a-tab-pane>
<a-tab-pane key="2" :style="{ paddingTop: '20px' }">
<template #tab>
<a-icon type="safety"></a-icon>
<span>{{ i18n "pages.settings.securitySettings" }}</span>
</template>
{{ template "settings/panel/security" . }}
</a-tab-pane>
<a-tab-pane key="3" :style="{ paddingTop: '20px' }">
<template #tab>
<a-icon type="message"></a-icon>
<span>{{ i18n "pages.settings.TGBotSettings" }}</span>
</template>
{{ template "settings/panel/telegram" . }}
</a-tab-pane>
<a-tab-pane key="4" :style="{ paddingTop: '20px' }">
<template #tab>
<a-icon type="cloud-server"></a-icon>
<span>{{ i18n "pages.settings.subSettings" }}</span>
</template>
{{ template "settings/panel/subscription/general" . }}
</a-tab-pane>
<a-tab-pane key="5" v-if="allSetting.subEnable" :style="{ paddingTop: '20px' }">
<template #tab>
<a-icon type="code"></a-icon>
<span>{{ i18n "pages.settings.subSettings" }} (JSON)</span>
</template>
{{ template "settings/panel/subscription/json" . }}
</a-tab-pane>
</a-tabs>
</a-col>
</a-row>
</template>
</transition>
</a-spin>
</a-layout-content>
</a-layout>
</a-layout>
{{template "page/body_scripts" .}}
<script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/otpauth/otpauth.umd.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/model/setting.js?{{ .cur_ver }}"></script>
{{template "component/aSidebar" .}}
{{template "component/aThemeSwitch" .}}
{{template "component/aSettingListItem" .}}
{{template "modals/twoFactorModal"}}
<script>
const app = new Vue({
delimiters: ['[[', ']]'],
mixins: [MediaQueryMixin],
el: '#app',
data: {
themeSwitcher,
// 【修改点 2新增 activeTab 变量】
// 默认为 '1',即面板设置
activeTab: '1',
loadingStates: {
fetched: false,
spinning: 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", applyTo: "ip" },
],
},
},
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.loadingStates.spinning = spinning;
},
async getAllSetting() {
const msg = await HttpUtil.post("/panel/setting/all");
if (msg.success) {
if (!this.loadingStates.fetched) {
this.loadingStates.fetched = true
}
this.oldAllSetting = new AllSetting(msg.obj);
this.allSetting = new AllSetting(msg.obj);
app.changeRemarkSample();
this.saveBtnDisable = true;
}
},
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() {
const sendUpdateUserRequest = async () => {
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");
}
}
if (this.allSetting.twoFactorEnable) {
twoFactorModal.show({
title: '{{ i18n "pages.settings.security.twoFactorModalChangeCredentialsTitle" }}',
description: '{{ i18n "pages.settings.security.twoFactorModalChangeCredentialsStep" }}',
token: this.allSetting.twoFactorToken,
type: 'confirm',
confirm: (success) => {
if (success) {
sendUpdateUserRequest();
}
}
})
} else {
sendUpdateUserRequest();
}
},
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);
}
},
toggleTwoFactor(newValue) {
if (newValue) {
const newTwoFactorToken = RandomUtil.randomBase32String()
twoFactorModal.show({
title: '{{ i18n "pages.settings.security.twoFactorModalSetTitle" }}',
token: newTwoFactorToken,
type: 'set',
confirm: (success) => {
if (success) {
Vue.prototype.$message['success']('{{ i18n "pages.settings.security.twoFactorModalSetSuccess" }}')
this.allSetting.twoFactorToken = newTwoFactorToken
}
this.allSetting.twoFactorEnable = success
}
})
} else {
twoFactorModal.show({
title: '{{ i18n "pages.settings.security.twoFactorModalDeleteTitle" }}',
description: '{{ i18n "pages.settings.security.twoFactorModalRemoveStep" }}',
token: this.allSetting.twoFactorToken,
type: 'confirm',
confirm: (success) => {
if (success) {
Vue.prototype.$message['success']('{{ i18n "pages.settings.security.twoFactorModalDeleteSuccess" }}')
this.allSetting.twoFactorEnable = false
this.allSetting.twoFactorToken = ""
}
}
})
}
},
addNoise() {
const newNoise = { type: "rand", packet: "10-20", delay: "10-16", applyTo: "ip" };
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;
},
updateNoiseApplyTo(index, value) {
const updatedNoises = [...this.noisesArray];
updatedNoises[index] = { ...updatedNoises[index], applyTo: 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 === 2053) 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() {
// 【修改点 3页面加载时检查 URL 参数】
// 如果 URL 包含 ?tab=telegram则自动跳转到 Telegram 配置 (Key='3')
const params = new URLSearchParams(window.location.search);
if (params.get('tab') === 'telegram') {
this.activeTab = '3';
}
await this.getAllSetting();
while (true) {
await PromiseUtil.sleep(1000);
this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);
}
}
});
</script>
{{ template "page/body_end" .}}