Y-Panel/web/html/index.html
2025-08-08 14:11:04 +08:00

917 lines
38 KiB
HTML
Raw 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;
}
}
.ant-card-dark h2 {
color: var(--dark-color-text-primary);
}
.ant-backup-list-item {
gap: 10px;
}
.ant-version-list-item {
--padding: 12px;
padding: var(--padding) !important;
gap: var(--padding);
}
.dark .ant-version-list-item svg{
color: var(--dark-color-text-primary);
}
.dark .ant-backup-list-item svg,
.dark .ant-badge-status-text,
.dark .ant-card-extra {
color: var(--dark-color-text-primary);
}
.dark .ant-card-actions>li {
color: rgba(255, 255, 255, 0.55);
}
.dark .ant-radio-inner {
background-color: var(--dark-color-surface-100);
border-color: var(--dark-color-surface-600);
}
.dark .ant-radio-checked .ant-radio-inner {
border-color: var(--color-primary-100);
}
.dark .ant-backup-list,
.dark .ant-version-list,
.dark .ant-card-actions,
.dark .ant-card-actions>li:not(:last-child) {
border-color: var(--dark-color-stroke);
}
.ant-card-actions {
background: transparent;
}
.ip-hidden {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
filter: blur(10px);
}
.running-animation .ant-badge-status-dot {
animation: runningAnimation 1.2s linear infinite;
}
.running-animation .ant-badge-status-processing:after {
border-color: var(--color-primary-100);
}
@keyframes runningAnimation {
0%,
50%,
100% {
transform: scale(1);
opacity: 1;
}
10% {
transform: scale(1.5);
opacity: .2;
}
}
</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="200" :tip="loadingTip">
<transition name="list" appear>
<a-alert type="error" v-if="showAlert && loadingStates.fetched" :style="{ marginBottom: '10px' }"
message='{{ i18n "secAlertTitle" }}'
color="red"
description='{{ i18n "secAlertSsl" }}'
show-icon closable>
</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 :gutter="[0, isMobile ? 16 : 0]">
<a-col :sm="24" :md="12">
<a-row>
<a-col :span="12" :style="{ textAlign: 'center' }">
<a-progress type="dashboard" status="normal"
:stroke-color="status.cpu.color"
:percent="status.cpu.percent"></a-progress>
<div>
<b>{{ i18n "pages.index.cpu" }}:</b> [[ CPUFormatter.cpuCoreFormat(status.cpuCores) ]]
<a-tooltip>
<a-icon type="area-chart"></a-icon>
<template slot="title">
<div><b>{{ i18n "pages.index.logicalProcessors" }}:</b> [[ (status.logicalPro) ]]</div>
<div><b>{{ i18n "pages.index.frequency" }}:</b> [[ CPUFormatter.cpuSpeedFormat(status.cpuSpeedMhz) ]]</div>
</template>
</a-tooltip>
</div>
</a-col>
<a-col :span="12" :style="{ textAlign: 'center' }">
<a-progress type="dashboard" status="normal"
:stroke-color="status.mem.color"
:percent="status.mem.percent"></a-progress>
<div>
<b>{{ i18n "pages.index.memory"}}:</b> [[ SizeFormatter.sizeFormat(status.mem.current) ]] / [[ SizeFormatter.sizeFormat(status.mem.total) ]]
</div>
</a-col>
</a-row>
</a-col>
<a-col :sm="24" :md="12">
<a-row>
<a-col :span="12" :style="{ textAlign: 'center' }">
<a-progress type="dashboard" status="normal"
:stroke-color="status.swap.color"
:percent="status.swap.percent"></a-progress>
<div>
<b>{{ i18n "pages.index.swap" }}:</b> [[ SizeFormatter.sizeFormat(status.swap.current) ]] / [[ SizeFormatter.sizeFormat(status.swap.total) ]]
</div>
</a-col>
<a-col :span="12" :style="{ textAlign: 'center' }">
<a-progress type="dashboard" status="normal"
:stroke-color="status.disk.color"
:percent="status.disk.percent"></a-progress>
<div>
<b>{{ i18n "pages.index.storage"}}:</b> [[ SizeFormatter.sizeFormat(status.disk.current) ]] / [[ SizeFormatter.sizeFormat(status.disk.total) ]]
</div>
</a-col>
</a-row>
</a-col>
</a-row>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card hoverable>
<template #title>
<a-space direction="horizontal">
<span>{{ i18n "pages.index.xrayStatus" }}</span>
<a-tag v-if="isMobile && status.xray.version != 'Unknown'" color="green">
v[[ status.xray.version ]]
</a-tag>
</a-space>
</template>
<template #extra>
<template v-if="status.xray.state != 'error'">
<a-badge status="processing" class="running-animation" :text="status.xray.stateMsg" :color="status.xray.color"/>
</template>
<template v-else>
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
<span slot="title">
<a-row type="flex" align="middle" justify="space-between">
<a-col>
<span>{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span>
</a-col>
<a-col>
<a-icon type="bars" :style="{ cursor: 'pointer', float: 'right' }" @click="openLogs()"></a-icon>
</a-col>
</a-row>
</span>
<template slot="content">
<span :style="{ maxWidth: '400px' }" v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</span>
</template>
<a-badge :text="status.xray.stateMsg" :color="status.xray.color"/>
</a-popover>
</template>
</template>
<template #actions>
<a-space v-if="app.ipLimitEnable" direction="horizontal" @click="openXrayLogs()" :style="{ justifyContent: 'center' }">
<a-icon type="bars"></a-icon>
<span v-if="!isMobile">{{ i18n "pages.index.logs" }}</span>
</a-space>
<a-space direction="horizontal" @click="stopXrayService" :style="{ justifyContent: 'center' }">
<a-icon type="poweroff"></a-icon>
<span v-if="!isMobile">{{ i18n "pages.index.stopXray" }}</span>
</a-space>
<a-space direction="horizontal" @click="restartXrayService" :style="{ justifyContent: 'center' }">
<a-icon type="reload"></a-icon>
<span v-if="!isMobile">{{ i18n "pages.index.restartXray" }}</span>
</a-space>
<a-space direction="horizontal" @click="openSelectV2rayVersion" :style="{ justifyContent: 'center' }">
<a-icon type="tool"></a-icon>
<span v-if="!isMobile">
[[ status.xray.version != 'Unknown' ? `v${status.xray.version}` : '{{ i18n "pages.index.xraySwitch" }}' ]]
</span>
</a-space>
</template>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "menu.link" }}' hoverable>
<template #actions>
<a-space direction="horizontal" @click="openLogs()" :style="{ justifyContent: 'center' }">
<a-icon type="bars"></a-icon>
<span v-if="!isMobile">{{ i18n "pages.index.logs" }}</span>
</a-space>
<a-space direction="horizontal" @click="openConfig" :style="{ justifyContent: 'center' }">
<a-icon type="control"></a-icon>
<span v-if="!isMobile">{{ i18n "pages.index.config" }}</span>
</a-space>
<a-space direction="horizontal" @click="openBackup" :style="{ justifyContent: 'center' }">
<a-icon type="cloud-server"></a-icon>
<span v-if="!isMobile">{{ i18n "pages.index.backup" }}</span>
</a-space>
</template>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='3X-UI中文优化版' hoverable>
<a rel="noopener" href="https://github.com/xeefei/3x-ui/releases" target="_blank">
<a-tag color="green">
<span>v{{ .cur_ver }}</span>
</a-tag>
</a>
<a rel="noopener" href="https://t.me/is_Chat_Bot" target="_blank">
<a-tag color="green">
<span>TG私聊交流</span>
</a-tag>
</a>
<a rel="noopener" href="https://t.me/XUI_CN" target="_blank">
<a-tag color="purple">
<span>3X-UI中文交流群</span>
</a-tag>
</a>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "pages.index.operationHours" }}' hoverable>
<a-tag :color="status.xray.color">Xray: [[ TimeFormatter.formatSecond(status.appStats.uptime) ]]</a-tag>
<a-tag color="green">OS: [[ TimeFormatter.formatSecond(status.uptime) ]]</a-tag>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "pages.index.systemLoad" }}' hoverable>
<a-tag color="green">
<a-tooltip>
[[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
<template slot="title">
{{ i18n "pages.index.systemLoadDesc" }}
</template>
</a-tooltip>
</a-tag>
<a rel="noopener" href="https://ping.pe" target="_blank"><a-tag color="green">端口检测</a-tag></a>
<a rel="noopener" href="https://www.speedtest.net" target="_blank"><a-tag color="green">网络测速</a-tag></a>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "usage"}}' hoverable>
<a-tag color="green"> {{ i18n "pages.index.memory" }}: [[ SizeFormatter.sizeFormat(status.appStats.mem) ]] </a-tag>
<a-tag color="green"> {{ i18n "pages.index.threads" }}: [[ status.appStats.threads ]] </a-tag>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "pages.index.overallSpeed" }}' hoverable>
<a-row :gutter="isMobile ? [8,8] : 0">
<a-col :span="12">
<a-custom-statistic title='{{ i18n "pages.index.upload" }}' :value="SizeFormatter.sizeFormat(status.netIO.up)">
<template #prefix>
<a-icon type="arrow-up" />
</template>
<template #suffix>
/s
</template>
</a-custom-statistic>
</a-col>
<a-col :span="12">
<a-custom-statistic title='{{ i18n "pages.index.download" }}' :value="SizeFormatter.sizeFormat(status.netIO.down)">
<template #prefix>
<a-icon type="arrow-down" />
</template>
<template #suffix>
/s
</template>
</a-custom-statistic>
</a-col>
</a-row>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "pages.index.totalData" }}' hoverable>
<a-row :gutter="isMobile ? [8,8] : 0">
<a-col :span="12">
<a-custom-statistic title='{{ i18n "pages.index.sent" }}' :value="SizeFormatter.sizeFormat(status.netTraffic.sent)">
<template #prefix>
<a-icon type="cloud-upload" />
</template>
</a-custom-statistic>
</a-col>
<a-col :span="12">
<a-custom-statistic title='{{ i18n "pages.index.received" }}' :value="SizeFormatter.sizeFormat(status.netTraffic.recv)">
<template #prefix>
<a-icon type="cloud-download" />
</template>
</a-custom-statistic>
</a-col>
</a-row>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "pages.index.ipAddresses" }}' hoverable>
<template #extra>
<a-tooltip :placement="isMobile ? 'topRight' : 'top'">
<template #title>
{{ i18n "pages.index.toggleIpVisibility" }}
</template>
<a-icon :type="showIp ? 'eye' : 'eye-invisible'" :style="{ fontSize: '1rem' }" @click="showIp = !showIp"></a-icon>
</a-tooltip>
</template>
<a-row :class="showIp ? 'ip-visible' : 'ip-hidden'" :gutter="isMobile ? [8,8] : 0">
<a-col :span="isMobile ? 24 : 12">
<a-custom-statistic title="IPv4" :value="status.publicIP.ipv4">
<template #prefix>
<a-icon type="global" />
</template>
</a-custom-statistic>
</a-col>
<a-col :span="isMobile ? 24 : 12">
<a-custom-statistic title="IPv6" :value="status.publicIP.ipv6">
<template #prefix>
<a-icon type="global" />
</template>
</a-custom-statistic>
</a-col>
</a-row>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "pages.index.connectionCount" }}' hoverable>
<a-row :gutter="isMobile ? [8,8] : 0">
<a-col :span="12">
<a-custom-statistic title="TCP" :value="status.tcpCount">
<template #prefix>
<a-icon type="swap" />
</template>
</a-custom-statistic>
</a-col>
<a-col :span="12">
<a-custom-statistic title="UDP" :value="status.udpCount">
<template #prefix>
<a-icon type="swap" />
</template>
</a-custom-statistic>
</a-col>
</a-row>
</a-card>
</a-col>
</a-row>
</template>
</transition>
</a-spin>
</a-layout-content>
</a-layout>
<a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}' :closable="true"
@ok="() => versionModal.visible = false" :class="themeSwitcher.currentTheme" footer="">
<a-collapse default-active-key="1">
<a-collapse-panel key="1" header='Xray'>
<a-alert type="warning" :style="{ marginBottom: '12px', width: '100%' }" message='{{ i18n "pages.index.xraySwitchClickDesk" }}' show-icon></a-alert>
<a-list class="ant-version-list" bordered :style="{ width: '100%' }">
<a-list-item class="ant-version-list-item" v-for="version, index in versionModal.versions">
<a-tag :color="index % 2 == 0 ? 'purple' : 'green'">[[ version ]]</a-tag>
<a-radio :class="themeSwitcher.currentTheme" :checked="version === `v${status.xray.version}`" @click="switchV2rayVersion(version)"></a-radio>
</a-list-item>
</a-list>
</a-collapse-panel>
<a-collapse-panel key="2" header='Geofiles'>
<a-list class="ant-version-list" bordered :style="{ width: '100%' }">
<a-list-item class="ant-version-list-item" v-for="file, index in ['geosite.dat', 'geoip.dat', 'geosite_IR.dat', 'geoip_IR.dat', 'geosite_RU.dat', 'geoip_RU.dat']">
<a-tag :color="index % 2 == 0 ? 'purple' : 'green'">[[ file ]]</a-tag>
<a-icon type="reload" @click="updateGeofile(file)" :style="{ marginRight: '8px' }"/>
</a-list-item>
</a-list>
<div style="margin-top: 5px; display: flex; justify-content: flex-end;">
<a-button @click="updateGeofile('')">{{ i18n "pages.index.geofilesUpdateAll" }}</a-button>
</div>
</a-collapse-panel>
</a-collapse>
</a-modal>
<a-modal id="log-modal" v-model="logModal.visible"
:closable="true" @cancel="() => logModal.visible = false"
:class="themeSwitcher.currentTheme"
width="800px" footer="">
<template slot="title">
{{ i18n "pages.index.logs" }}
<a-icon :spin="logModal.loading"
type="sync"
:style="{ verticalAlign: 'middle', marginLeft: '10px' }"
:disabled="logModal.loading"
@click="openLogs()">
</a-icon>
</template>
<a-form layout="inline">
<a-form-item :style="{ marginRight: '0.5rem' }">
<a-input-group compact>
<a-select size="small" v-model="logModal.rows" :style="{ width: '70px' }"
@change="openLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="10">10</a-select-option>
<a-select-option value="20">20</a-select-option>
<a-select-option value="50">50</a-select-option>
<a-select-option value="100">100</a-select-option>
<a-select-option value="500">500</a-select-option>
</a-select>
<a-select size="small" v-model="logModal.level" :style="{ width: '95px' }"
@change="openLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="debug">Debug</a-select-option>
<a-select-option value="info">Info</a-select-option>
<a-select-option value="notice">Notice</a-select-option>
<a-select-option value="warning">Warning</a-select-option>
<a-select-option value="err">Error</a-select-option>
</a-select>
</a-input-group>
</a-form-item>
<a-form-item>
<a-checkbox v-model="logModal.syslog" @change="openLogs()">SysLog</a-checkbox>
</a-form-item>
<a-form-item :style="{ float: 'right' }">
<a-button type="primary" icon="download" @click="FileManager.downloadTextFile(logModal.logs?.join('\n'), 'x-ui.log')"></a-button>
</a-form-item>
</a-form>
<div class="ant-input" :style="{ height: 'auto', maxHeight: '500px', overflow: 'auto', marginTop: '0.5rem' }" v-html="logModal.formattedLogs"></div>
</a-modal>
<a-modal id="xraylog-modal"
v-model="xraylogModal.visible"
:closable="true" @cancel="() => xraylogModal.visible = false"
:class="themeSwitcher.currentTheme"
width="80vw"
footer="">
<template slot="title">
{{ i18n "pages.index.logs" }}
<a-icon :spin="xraylogModal.loading"
type="sync"
:style="{ verticalAlign: 'middle', marginLeft: '10px' }"
:disabled="xraylogModal.loading"
@click="openXrayLogs()">
</a-icon>
</template>
<a-form layout="inline">
<a-form-item :style="{ marginRight: '0.5rem' }">
<a-input-group compact>
<a-select size="small" v-model="xraylogModal.rows" :style="{ width: '70px' }"
@change="openXrayLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="10">10</a-select-option>
<a-select-option value="20">20</a-select-option>
<a-select-option value="50">50</a-select-option>
<a-select-option value="100">100</a-select-option>
<a-select-option value="500">500</a-select-option>
</a-select>
</a-input-group>
</a-form-item>
<a-form-item label="Filter:">
<a-input size="small" v-model="xraylogModal.filter" @keyup.enter="openXrayLogs()"></a-input>
</a-form-item>
<a-form-item>
<a-checkbox v-model="xraylogModal.showDirect" @change="openXrayLogs()">Direct</a-checkbox>
<a-checkbox v-model="xraylogModal.showBlocked" @change="openXrayLogs()">Blocked</a-checkbox>
<a-checkbox v-model="xraylogModal.showProxy" @change="openXrayLogs()">Proxy</a-checkbox>
</a-form-item>
<a-form-item :style="{ float: 'right' }">
<a-button type="primary" icon="download" @click="FileManager.downloadTextFile(xraylogModal.logs?.join('\n'), 'x-ui.log')"></a-button>
</a-form-item>
</a-form>
<div class="ant-input" :style="{ height: 'auto', maxHeight: '500px', overflow: 'auto', marginTop: '0.5rem' }" v-html="xraylogModal.formattedLogs"></div>
</a-modal>
<a-modal id="backup-modal"
v-model="backupModal.visible"
title='{{ i18n "pages.index.backupTitle" }}'
:closable="true"
footer=""
:class="themeSwitcher.currentTheme">
<a-list class="ant-backup-list" bordered :style="{ width: '100%' }">
<a-list-item class="ant-backup-list-item">
<a-list-item-meta>
<template #title>{{ i18n "pages.index.exportDatabase" }}</template>
<template #description>{{ i18n "pages.index.exportDatabaseDesc" }}</template>
</a-list-item-meta>
<a-button @click="exportDatabase()" type="primary" icon="download"/>
</a-list-item>
<a-list-item class="ant-backup-list-item">
<a-list-item-meta>
<template #title>{{ i18n "pages.index.importDatabase" }}</template>
<template #description>{{ i18n "pages.index.importDatabaseDesc" }}</template>
</a-list-item-meta>
<a-button @click="importDatabase()" type="primary" icon="upload" />
</a-list-item>
</a-list>
</a-modal>
</a-layout>
{{template "page/body_scripts" .}}
{{template "component/aSidebar" .}}
{{template "component/aThemeSwitch" .}}
{{template "component/aCustomStatistic" .}}
{{template "modals/textModal"}}
<script>
class CurTotal {
constructor(current, total) {
this.current = current;
this.total = total;
}
get percent() {
if (this.total === 0) {
return 0;
}
return NumberFormatter.toFixed(this.current / this.total * 100, 2);
}
get color() {
const percent = this.percent;
if (percent < 80) {
return '#008771'; // Green
} else if (percent < 90) {
return "#f37b24"; // Orange
} else {
return "#cf3c3c"; // Red
}
}
}
class Status {
constructor(data) {
this.cpu = new CurTotal(0, 0);
this.cpuCores = 0;
this.logicalPro = 0;
this.cpuSpeedMhz = 0;
this.disk = new CurTotal(0, 0);
this.loads = [0, 0, 0];
this.mem = new CurTotal(0, 0);
this.netIO = { up: 0, down: 0 };
this.netTraffic = { sent: 0, recv: 0 };
this.publicIP = { ipv4: 0, ipv6: 0 };
this.swap = new CurTotal(0, 0);
this.tcpCount = 0;
this.udpCount = 0;
this.uptime = 0;
this.appUptime = 0;
this.appStats = {threads: 0, mem: 0, uptime: 0};
this.xray = { state: 'stop', stateMsg: "", errorMsg: "", version: "", color: "" };
if (data == null) {
return;
}
this.cpu = new CurTotal(data.cpu, 100);
this.cpuCores = data.cpuCores;
this.logicalPro = data.logicalPro;
this.cpuSpeedMhz = data.cpuSpeedMhz;
this.disk = new CurTotal(data.disk.current, data.disk.total);
this.loads = data.loads.map(load => NumberFormatter.toFixed(load, 2));
this.mem = new CurTotal(data.mem.current, data.mem.total);
this.netIO = data.netIO;
this.netTraffic = data.netTraffic;
this.publicIP = data.publicIP;
this.swap = new CurTotal(data.swap.current, data.swap.total);
this.tcpCount = data.tcpCount;
this.udpCount = data.udpCount;
this.uptime = data.uptime;
this.appUptime = data.appUptime;
this.appStats = data.appStats;
this.xray = data.xray;
switch (this.xray.state) {
case 'running':
this.xray.color = "green";
this.xray.stateMsg = '{{ i18n "pages.index.xrayStatusRunning" }}';
break;
case 'stop':
this.xray.color = "orange";
this.xray.stateMsg = '{{ i18n "pages.index.xrayStatusStop" }}';
break;
case 'error':
this.xray.color = "red";
this.xray.stateMsg ='{{ i18n "pages.index.xrayStatusError" }}';
break;
default:
this.xray.color = "gray";
this.xray.stateMsg = '{{ i18n "pages.index.xrayStatusUnknown" }}';
break;
}
}
}
const versionModal = {
visible: false,
versions: [],
show(versions) {
this.visible = true;
this.versions = versions;
},
hide() {
this.visible = false;
},
};
const logModal = {
visible: false,
logs: [],
rows: 20,
level: 'info',
syslog: false,
loading: false,
show(logs) {
this.visible = true;
this.logs = logs;
this.formattedLogs = this.logs?.length > 0 ? this.formatLogs(this.logs) : "No Record...";
},
formatLogs(logs) {
let formattedLogs = '';
const levels = ["DEBUG","INFO","NOTICE","WARNING","ERROR"];
const levelColors = ["#3c89e8","#008771","#008771","#f37b24","#e04141","#bcbcbc"];
logs.forEach((log, index) => {
let [data, message] = log.split(" - ",2);
const parts = data.split(" ")
if(index>0) formattedLogs += '<br>';
if (parts.length === 3) {
const d = parts[0];
const t = parts[1];
const level = parts[2];
const levelIndex = levels.indexOf(level,levels) || 5;
//formattedLogs += `<span style="color: gray;">${index + 1}.</span>`;
formattedLogs += `<span style="color: ${levelColors[0]};">${d} ${t}</span> `;
formattedLogs += `<span style="color: ${levelColors[levelIndex]}">${level}</span>`;
} else {
const levelIndex = levels.indexOf(data,levels) || 5;
formattedLogs += `<span style="color: ${levelColors[levelIndex]}">${data}</span>`;
}
if(message){
if(message.startsWith("XRAY:"))
message = "<b>XRAY: </b>" + message.substring(5);
else
message = "<b>X-UI: </b>" + message;
}
formattedLogs += message ? ' - ' + message : '';
});
return formattedLogs;
},
hide() {
this.visible = false;
},
};
const xraylogModal = {
visible: false,
logs: [],
rows: 20,
showDirect: true,
showBlocked: true,
showProxy: true,
loading: false,
show(logs) {
this.visible = true;
this.logs = logs;
this.formattedLogs = this.logs?.length > 0 ? this.formatLogs(this.logs) : "No Record...";
},
formatLogs(logs) {
let formattedLogs = '';
logs.forEach((log, index) => {
if(index > 0) formattedLogs += '<br>';
const parts = log.split(' ');
if(parts.length === 10) {
const dateTime = `<b>${parts[0]} ${parts[1]}</b>`;
const from = `<b>${parts[3]}</b>`;
const to = `<b>${parts[5].replace(/^\/+/, "")}</b>`;
let outboundColor = '';
if (parts[9] === "b") {
outboundColor = ' style="color: #e04141;"'; //red for blocked
}
else if (parts[9] === "p") {
outboundColor = ' style="color: #3c89e8;"'; //blue for proxies
}
formattedLogs += `<span${outboundColor}>
${dateTime}
${parts[2]}
${from}
${parts[4]}
${to}
${parts.slice(6, 9).join(' ')}
</span>`;
} else {
formattedLogs += `<span>${log}</span>`;
}
});
return formattedLogs;
},
hide() {
this.visible = false;
},
};
const backupModal = {
visible: false,
show() {
this.visible = true;
},
hide() {
this.visible = false;
},
};
const app = new Vue({
delimiters: ['[[', ']]'],
el: '#app',
mixins: [MediaQueryMixin],
data: {
themeSwitcher,
loadingStates: {
fetched: false,
spinning: false
},
status: new Status(),
versionModal,
logModal,
xraylogModal,
backupModal,
loadingTip: '{{ i18n "loading"}}',
showAlert: false,
showIp: false,
ipLimitEnable: false,
},
methods: {
loading(spinning, tip = '{{ i18n "loading"}}') {
this.loadingStates.spinning = spinning;
this.loadingTip = tip;
},
async getStatus() {
try {
const msg = await HttpUtil.post('/server/status');
if (msg.success) {
if (!this.loadingStates.fetched) {
this.loadingStates.fetched = true;
}
this.setStatus(msg.obj, true);
}
} catch (e) {
console.error("Failed to get status:", e);
}
},
setStatus(data) {
this.status = new Status(data);
},
async openSelectV2rayVersion() {
this.loading(true);
const msg = await HttpUtil.post('server/getXrayVersion');
this.loading(false);
if (!msg.success) {
return;
}
versionModal.show(msg.obj);
},
switchV2rayVersion(version) {
this.$confirm({
title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}',
content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}'.replace('#version#', version),
okText: '{{ i18n "confirm"}}',
class: themeSwitcher.currentTheme,
cancelText: '{{ i18n "cancel"}}',
onOk: async () => {
versionModal.hide();
this.loading(true, '{{ i18n "pages.index.dontRefresh"}}');
await HttpUtil.post(`/server/installXray/${version}`);
this.loading(false);
},
});
},
updateGeofile(fileName) {
const isSingleFile = !!fileName;
this.$confirm({
title: '{{ i18n "pages.index.geofileUpdateDialog" }}',
content: isSingleFile
? '{{ i18n "pages.index.geofileUpdateDialogDesc" }}'.replace("#filename#", fileName)
: '{{ i18n "pages.index.geofilesUpdateDialogDesc" }}',
okText: '{{ i18n "confirm"}}',
class: themeSwitcher.currentTheme,
cancelText: '{{ i18n "cancel"}}',
onOk: async () => {
versionModal.hide();
this.loading(true, '{{ i18n "pages.index.dontRefresh"}}');
const url = isSingleFile
? `/server/updateGeofile/${fileName}`
: `/server/updateGeofile`;
await HttpUtil.post(url);
this.loading(false);
},
});
},
async stopXrayService() {
this.loading(true);
const msg = await HttpUtil.post('server/stopXrayService');
this.loading(false);
if (!msg.success) {
return;
}
},
async restartXrayService() {
this.loading(true);
const msg = await HttpUtil.post('server/restartXrayService');
this.loading(false);
if (!msg.success) {
return;
}
},
async openLogs(){
logModal.loading = true;
const msg = await HttpUtil.post('server/logs/'+logModal.rows,{level: logModal.level, syslog: logModal.syslog});
if (!msg.success) {
return;
}
logModal.show(msg.obj);
await PromiseUtil.sleep(500);
logModal.loading = false;
},
async openXrayLogs(){
xraylogModal.loading = true;
const msg = await HttpUtil.post('server/xraylogs/'+xraylogModal.rows,{filter: xraylogModal.filter, showDirect: xraylogModal.showDirect, showBlocked: xraylogModal.showBlocked, showProxy: xraylogModal.showProxy});
if (!msg.success) {
return;
}
xraylogModal.show(msg.obj);
await PromiseUtil.sleep(500);
xraylogModal.loading = false;
},
async openConfig() {
this.loading(true);
const msg = await HttpUtil.post('server/getConfigJson');
this.loading(false);
if (!msg.success) {
return;
}
txtModal.show('config.json', JSON.stringify(msg.obj, null, 2), 'config.json');
},
openBackup() {
backupModal.show();
},
exportDatabase() {
window.location = basePath + 'server/getDb';
},
importDatabase() {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.db';
fileInput.addEventListener('change', async (event) => {
const dbFile = event.target.files[0];
if (dbFile) {
const formData = new FormData();
formData.append('db', dbFile);
backupModal.hide();
this.loading(true);
const uploadMsg = await HttpUtil.post('server/importDB', formData, {
headers: {
'Content-Type': 'multipart/form-data',
}
});
this.loading(false);
if (!uploadMsg.success) {
return;
}
this.loading(true);
const restartMsg = await HttpUtil.post("/panel/setting/restartPanel");
this.loading(false);
if (restartMsg.success) {
this.loading(true);
await PromiseUtil.sleep(5000);
location.reload();
}
}
});
fileInput.click();
},
},
async mounted() {
if (window.location.protocol !== "https:") {
this.showAlert = true;
}
const msg = await HttpUtil.post('/panel/setting/defaultSettings');
if (msg.success) {
this.ipLimitEnable = msg.obj.ipLimitEnable;
}
while (true) {
try {
await this.getStatus();
} catch (e) {
console.error(e);
}
await PromiseUtil.sleep(2000);
}
},
});
</script>
{{ template "page/body_end" .}}