diff options
Diffstat (limited to 'web/html/xui/index.html')
| -rw-r--r-- | web/html/xui/index.html | 743 |
1 files changed, 0 insertions, 743 deletions
diff --git a/web/html/xui/index.html b/web/html/xui/index.html deleted file mode 100644 index 20b44bd8..00000000 --- a/web/html/xui/index.html +++ /dev/null @@ -1,743 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> -{{template "head" .}} -<style> - @media (min-width: 769px) { - .ant-layout-content { - margin: 24px 16px; - } - .ant-card-hoverable { - margin-inline: 0.3rem; - } - .ant-alert-error { - margin-inline: 0.3rem; - } - } - .ant-col-sm-24 { - margin-top: 10px; - } - .ant-card-dark h2 { - color: var(--dark-color-text-primary); - } - .ant-backup-list-item { - gap: 10px; - } - .ant-xray-version-list-item { - --padding: 12px; - padding: var(--padding) !important; - gap: var(--padding); - } - .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-xray-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> - -<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="200" :tip="loadingTip"> - <transition name="list" appear> - <a-alert type="error" v-if="showAlert" style="margin-bottom: 10px" - message='{{ i18n "secAlertTitle" }}' - color="red" - description='{{ i18n "secAlertSsl" }}' - show-icon closable> - </a-alert> - </transition> - <transition name="list" appear> - <template> - <a-row v-if="!status.isLoaded"> - <a-card hoverable style="text-align: center; padding: 30px 0; margin-top: 10px; background: transparent;"> - <a-spin tip="Loading..."></a-spin> - </a-card> - </a-row> - <a-row v-else> - <a-row> - <a-card hoverable> - <a-row> - <a-col :sm="24" :md="12"> - <a-row> - <a-col :span="12" style="text-align: center"> - <a-progress type="dashboard" status="normal" - :stroke-color="status.cpu.color" - :percent="status.cpu.percent"></a-progress> - <div><b>CPU:</b> [[ CPUFormatter.cpuCoreFormat(status.cpuCores) ]] <a-tooltip> - <a-icon type="area-chart"></a-icon> - <template slot="title"> - <div><b>Logical Processors:</b> [[ (status.logicalPro) ]]</div> - <div><b>Speed:</b> [[ CPUFormatter.cpuSpeedFormat(status.cpuSpeedMhz) ]]</div> - </template> - </a-tooltip></div> - </a-col> - <a-col :span="12" style="text-align: 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="text-align: center"> - <a-progress type="dashboard" status="normal" - :stroke-color="status.swap.color" - :percent="status.swap.percent"></a-progress> - <div> - <b>Swap:</b> [[ SizeFormatter.sizeFormat(status.swap.current) ]] / [[ SizeFormatter.sizeFormat(status.swap.total) ]] - </div> - </a-col> - <a-col :span="12" style="text-align: center"> - <a-progress type="dashboard" status="normal" - :stroke-color="status.disk.color" - :percent="status.disk.percent"></a-progress> - <div> - <b>{{ i18n "pages.index.hard"}}:</b> [[ SizeFormatter.sizeFormat(status.disk.current) ]] / [[ SizeFormatter.sizeFormat(status.disk.total) ]] - </div> - </a-col> - </a-row> - </a-col> - </a-row> - </a-card> - </a-row> - <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 != State.Error"> - <a-badge status="processing" class="running-animation" :text="status.xray.state" :color="status.xray.color" style="text-transform: capitalize;"/> - </template> - <template v-else> - <a-popover :overlay-class-name="themeSwitcher.currentTheme"> - <span slot="title" style="font-size: 12pt">An error occurred while running Xray - <a-tag color="purple" style="cursor: pointer; float: right;" @click="openLogs()">{{ i18n "pages.index.logs" }}</a-tag> - </span> - <template slot="content"> - <p style="max-width: 400px" v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</p> - </template> - <a-badge :text="status.xray.state" :color="status.xray.color" style="text-transform: capitalize;"/> - </a-popover> - </template> - </template> - <template #actions> - <a-space direction="horizontal" @click="stopXrayService" style="justify-content: 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="justify-content: 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="justify-content: 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="justify-content: 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="justify-content: 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="justify-content: 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/MHSanaei/3x-ui/releases" target="_blank"><a-tag color="green">v{{ .cur_ver }}</a-tag></a> - <a rel="noopener" href="https://t.me/XrayUI" target="_blank"><a-tag color="green">@XrayUI</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-card> - </a-col> - <a-col :sm="24" :lg="12"> - <a-card title='{{ i18n "usage"}}' hoverable> - <a-tag color="green"> RAM: [[ SizeFormatter.sizeFormat(status.appStats.mem) ]] </a-tag> - <a-tag color="green"> 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> - <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> - <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> - <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'"> - <a-col :xs="24" :xxl="12" :style="{ marginTop: isMobile ? '10px' : 0 }"> - <a-custom-statistic title="IPv4" :value="status.publicIP.ipv4"> - <template #prefix> - <a-icon type="global" /> - </template> - </a-custom-statistic> - </a-col> - <a-col :xs="24" :xxl="12" :style="{ marginTop: isMobile ? '10px' : 0 }"> - <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> - <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-alert type="warning" style="margin-bottom: 12px; width: 100%;" - message='{{ i18n "pages.index.xraySwitchClickDesk" }}' show-icon></a-alert> - <a-list class="ant-xray-version-list" bordered style="width: 100%;"> - <a-list-item class="ant-xray-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-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="vertical-align: middle; margin-left: 10px;" - :disabled="logModal.loading" - @click="openLogs()"> - </a-icon> - </template> - <a-form layout="inline"> - <a-form-item style="margin-right: 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" - :href="'data:application/text;charset=utf-8,' + encodeURIComponent(logModal.logs?.join('\n'))" download="x-ui.log"> - </a-button> - </a-form-item> - </a-form> - <div class="ant-input" style="height: auto; max-height: 500px; overflow: auto; margin-top: 0.5rem;" v-html="logModal.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 "js" .}} -{{template "component/aSidebar" .}} -{{template "component/aThemeSwitch" .}} -{{template "component/aCustomStatistic" .}} -{{template "modals/textModal"}} -<script> - const State = { - Running: "running", - Stop: "stop", - Error: "error", - } - Object.freeze(State); - - 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, isLoaded = false) { - 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: State.Stop, errorMsg: "", version: "", color: "" }; - - if (data == null) { - return; - } - - this.isLoaded = isLoaded; - 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 State.Running: - this.xray.color = "green"; - break; - case State.Stop: - this.xray.color = "orange"; - break; - case State.Error: - this.xray.color = "red"; - break; - default: - this.xray.color = "gray"; - } - } - } - - 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 backupModal = { - visible: false, - show() { - this.visible = true; - }, - hide() { - this.visible = false; - }, - }; - - const app = new Vue({ - delimiters: ['[[', ']]'], - el: '#app', - data: { - themeSwitcher, - status: new Status(), - versionModal, - logModal, - backupModal, - spinning: false, - loadingTip: '{{ i18n "loading"}}', - showAlert: false, - showIp: false, - isMobile: DeviceUtils.isMobile() - }, - methods: { - loading(spinning, tip = '{{ i18n "loading"}}') { - this.spinning = spinning; - this.loadingTip = tip; - }, - async getStatus() { - try { - const msg = await HttpUtil.post('/server/status'); - if (msg.success) { - this.setStatus(msg.obj, true); - } - } catch (e) { - console.error("Failed to get status:", e); - } - }, - setStatus(data, isLoaded = false) { - this.status = new Status(data, isLoaded); - }, - 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"}}' + ` ${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); - }, - }); - }, - 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 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; - } - while (true) { - try { - await this.getStatus(); - } catch (e) { - console.error(e); - } - await PromiseUtil.sleep(2000); - } - }, - }); -</script> -</body> -</html> |
