mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 21:24:10 +00:00
feat: add nodes.html page with node list and config form
This commit is contained in:
parent
fe46db245e
commit
7d75d02c1e
1 changed files with 255 additions and 0 deletions
255
web/html/nodes.html
Normal file
255
web/html/nodes.html
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
{{ template "page/head_start" .}}
|
||||
{{ template "page/head_end" .}}
|
||||
|
||||
{{ template "page/body_start" .}}
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' nodes-page'">
|
||||
<a-sidebar></a-sidebar>
|
||||
<a-layout id="content-layout">
|
||||
<a-layout-content>
|
||||
<a-spin :spinning="loading" :delay="200" tip='{{ i18n "loading"}}'>
|
||||
<transition name="list" appear>
|
||||
<a-row :gutter="[isMobile ? 8 : 16, isMobile ? 8 : 12]">
|
||||
<!-- Connected Nodes Section -->
|
||||
<a-col :span="24">
|
||||
<a-card hoverable>
|
||||
<template #title>
|
||||
<a-row type="flex" justify="space-between" align="middle">
|
||||
<a-col>
|
||||
<a-space>
|
||||
<a-icon type="cluster"></a-icon>
|
||||
<span>{{ i18n "pages.nodes.connectedNodes" }}</span>
|
||||
<a-tag :color="nodeRole === 'master' ? 'blue' : 'green'">[[ nodeRole ]]</a-tag>
|
||||
</a-space>
|
||||
</a-col>
|
||||
<a-col>
|
||||
<a-button icon="reload" size="small" @click="loadNodes">{{ i18n "pages.nodes.refresh" }}</a-button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
<a-table
|
||||
v-if="nodeRole === 'master'"
|
||||
:columns="nodeColumns"
|
||||
:data-source="nodes"
|
||||
:row-key="record => record.nodeId"
|
||||
:pagination="false"
|
||||
:scroll="isMobile ? { x: 700 } : undefined"
|
||||
size="middle">
|
||||
<template slot="status" slot-scope="text, record">
|
||||
<a-badge :status="record.online ? 'success' : 'error'" :text="record.online ? '{{ i18n "pages.nodes.online" }}' : '{{ i18n "pages.nodes.offline" }}'" />
|
||||
</template>
|
||||
<template slot="role" slot-scope="text, record">
|
||||
<a-tag :color="record.nodeRole === 'master' ? 'blue' : 'green'">[[ record.nodeRole ]]</a-tag>
|
||||
</template>
|
||||
<template slot="lastHeartbeat" slot-scope="text, record">
|
||||
[[ record.lastHeartbeatAt ? formatTime(record.lastHeartbeatAt) : '-' ]]
|
||||
</template>
|
||||
<template slot="lastSync" slot-scope="text, record">
|
||||
[[ record.lastSyncAt ? formatTime(record.lastSyncAt) : '-' ]]
|
||||
</template>
|
||||
</a-table>
|
||||
<div v-if="nodeRole === 'worker'">
|
||||
<a-empty v-if="nodes.length === 0" description='{{ i18n "pages.nodes.noWorkerNodes" }}' />
|
||||
<a-descriptions v-else bordered size="small" :column="isMobile ? 1 : 2">
|
||||
<a-descriptions-item label='{{ i18n "pages.nodes.nodeId" }}'>[[ nodes[0].nodeId ]]</a-descriptions-item>
|
||||
<a-descriptions-item label='{{ i18n "pages.nodes.status" }}'>
|
||||
<a-badge :status="nodes[0].online ? 'success' : 'error'" :text="nodes[0].online ? '{{ i18n "pages.nodes.online" }}' : '{{ i18n "pages.nodes.offline" }}'" />
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label='{{ i18n "pages.nodes.lastHeartbeat" }}'>[[ nodes[0].lastHeartbeatAt ? formatTime(nodes[0].lastHeartbeatAt) : '-' ]]</a-descriptions-item>
|
||||
<a-descriptions-item label='{{ i18n "pages.nodes.lastSync" }}'>[[ nodes[0].lastSyncAt ? formatTime(nodes[0].lastSyncAt) : '-' ]]</a-descriptions-item>
|
||||
<a-descriptions-item label='{{ i18n "pages.nodes.syncVersion" }}'>[[ nodes[0].lastSeenVersion ]]</a-descriptions-item>
|
||||
<a-descriptions-item label='{{ i18n "pages.nodes.error" }}'>[[ nodes[0].lastError || '-' ]]</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
<a-empty v-if="nodeRole === 'master' && nodes.length === 0" description='{{ i18n "pages.nodes.noWorkerNodes" }}' />
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<!-- Current Node Config Section -->
|
||||
<a-col :span="24">
|
||||
<a-card hoverable>
|
||||
<template #title>
|
||||
<a-space>
|
||||
<a-icon type="setting"></a-icon>
|
||||
<span>{{ i18n "pages.nodes.currentNodeConfig" }}</span>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-form layout="vertical">
|
||||
<a-row :gutter="16">
|
||||
<a-col :xs="24" :sm="12" :md="8">
|
||||
<a-form-item label='{{ i18n "pages.nodes.role" }}'>
|
||||
<a-input :value="nodeConfig.role" disabled></a-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :md="8">
|
||||
<a-form-item label='{{ i18n "pages.nodes.nodeId" }}'>
|
||||
<a-input :value="nodeConfig.nodeId" disabled></a-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :md="8">
|
||||
<a-form-item label='{{ i18n "pages.nodes.dbType" }}'>
|
||||
<a-select v-model="nodeConfig.dbType" :disabled="saving">
|
||||
<a-select-option value="sqlite">SQLite</a-select-option>
|
||||
<a-select-option value="mysql">MySQL/MariaDB</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :md="8">
|
||||
<a-form-item label='{{ i18n "pages.nodes.syncInterval" }}'>
|
||||
<a-input-number v-model="nodeConfig.syncInterval" :min="5" :max="3600" style="width: 100%"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :md="8">
|
||||
<a-form-item label='{{ i18n "pages.nodes.trafficFlushInterval" }}'>
|
||||
<a-input-number v-model="nodeConfig.trafficFlushInterval" :min="5" :max="3600" style="width: 100%"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-divider>{{ i18n "pages.nodes.dbHost" }}</a-divider>
|
||||
<a-row :gutter="16">
|
||||
<a-col :xs="24" :sm="12" :md="8">
|
||||
<a-form-item label='{{ i18n "pages.nodes.dbHost" }}'>
|
||||
<a-input v-model="nodeConfig.dbHost" :disabled="saving"></a-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :md="8">
|
||||
<a-form-item label='{{ i18n "pages.nodes.dbPort" }}'>
|
||||
<a-input v-model="nodeConfig.dbPort" :disabled="saving"></a-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :md="8">
|
||||
<a-form-item label='{{ i18n "pages.nodes.dbName" }}'>
|
||||
<a-input v-model="nodeConfig.dbName" :disabled="saving"></a-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :md="8">
|
||||
<a-form-item label='{{ i18n "pages.nodes.dbUser" }}'>
|
||||
<a-input v-model="nodeConfig.dbUser" :disabled="saving"></a-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :md="8">
|
||||
<a-form-item label='{{ i18n "pages.nodes.dbPass" }}'>
|
||||
<a-input-password v-model="nodeConfig.dbPass" :disabled="saving"></a-input-password>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item>
|
||||
<a-button type="primary" icon="save" :loading="saving" @click="saveConfig">
|
||||
{{ i18n "pages.nodes.save" }}
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</transition>
|
||||
</a-spin>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
{{template "page/body_scripts" .}}
|
||||
<script>
|
||||
const app = new Vue({
|
||||
el: '#app',
|
||||
delimiters: ['[[', ']]'],
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
saving: false,
|
||||
nodeRole: '',
|
||||
nodes: [],
|
||||
nodeConfig: {
|
||||
role: '',
|
||||
nodeId: '',
|
||||
syncInterval: 30,
|
||||
trafficFlushInterval: 10,
|
||||
dbType: '',
|
||||
dbHost: '',
|
||||
dbPort: '',
|
||||
dbUser: '',
|
||||
dbPass: '',
|
||||
dbName: '',
|
||||
},
|
||||
nodeColumns: [
|
||||
{ title: '{{ i18n "pages.nodes.nodeId" }}', dataIndex: 'nodeId', width: 150 },
|
||||
{ title: '{{ i18n "pages.nodes.status" }}', scopedSlots: { customRender: 'status' }, width: 100 },
|
||||
{ title: '{{ i18n "pages.nodes.role" }}', scopedSlots: { customRender: 'role' }, width: 80 },
|
||||
{ title: '{{ i18n "pages.nodes.lastHeartbeat" }}', scopedSlots: { customRender: 'lastHeartbeat' }, width: 180 },
|
||||
{ title: '{{ i18n "pages.nodes.lastSync" }}', scopedSlots: { customRender: 'lastSync' }, width: 180 },
|
||||
{ title: '{{ i18n "pages.nodes.syncVersion" }}', dataIndex: 'lastSeenVersion', width: 120 },
|
||||
{ title: '{{ i18n "pages.nodes.error" }}', dataIndex: 'lastError', ellipsis: true },
|
||||
],
|
||||
refreshTimer: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isMobile() {
|
||||
return window.innerWidth <= 768;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadNodes() {
|
||||
try {
|
||||
const res = await axios.get('api/nodes/list');
|
||||
if (res.data.success) {
|
||||
this.nodes = res.data.obj;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load nodes', e);
|
||||
}
|
||||
},
|
||||
async loadConfig() {
|
||||
try {
|
||||
const res = await axios.get('api/nodes/config');
|
||||
if (res.data.success) {
|
||||
Object.assign(this.nodeConfig, res.data.obj);
|
||||
this.nodeRole = res.data.obj.role;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load node config', e);
|
||||
}
|
||||
},
|
||||
async saveConfig() {
|
||||
this.saving = true;
|
||||
try {
|
||||
const res = await axios.post('api/nodes/config', {
|
||||
syncInterval: this.nodeConfig.syncInterval,
|
||||
trafficFlushInterval: this.nodeConfig.trafficFlushInterval,
|
||||
dbType: this.nodeConfig.dbType,
|
||||
dbHost: this.nodeConfig.dbHost,
|
||||
dbPort: this.nodeConfig.dbPort,
|
||||
dbUser: this.nodeConfig.dbUser,
|
||||
dbPass: this.nodeConfig.dbPass,
|
||||
dbName: this.nodeConfig.dbName,
|
||||
});
|
||||
if (res.data.success) {
|
||||
this.$message.success(res.data.msg);
|
||||
} else {
|
||||
this.$message.error(res.data.msg);
|
||||
}
|
||||
} catch (e) {
|
||||
this.$message.error('Save failed');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
formatTime(ts) {
|
||||
if (!ts) return '-';
|
||||
return moment.unix(ts).format('YYYY-MM-DD HH:mm:ss');
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadNodes();
|
||||
this.loadConfig();
|
||||
this.refreshTimer = setInterval(() => {
|
||||
this.loadNodes();
|
||||
}, 10000);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.refreshTimer) {
|
||||
clearInterval(this.refreshTimer);
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
{{ template "page/body_end" }}
|
||||
</html>
|
||||
Loading…
Reference in a new issue