diff options
37 files changed, 1342 insertions, 1202 deletions
diff --git a/web/assets/ant-design-vue@1.7.2/antd.min.css b/web/assets/ant-design-vue@1.7.2/antd.min.css index 5f731c06..4de7fad0 100644 --- a/web/assets/ant-design-vue@1.7.2/antd.min.css +++ b/web/assets/ant-design-vue@1.7.2/antd.min.css @@ -992,7 +992,7 @@ to{transform:scale(0) translate(50%,-50%);opacity:0} .ant-menu-item>.ant-badge>a{color:rgba(0,0,0,.65)} .ant-menu-item>.ant-badge>a:hover{color:#1890ff} .ant-menu-item-divider{height:1px;overflow:hidden;line-height:0;background-color:#e8e8e8} -.ant-menu-item-active,.ant-menu-item:hover,.ant-menu-submenu-active,.ant-menu-submenu-title:hover,.ant-menu:not(.ant-menu-inline) .ant-menu-submenu-open{color:#fff;background-image: linear-gradient(90deg,#99999980 0,#8888889e 100%);border-radius: 0.5rem} +.ant-menu-item-active,.ant-menu-item:hover,.ant-menu-submenu-active,.ant-menu-submenu-title:hover,.ant-menu:not(.ant-menu-inline) .ant-menu-submenu-open{color:#2d2d2d;background-image: linear-gradient(90deg,#99999980 0,#8888889e 100%);border-radius: 0.5rem} .ant-menu-horizontal .ant-menu-item,.ant-menu-horizontal .ant-menu-submenu{margin-top:-1px} .ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu .ant-menu-submenu-title:hover{background-color:transparent} .ant-menu-item-selected,.ant-menu-item-selected>a,.ant-menu-item-selected>a:hover{color:#1890ff} diff --git a/web/assets/css/custom.css b/web/assets/css/custom.css index 718e87f8..7cdfe711 100644 --- a/web/assets/css/custom.css +++ b/web/assets/css/custom.css @@ -1,5 +1,23 @@ -#app { +html, +body { height: 100vh; + width: 100vw; + margin: 0; + padding: 0; + overflow: hidden; +} + +#app { + height: 100%; + min-height: 100vh; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: 0; + padding: 0; + overflow: auto; } .ant-space { @@ -180,12 +198,12 @@ .ant-card-dark:hover { border-color: #e8e8e8; - box-shadow: 0 1px 10px -1px rgb(154 175 238); + box-shadow: 0 1px 10px -1px rgb(76, 88, 126); } -.ant-card-bordered:hover { - /*box-shadow: 0 3px 12px -0.8px #0000005c;*/ -} +/* .ant-card-bordered:hover { + box-shadow: 0 3px 12px -0.8px #0000005c; +} */ .ant-card-dark .ant-table-thead th { color: hsla(0,0%,100%,.65); @@ -203,6 +221,7 @@ .ant-card-dark .ant-input-group-addon { color: hsla(0,0%,100%,.65); background-color: #262f3d; + border: 1px solid rgb(0 150 112 / 0%); } .ant-card-dark .ant-list-item-meta-title, diff --git a/web/assets/js/axios-init.js b/web/assets/js/axios-init.js index bd55c3cf..b864b714 100644 --- a/web/assets/js/axios-init.js +++ b/web/assets/js/axios-init.js @@ -2,7 +2,7 @@ axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; axios.interceptors.request.use( - config => { + (config) => { if (config.data instanceof FormData) { config.headers['Content-Type'] = 'multipart/form-data'; } else { @@ -12,5 +12,5 @@ axios.interceptors.request.use( } return config; }, - error => Promise.reject(error) + (error) => Promise.reject(error), ); diff --git a/web/assets/js/langs.js b/web/assets/js/langs.js index e83b05f3..9e1f0911 100644 --- a/web/assets/js/langs.js +++ b/web/assets/js/langs.js @@ -1,41 +1,41 @@ const supportLangs = [ { - name : "English", - value : "en-US", - icon : "🇺🇸" + name: 'English', + value: 'en-US', + icon: '🇺🇸', }, { - name : "Farsi", - value : "fa_IR", - icon : "🇮🇷" + name: 'Farsi', + value: 'fa_IR', + icon: '🇮🇷', }, { - name : "汉è¯", - value : "zh-Hans", - icon : "🇨🇳" + name: '汉è¯', + value: 'zh-Hans', + icon: '🇨🇳', }, { - name : "Russian", - value : "ru_RU", - icon : "🇷🇺" + name: 'Russian', + value: 'ru_RU', + icon: '🇷🇺', }, -] +]; -function getLang(){ - let lang = getCookie('lang') +function getLang() { + let lang = getCookie('lang'); - if (! lang){ - if (window.navigator){ + if (!lang) { + if (window.navigator) { lang = window.navigator.language || window.navigator.userLanguage; - if (isSupportLang(lang)){ - setCookie('lang' , lang , 150) - }else{ - setCookie('lang' , 'en-US' , 150) + if (isSupportLang(lang)) { + setCookie('lang', lang, 150); + } else { + setCookie('lang', 'en-US', 150); window.location.reload(); } - }else{ - setCookie('lang' , 'en-US' , 150) + } else { + setCookie('lang', 'en-US', 150); window.location.reload(); } } @@ -43,47 +43,21 @@ function getLang(){ return lang; } -function setLang(lang){ - - if (!isSupportLang(lang)){ +function setLang(lang) { + if (!isSupportLang(lang)) { lang = 'en-US'; } - setCookie('lang' , lang , 150) + setCookie('lang', lang, 150); window.location.reload(); } -function isSupportLang(lang){ - for (l of supportLangs){ - if (l.value === lang){ +function isSupportLang(lang) { + for (l of supportLangs) { + if (l.value === lang) { return true; } } return false; } - - - -function getCookie(cname) { - let name = cname + "="; - let decodedCookie = decodeURIComponent(document.cookie); - let ca = decodedCookie.split(';'); - for(let i = 0; i <ca.length; i++) { - let c = ca[i]; - while (c.charAt(0) == ' ') { - c = c.substring(1); - } - if (c.indexOf(name) == 0) { - return c.substring(name.length, c.length); - } - } - return ""; -} - -function setCookie(cname, cvalue, exdays) { - const d = new Date(); - d.setTime(d.getTime() + (exdays*24*60*60*1000)); - let expires = "expires="+ d.toUTCString(); - document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/"; -}
\ No newline at end of file diff --git a/web/assets/js/util/common.js b/web/assets/js/util/common.js index b3ebc0bd..808b1ba9 100644 --- a/web/assets/js/util/common.js +++ b/web/assets/js/util/common.js @@ -56,14 +56,37 @@ function toFixed(num, n) { return Math.round(num * n) / n; } -function debounce (fn, delay) { - var timeoutID = null +function debounce(fn, delay) { + var timeoutID = null; return function () { - clearTimeout(timeoutID) - var args = arguments - var that = this - timeoutID = setTimeout(function () { - fn.apply(that, args) - }, delay) + clearTimeout(timeoutID); + var args = arguments; + var that = this; + timeoutID = setTimeout(function () { + fn.apply(that, args); + }, delay); + }; +} + +function getCookie(cname) { + let name = cname + '='; + let decodedCookie = decodeURIComponent(document.cookie); + let ca = decodedCookie.split(';'); + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) == ' ') { + c = c.substring(1); + } + if (c.indexOf(name) == 0) { + return c.substring(name.length, c.length); + } } - }
\ No newline at end of file + return ''; +} + +function setCookie(cname, cvalue, exdays) { + const d = new Date(); + d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000); + let expires = 'expires=' + d.toUTCString(); + document.cookie = cname + '=' + cvalue + ';' + expires + ';path=/'; +} diff --git a/web/assets/js/util/date-util.js b/web/assets/js/util/date-util.js index 24e08879..651101d8 100644 --- a/web/assets/js/util/date-util.js +++ b/web/assets/js/util/date-util.js @@ -128,14 +128,13 @@ Date.prototype.formatDateTime = function (split = ' ') { }; class DateUtil { - // å—符串转 Date 对象 static parseDate(str) { return new Date(str.replace(/-/g, '/')); } static formatMillis(millis) { - return moment(millis).format('YYYY-M-D H:m:s') + return moment(millis).format('YYYY-M-D H:m:s'); } static firstDayOfMonth() { @@ -144,4 +143,4 @@ class DateUtil { date.setMinTime(); return date; } -}
\ No newline at end of file +} diff --git a/web/assets/js/util/utils.js b/web/assets/js/util/utils.js index 9c441537..28891cd4 100644 --- a/web/assets/js/util/utils.js +++ b/web/assets/js/util/utils.js @@ -68,13 +68,11 @@ class HttpUtil { } class PromiseUtil { - static async sleep(timeout) { await new Promise(resolve => { setTimeout(resolve, timeout) }); } - } const seq = [ @@ -95,7 +93,6 @@ const shortIdSeq = [ ]; class RandomUtil { - static randomIntRange(min, max) { return parseInt(Math.random() * (max - min) + min, 10); } @@ -153,8 +150,8 @@ class RandomUtil { static randomText() { var chars = 'abcdefghijklmnopqrstuvwxyz1234567890'; var string = ''; - var len = 6 + Math.floor(Math.random() * 5) - for(var ii=0; ii<len; ii++){ + var len = 6 + Math.floor(Math.random() * 5); + for (var ii = 0; ii < len; ii++) { string += chars[Math.floor(Math.random() * chars.length)]; } return string; @@ -162,11 +159,11 @@ class RandomUtil { static randowShortId() { let str = ''; - str += this.randomShortIdSeq(8) + str += this.randomShortIdSeq(8); return str; } - - static randomShadowsocksPassword(){ + + static randomShadowsocksPassword() { let array = new Uint8Array(32); window.crypto.getRandomValues(array); return btoa(String.fromCharCode.apply(null, array)); @@ -174,7 +171,6 @@ class RandomUtil { } class ObjectUtil { - static getPropIgnoreCase(obj, prop) { for (const name in obj) { if (!obj.hasOwnProperty(name)) { @@ -322,5 +318,4 @@ class ObjectUtil { } return true; } - } diff --git a/web/html/common/prompt_modal.html b/web/html/common/prompt_modal.html index 4b8a13d0..17a65ec1 100644 --- a/web/html/common/prompt_modal.html +++ b/web/html/common/prompt_modal.html @@ -1,7 +1,7 @@ {{define "promptModal"}} <a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title" :closable="true" @ok="promptModal.ok" :mask-closable="false" - :class="siderDrawer.isDarkTheme ? darkClass : ''" + :class="themeSwitcher.darkCardClass" :ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}'> <a-input id="prompt-modal-input" :type="promptModal.type" v-model="promptModal.value" @@ -36,12 +36,12 @@ }, confirm() {}, open({ - title='', - type='text', - value='', - okText='{{ i18n "sure"}}', - confirm=() => {}, - }) { + title = '', + type = 'text', + value = '', + okText = '{{ i18n "sure"}}', + confirm = () => {}, + }) { this.title = title; this.type = type; this.value = value; diff --git a/web/html/common/qrcode_modal.html b/web/html/common/qrcode_modal.html index f492dabb..2bd2f00f 100644 --- a/web/html/common/qrcode_modal.html +++ b/web/html/common/qrcode_modal.html @@ -1,10 +1,12 @@ {{define "qrcodeModal"}} <a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title" :closable="true" - :class="siderDrawer.isDarkTheme ? darkClass : ''" + :class="themeSwitcher.darkCardClass" :footer="null" width="300px"> - <a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;" >{{ i18n "pages.inbounds.clickOnQRcode" }}</a-tag> + <a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;"> + {{ i18n "pages.inbounds.clickOnQRcode" }} + </a-tag> <canvas @click="copyToClipboard()" id="qrCode" style="width: 100%; height: 100%;"></canvas> </a-modal> @@ -19,7 +21,7 @@ qrcode: null, clipboard: null, visible: false, - show: function (title='', content='', dbInbound=new DBInbound(), copyText='') { + show: function (title = '', content = '', dbInbound = new DBInbound(), copyText = '') { this.title = title; this.content = content; this.dbInbound = dbInbound; diff --git a/web/html/common/text_modal.html b/web/html/common/text_modal.html index ce77d0ca..1514051b 100644 --- a/web/html/common/text_modal.html +++ b/web/html/common/text_modal.html @@ -1,7 +1,7 @@ {{define "textModal"}} <a-modal id="text-modal" v-model="txtModal.visible" :title="txtModal.title" :closable="true" ok-text='{{ i18n "copy" }}' cancel-text='{{ i18n "close" }}' - :class="siderDrawer.isDarkTheme ? darkClass : ''" + :class="themeSwitcher.darkCardClass" :ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}"> <a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;" :href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)" @@ -21,7 +21,7 @@ qrcode: null, clipboard: null, visible: false, - show: function (title='', content='', fileName='') { + show: function (title = '', content = '', fileName = '') { this.title = title; this.content = content; this.fileName = fileName; diff --git a/web/html/login.html b/web/html/login.html index 2f4cb3e6..1b6478de 100644 --- a/web/html/login.html +++ b/web/html/login.html @@ -18,6 +18,12 @@ border-radius: 30px; } + .ant-input-group-addon { + border-radius: 0 30px 30px 0; + width: 50px; + font-size: 18px; + } + .ant-input-affix-wrapper .ant-input-prefix { left: 23px; } @@ -26,20 +32,26 @@ padding-left: 50px; } - .selectLang{ + .centered { display: flex; text-align: center; + align-items: center; justify-content: center; } + .title { + font-size: 32px; + font-weight: bold; + } + </style> <body> -<a-layout id="app" v-cloak> +<a-layout id="app" v-cloak :class="themeSwitcher.darkClass"> <transition name="list" appear> <a-layout-content> <a-row type="flex" justify="center"> <a-col :xs="22" :sm="20" :md="16" :lg="12" :xl="8"> - <h1>{{ i18n "pages.login.title" }}</h1> + <h1 class="title">{{ i18n "pages.login.title" }}</h1> </a-col> </a-row> <a-row type="flex" justify="center"> @@ -48,35 +60,33 @@ <a-form-item> <a-input v-model.trim="user.username" placeholder='{{ i18n "username" }}' @keydown.enter.native="login" autofocus> - <a-icon slot="prefix" type="user" style="color: rgba(0,0,0,.25)"/> + <a-icon slot="prefix" type="user" :style="'font-size: 16px;' + themeSwitcher.textStyle" /> </a-input> </a-form-item> <a-form-item> - <a-input type="password" v-model.trim="user.password" - placeholder='{{ i18n "password" }}' @keydown.enter.native="login"> - <a-icon slot="prefix" type="lock" style="color: rgba(0,0,0,.25)"/> - </a-input> + <password-input icon="lock" v-model.trim="user.password" + placeholder='{{ i18n "password" }}' @keydown.enter.native="login"> + </password-input> </a-form-item> <a-form-item v-if="secretEnable"> - <a-input type="text" placeholder='{{ i18n "secretToken" }}' v-model.trim="user.loginSecret" @keydown.enter.native="login"> - <a-icon slot="prefix" type="key" style="color: rgba(0,0,0,.25)"/> + <password-input icon="key" v-model.trim="user.loginSecret" + placeholder='{{ i18n "secretToken" }}' @keydown.enter.native="login"> + </password-input> </a-input> </a-form-item> <a-form-item> - <a-button block @click="login" :loading="loading">{{ i18n "login" }}</a-button> + <a-row justify="center" class="centered"> + <a-button type="primary" :loading="loading" @click="login" :icon="loading ? 'poweroff' : undefined" + :style="loading ? { width: '50px' } : { display: 'block', width: '100%' }"> + [[ loading ? '' : '{{ i18n "login" }}' ]] + </a-button> + </a-row> </a-form-item> <a-form-item> - - <a-row justify="center" class="selectLang"> - <a-col :span="5"><span>Language :</span></a-col> - - <a-col :span="7"> - <a-select - ref="selectLang" - v-model="lang" - @change="setLang(lang)" - > - <a-select-option :value="l.value" label="English" v-for="l in supportLangs" > + <a-row justify="center" class="centered"> + <a-col :span="12"> + <a-select ref="selectLang" v-model="lang" @change="setLang(lang)" :dropdown-class-name="themeSwitcher.darkCardClass"> + <a-select-option :value="l.value" label="English" v-for="l in supportLangs"> <span role="img" aria-label="l.name" v-text="l.icon"></span> <span v-text="l.name"></span> </a-select-option> @@ -84,6 +94,11 @@ </a-col> </a-row> </a-form-item> + <a-form-item> + <a-row justify="center" class="centered"> + <theme-switch /> + </a-row> + </a-form-item> </a-form> </a-col> </a-row> @@ -91,24 +106,24 @@ </transition> </a-layout> {{template "js" .}} +{{template "component/themeSwitcher" .}} +{{template "component/password" .}} <script> - const leftColor = RandomUtil.randomIntRange(0x222222, 0xFFFFFF / 2).toString(16); - const rightColor = RandomUtil.randomIntRange(0xFFFFFF / 2, 0xDDDDDD).toString(16); - const deg = RandomUtil.randomIntRange(0, 360); - const background = `linear-gradient(${deg}deg, #${leftColor} 10%, #${rightColor} 100%)`; - document.querySelector('#app').style.background = background; + const app = new Vue({ delimiters: ['[[', ']]'], el: '#app', data: { + themeSwitcher, loading: false, user: new User(), secretEnable: false, - lang : "" + lang: "" }, - created(){ - this.lang = getLang(); - this.secretEnable = this.getSecretStatus(); + created() { + this.updateBackground(); + this.lang = getLang(); + this.secretEnable = this.getSecretStatus(); }, methods: { async login() { @@ -120,16 +135,29 @@ } }, async getSecretStatus() { - this.loading= true; + this.loading = true; const msg = await HttpUtil.post('/getSecretStatus'); this.loading = false; - if (msg.success){ + if (msg.success) { this.secretEnable = msg.obj; return msg.obj; } - } - } + }, + updateBackground() { + const leftColor = RandomUtil.randomIntRange(0x222222, 0xFFFFFF / 2).toString(16); + const rightColor = RandomUtil.randomIntRange(0xFFFFFF / 2, 0xDDDDDD).toString(16); + const deg = RandomUtil.randomIntRange(0, 360); + const background = `linear-gradient(${deg}deg, #${leftColor} 10%, #${rightColor} 100%)`; + document.querySelector('#app').style.background = this.themeSwitcher.isDarkTheme ? colors.dark.bg : background; + }, + }, + watch: { + 'themeSwitcher.isDarkTheme'(newVal, oldVal) { + this.updateBackground(); + }, + }, }); + </script> </body> </html>
\ No newline at end of file diff --git a/web/html/xui/client_bulk_modal.html b/web/html/xui/client_bulk_modal.html index d35d7278..076579a9 100644 --- a/web/html/xui/client_bulk_modal.html +++ b/web/html/xui/client_bulk_modal.html @@ -1,11 +1,11 @@ {{define "clientsBulkModal"}} <a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible" :title="clientsBulkModal.title" @ok="clientsBulkModal.ok" :confirm-loading="clientsBulkModal.confirmLoading" :closable="true" :mask-closable="false" - :class="siderDrawer.isDarkTheme ? darkClass : ''" + :class="themeSwitcher.darkCardClass" :ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}'> <a-form layout="inline"> <a-form-item label='{{ i18n "pages.client.method" }}'> - <a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 350px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> + <a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 350px" :dropdown-class-name="themeSwitcher.darkCardClass"> <a-select-option :value="0">Random</a-select-option> <a-select-option :value="1">Random+Prefix</a-select-option> <a-select-option :value="2">Random+Prefix+Num</a-select-option> @@ -71,20 +71,20 @@ </a-form-item> <br> <a-form-item v-if="clientsBulkModal.inbound.xtls" label="Flow"> - <a-select v-model="clientsBulkModal.flow" style="width: 200px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> + <a-select v-model="clientsBulkModal.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass"> <a-select-option value="">{{ i18n "none" }}</a-select-option> <a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> </a-select> </a-form-item> <a-form-item v-if="clientsBulkModal.inbound.canEnableTlsFlow()" label="Flow" layout="inline"> - <a-select v-model="clientsBulkModal.flow" style="width: 200px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> + <a-select v-model="clientsBulkModal.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass"> <a-select-option value="" selected>{{ i18n "none" }}</a-select-option> <a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> </a-select> </a-form-item> <a-form-item> <span slot="label"> - <span >{{ i18n "pages.inbounds.totalFlow" }}</span> (GB) + <span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB) <a-tooltip> <template slot="title"> 0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span> @@ -104,7 +104,7 @@ </a-form-item> <a-form-item v-else> <span slot="label"> - <span >{{ i18n "pages.inbounds.expireDate" }}</span> + <span>{{ i18n "pages.inbounds.expireDate" }}</span> <a-tooltip> <template slot="title"> <span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span> @@ -113,7 +113,7 @@ </a-tooltip> </span> <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss" - :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''" + :dropdown-class-name="themeSwitcher.darkCardClass" v-model="clientsBulkModal.expiryTime" style="width: 300px;"></a-date-picker> </a-form-item> </a-form> @@ -143,37 +143,42 @@ delayedStart: false, ok() { clients = []; - method=clientsBulkModal.emailMethod; - if(method>1){ - start
|
