Fixed the errors that prevented the project from launching and displaying the page. Fixed the modal window to correctly appear. Fixed the API to handle CRUD requests.

This commit is contained in:
Дмитрий Саенко 2025-10-09 12:18:49 +03:00
parent c0ec0221e2
commit 6c3dc5ff68
7 changed files with 14164 additions and 153 deletions

View file

@ -121,10 +121,10 @@ type Client struct {
} }
type Server struct { type Server struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"` Id int `json:"id" gorm:"primaryKey;autoIncrement" form:"id"`
Name string `json:"name" gorm:"unique;not null"` Name string `json:"name" gorm:"unique;not null" form:"name"`
Address string `json:"address" gorm:"not null"` Address string `json:"address" gorm:"not null" form:"address"`
Port int `json:"port" gorm:"not null"` Port int `json:"port" gorm:"not null" form:"port"`
APIKey string `json:"apiKey" gorm:"not null"` APIKey string `json:"apiKey" gorm:"not null" form:"apiKey"`
Enable bool `json:"enable" gorm:"default:true"` Enable bool `json:"enable" gorm:"default:true" form:"enable"`
} }

13894
web/assets/bootstrap/bootstrap.min.css vendored Normal file

File diff suppressed because it is too large Load diff

7
web/assets/bootstrap/bootstrap.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -12,9 +12,10 @@ import (
// APIController handles the main API routes for the 3x-ui panel, including inbounds and server management. // APIController handles the main API routes for the 3x-ui panel, including inbounds and server management.
type APIController struct { type APIController struct {
BaseController BaseController
inboundController *InboundController inboundController *InboundController
serverController *ServerController serverController *ServerController
Tgbot service.Tgbot multiServerController *MultiServerController
Tgbot service.Tgbot
} }
// NewAPIController creates a new APIController instance and initializes its routes. // NewAPIController creates a new APIController instance and initializes its routes.
@ -48,6 +49,10 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
server := api.Group("/server") server := api.Group("/server")
a.serverController = NewServerController(server) a.serverController = NewServerController(server)
// Servers api
servers := api.Group("/servers")
a.multiServerController = NewMultiServerController(servers)
// Extra routes // Extra routes
api.GET("/backuptotgbot", a.BackuptoTgbot) api.GET("/backuptotgbot", a.BackuptoTgbot)
} }

View file

@ -20,7 +20,6 @@ func NewMultiServerController(g *gin.RouterGroup) *MultiServerController {
} }
func (c *MultiServerController) initRouter(g *gin.RouterGroup) { func (c *MultiServerController) initRouter(g *gin.RouterGroup) {
g = g.Group("/server")
g.GET("/list", c.getServers) g.GET("/list", c.getServers)
g.POST("/add", c.addServer) g.POST("/add", c.addServer)

View file

@ -1,174 +1,277 @@
{{template "header" .}} {{ template "page/head_start" .}}
<!-- <link rel="stylesheet" href="{{ .base_path }}assets/jquery/jquery.modal.min.css" /> -->
<link rel="stylesheet" href="{{ .base_path }}assets/bootstrap/bootstrap.min.css">
{{ template "page/head_end" .}} {{ template
"page/body_start" .}}
<div id="app" class="row" v-cloak> <a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' settings-page'">
<div class="col-md-12"> <a-sidebar></a-sidebar>
<div class="card"> <a-layout id="content-layout">
<div class="card-header"> <a-layout-content>
<h3 class="card-title">Server Management</h3> <a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}'>
<div class="card-tools"> <transition name="list" appear>
<button class="btn btn-primary" @click="showAddModal">Add Server</button> <a-alert type="error" v-if="confAlerts.length>0 && loadingStates.fetched"
</div> :style="{ marginBottom: '10px' }" message='{{ i18n "secAlertTitle" }}' color="red" show-icon
</div> slot="description">
<div class="card-body"> <b>{{ i18n "secAlertConf" }}</b>
<table class="table table-bordered"> <ul>
<thead> <li v-for="a in confAlerts">[[ a ]]</li>
<tr> </ul>
<th>#</th> </template>
<th>Name</th> </a-alert>
<th>Address</th> </transition>
<th>Port</th> <transition name="list" appear>
<th>Enabled</th> <template>
<th>Actions</th> <div class="row" v-cloak>
</tr> <div class="col-md-12">
</thead> <div class="card-header">
<tbody> <h3 class="card-title">Server Management</h3>
<tr v-for="(server, index) in servers"> <div class="card-tools">
<td>{{index + 1}}</td> <button class="btn btn-primary" @click="showAddModal">Add Server</button>
<td>{{server.name}}</td> </div>
<td>{{server.address}}</td> </div>
<td>{{server.port}}</td> <div class="card-body">
<td> <table class="table table-bordered">
<span v-if="server.enable" class="badge bg-success">Yes</span> <thead>
<span v-else class="badge bg-danger">No</span> <tr>
</td> <th>#</th>
<td> <th>Name</th>
<button class="btn btn-info btn-sm" @click="showEditModal(server)">Edit</button> <th>Address</th>
<button class="btn btn-danger btn-sm" @click="deleteServer(server.id)">Delete</button> <th>Port</th>
</td> <th>Enabled</th>
</tr> <th>Actions</th>
</tbody> </tr>
</table> </thead>
</div> <tbody>
</div> <template v-if="servers.length>0">
</div> <tr v-for="(server, index) in servers">
<td>[[ index + 1 ]]</td>
<td>[[ server.name ]]</td>
<td>[[ server.address ]]</td>
<td>[[ server.port ]]</td>
<td><span v-if="server.enable"
class="badge bg-success">Yes</span><span v-else
class="badge bg-danger">No</span>
</td>
<td><button class="btn btn-info btn-sm"
@click="showEditModal(server)">Edit</button><button
class="btn btn-danger btn-sm"
@click="deleteServer(server.id)">Delete</button></td>
</tr>
</template>
<tr v-else>
<td colspan="6" class="text-center">No servers found</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Add/Edit Modal -->
<div class="modal fade" id="serverModal" tabindex="-1" role="dialog"> </template>
<div class="modal-dialog" role="document"> </transition>
<div class="modal-content"> <transition>
<div class="modal-header"> <template>
<h5 class="modal-title">{{modal.title}}</h5> <!-- Add/Edit Modal -->
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> <div class="modal fade" data-backdrop="false" id="serverModal" tabindex="-1" role="dialog">
<span aria-hidden="true">&times;</span> <div class="modal-dialog" role="document">
</button> <div class="modal-content">
</div> <div class="modal-header">
<div class="modal-body"> <h5 class="modal-title">[[ modal.title ]]</h5>
<form> <button type="button" class="btn-close" data-bs-dismiss="modal"
<div class="form-group"> aria-label="Close">
<label>Name</label> <span aria-hidden="true">&times;</span>
<input type="text" class="form-control" v-model="modal.server.name"> </button>
</div>
<div class="modal-body">
<form>
<div class="form-group">
<label>Name</label>
<input type="text" class="form-control" v-model="modal.server.name" />
</div>
<div class="form-group">
<label>Address (IP or Domain)</label>
<input type="text" class="form-control"
v-model="modal.server.address" />
</div>
<div class="form-group">
<label>Port</label>
<input type="number" class="form-control"
v-model.number="modal.server.port" />
</div>
<div class="form-group">
<label>API Key</label>
<input type="text" class="form-control" v-model="modal.server.apiKey" />
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input"
v-model="modal.server.enable" />
<label class="form-check-label">Enabled</label>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Close
</button>
<button type="button" class="btn btn-primary" @click="saveServer">
Save
</button>
</div>
</div>
</div>
</div> </div>
<div class="form-group"> </template>
<label>Address (IP or Domain)</label> </transition>
<input type="text" class="form-control" v-model="modal.server.address">
</div> </a-spin>
<div class="form-group">
<label>Port</label> </a-layout-content>
<input type="number" class="form-control" v-model.number="modal.server.port">
</div> </a-layout>
<div class="form-group"> </a-layout>
<label>API Key</label>
<input type="text" class="form-control" v-model="modal.server.apiKey">
</div> {{template "page/body_scripts" .}} {{template "component/aSidebar" .}}
<div class="form-check"> {{template "component/aThemeSwitch" .}}
<input type="checkbox" class="form-check-input" v-model="modal.server.enable"> <!-- <script src="{{ .base_path }}assets/jquery/jquery.min.js"></script> -->
<label class="form-check-label">Enabled</label> <!--<script src="{{ .base_path }}assets/jquery/jquery.modal.min.js"></script> -->
</div>
</form> <script src="{{ .base_path }}assets/bootstrap/bootstrap.min.js"></script>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" @click="saveServer">Save</button>
</div>
</div>
</div>
</div>
</div>
<script> <script>
const app = new Vue({ const app = new Vue({
el: '#app', delimiters: ["[[", "]]"],
mixins: [MediaQueryMixin],
el: "#app",
data: { data: {
servers: [ themeSwitcher,
{ loadingStates: {
id: 1, fetched: false,
name: 'Server 1', spinning: false,
address: '127.0.0.1', },
port: 8080, servers: [],
apiKey: '1234567890',
enable: true
},
{
id: 2,
name: 'Server 2',
address: 'example.com',
port: 8081,
apiKey: '0987654321',
enable: false
}
],
modal: { modal: {
title: '', title: "",
server: { server: {
name: '', name: "",
address: '', address: "",
port: 0, port: 0,
apiKey: '', apiKey: "",
enable: true enable: true,
} },
} },
}, },
methods: { methods: {
showAddModal() { loadServers() {
this.modal.title = 'Add Server';
this.modal.server = { axios.get('{{.base_path}}panel/api/servers/list')
name: '',
address: '',
port: 0,
apiKey: '',
enable: true
};
$('#serverModal').modal('show');
},
showEditModal(server) {
this.modal.title = 'Edit Server';
this.modal.server = Object.assign({}, server);
$('#serverModal').modal('show');
},
saveServer() {
let url = '{{.base_path}}server/add';
if (this.modal.server.id) {
url = `{{.base_path}}server/update/${this.modal.server.id}`;
}
axios.post(url, this.modal.server)
.then(response => { .then(response => {
alert(response.data.msg); this.servers = response.data.obj;
$('#serverModal').modal('hide'); if (this.servers.length == 0) {
this.loadServers();
}
}) })
.catch(error => { .catch(error => {
alert(error);
});
},
showAddModal() {
this.modal.title = "Add Server";
this.modal.server = {
name: "",
address: "",
port: 0,
apiKey: "",
enable: true,
};
const modalEl = document.getElementById('serverModal');
const modal = new bootstrap.Modal(modalEl);
modal.show();
},
showEditModal(server) {
this.modal.title = "Edit Server";
this.modal.server = Object.assign({}, server);
const modalEl = document.getElementById('serverModal');
const modal = new bootstrap.Modal(modalEl);
modal.show();
},
saveServer() {
let url = "{{.base_path}}panel/api/servers/add";
if (this.modal.server.id) {
url = `{{.base_path}}panel/api/servers/update/${this.modal.server.id}`;
}
console.log(this.modal.server);
axios
.post(url, this.modal.server)
.then((response) => {
alert(response.data.msg);
const modalEl = document.getElementById('serverModal');
const modal = bootstrap.Modal.getInstance(modalEl);
modal.hide();
this.loadServers();
})
.catch((error) => {
alert(error.response.data.msg); alert(error.response.data.msg);
}); });
}, },
deleteServer(id) { deleteServer(id) {
if (!confirm('Are you sure you want to delete this server?')) { if (!confirm("Are you sure you want to delete this server?")) {
return; return;
} }
axios.post(`{{.base_path}}server/del/${id}`) axios
.then(response => { .post(`{{.base_path}}panel/api/servers/del/${id}`)
.then((response) => {
alert(response.data.msg); alert(response.data.msg);
this.loadServers(); this.loadServers();
}) })
.catch(error => { .catch((error) => {
alert(error.response.data.msg); alert(error.response.data.msg);
}); });
} },
}, },
computed: {
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" }}');
}
if (this.allSetting.subJsonEnable) {
subJsonPath =
this.allSetting.subJsonURI.length > 0
? new URL(this.allSetting.subJsonURI).pathname
: this.allSetting.subJsonPath;
if (subJsonPath == "/json/")
alerts.push('{{ i18n "secAlertSubJsonURI" }}');
}
return alerts;
},
},
},
mounted() { mounted() {
this.loadServers(); this.loadServers();
} },
}); });
</script> </script>
{{ template "page/body_end" .}}
{{template "footer" .}}

View file

@ -72,6 +72,9 @@
"emptyReverseDesc" = "Нет добавленных реверс-прокси." "emptyReverseDesc" = "Нет добавленных реверс-прокси."
"somethingWentWrong" = "Что-то пошло не так" "somethingWentWrong" = "Что-то пошло не так"
"Server" = "Сервер"
"Servers" = "Серверы"
[subscription] [subscription]
"title" = "Информация о подписке" "title" = "Информация о подписке"
"subId" = "ID подписки" "subId" = "ID подписки"