Browse Source

feat: tabler theme in admin/node

Cat 3 years ago
parent
commit
09537cd911

+ 2 - 1
app/routes.php

@@ -147,8 +147,9 @@ return function (SlimApp $app): void {
         $this->post('/node', App\Controllers\Admin\NodeController::class . ':add');
         $this->get('/node/{id}/edit', App\Controllers\Admin\NodeController::class . ':edit');
         $this->post('/node/{id}/password_reset', App\Controllers\Admin\NodeController::class . ':resetNodePassword');
+        $this->post('/node/{id}/copy', App\Controllers\Admin\NodeController::class . ':copy');
         $this->put('/node/{id}', App\Controllers\Admin\NodeController::class . ':update');
-        $this->delete('/node', App\Controllers\Admin\NodeController::class . ':delete');
+        $this->delete('/node/{id}', App\Controllers\Admin\NodeController::class . ':delete');
         $this->post('/node/ajax', App\Controllers\Admin\NodeController::class . ':ajax');
 
         // Ticket Mange

+ 163 - 173
resources/views/tabler/admin/node/create.tpl

@@ -1,149 +1,172 @@
-{include file='admin/main.tpl'}
+{include file='admin/tabler_header.tpl'}
 
 <script src="//cdn.jsdelivr.net/npm/[email protected]/dist/jsoneditor.min.js"></script>
 <link href="//cdn.jsdelivr.net/npm/[email protected]/dist/jsoneditor.min.css" rel="stylesheet" type="text/css">
 
-<main class="content">
-    <div class="content-header ui-content-header">
-        <div class="container">
-            <h1 class="content-heading">添加节点</h1>
+<div class="page-wrapper">
+    <div class="container-xl">
+        <div class="page-header d-print-none text-white">
+            <div class="row align-items-center">
+                <div class="col">
+                    <h2 class="page-title">
+                        <span class="home-title">创建节点</span>
+                    </h2>
+                    <div class="page-pretitle my-3">
+                        <span class="home-subtitle">创建各类节点</span>
+                    </div>
+                </div>
+                <div class="col-auto ms-auto d-print-none">
+                    <div class="btn-list">
+                        <a id="create-node" href="#" class="btn btn-primary d-none d-sm-inline-block">
+                            <i class="icon ti ti-device-floppy"></i>
+                            保存
+                        </a>
+                        <a id="create-node" href="#" class="btn btn-primary d-sm-none btn-icon">
+                            <i class="icon ti ti-device-floppy"></i>
+                        </a>
+                    </div>
+                </div>
+            </div>
         </div>
     </div>
-    <div class="container">
-        <div class="col-lg-12 col-sm-12">
-            <section class="content-inner margin-top-no">
-                <form id="main_form">
+    <div class="page-body">
+        <div class="container-xl">
+            <div class="row row-deck row-cards">
+                <div class="col-md-6 col-sm-12">
                     <div class="card">
-                        <div class="card-main">
-                            <div class="card-inner">
-                                <div class="form-group form-group-label">
-                                    <label class="floating-label" for="name">节点名称</label>
-                                    <input class="form-control maxwidth-edit" id="name" type="text" name="name">
+                        <div class="card-header card-header-light">
+                            <h3 class="card-title">基础信息</h3>
+                        </div>
+                        <div class="card-body">
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">名称</label>
+                                <div class="col">
+                                    <input id="name" type="text" class="form-control" value="">
+                                </div>
+                            </div>
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">连接地址</label>
+                                <div class="col">
+                                    <textarea id="server" class="col form-control" rows="5"></textarea>
                                 </div>
-                                <div class="form-group form-group-label">
-                                    <label class="floating-label" for="server">节点地址</label>
-                                    <input class="form-control maxwidth-edit" id="server" type="text" name="server">
-                                    <p class="form-control-guide"><i class="mdi mdi-information"></i>如果填写为域名,“节点IP”会自动设置为解析的IP</p>
+                            </div>
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">服务器IP</label>
+                                <div class="col">
+                                    <input id="node_ip" type="text" class="form-control" value="">
                                 </div>
-                                <div class="form-group form-group-label">
-                                    <label class="floating-label" for="server">节点IP</label>
-                                    <input class="form-control maxwidth-edit" id="node_ip" name="node_ip" type="text">
-                                    <p class="form-control-guide"><i class="mdi mdi-information"></i>如果“节点地址”填写为域名,则此处的值会被忽视
-                                    </p>
+                            </div>
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">流量倍率</label>
+                                <div class="col">
+                                    <input id="traffic_rate" type="text" class="form-control"
+                                        value="">
                                 </div>
-                                <div class="form-group form-group-label">
-                                    <label class="floating-label" for="rate">流量比例</label>
-                                    <input class="form-control maxwidth-edit" id="rate" type="text" name="rate"
-                                           value="1">
+                            </div>
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">接入类型</label>
+                                <div class="col">
+                                    <select id="sort" class="col form-select">
+                                        <option value="11">V2Ray</option>
+                                        <option value="14">Trojan</option>
+                                        <option value="0">ShadowSocks</option>
+                                        <option value="1">ShadowSocksR</option>
+                                        <option value="9">ShadowsocksR 单端口多用户(旧)</option>
+                                    </select>
                                 </div>
-                                <div class="form-group form-group-label">
-                                    <label for="mu_only">
-                                        <label class="floating-label" for="sort">单端口多用户启用</label>
-                                        <select id="mu_only" class="form-control maxwidth-edit" name="is_multi_user">
-                                            <option value="-1">只启用普通端口</option>
-                                            <option value="1">只启用单端口多用户</option>
-                                        </select>
-                                    </label>
+                            </div>
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">单端口多用户</label>
+                                <div class="col">
+                                    <select id="mu_only" class="col form-select">
+                                        <option value="-1">只启用普通端口</option>
+                                        <option value="1">只启用单端口多用户</option>
+                                    </select>
                                 </div>
-                                <div class="form-group form-group-label">
-                                    <div class="checkbox switch">
-                                        <label for="type">
-                                            <input checked class="access-hide" id="type" type="checkbox"
-                                                   name="type"><span class="switch-toggle"></span>是否显示
+                            </div>
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">自定义配置</label>
+                                <dev id="custom_config"></dev>
+                                <label class="form-label col-3 col-form-label">
+                                    请参考 <a href="//wiki.sspanel.org/#/setup-custom-config" target="_blank">wiki.sspanel.org/#/setup-custom-config</a> 修改节点自定义配置
+                                </label>
+                            </div>
+                            <div class="hr-text">
+                                <span>高级选项</span>
+                            </div>
+                            <div class="mb-3">
+                                <div class="divide-y">
+                                    <div>
+                                        <label class="row">
+                                            <span class="col">显示此节点</span>
+                                            <span class="col-auto">
+                                                <label class="form-check form-check-single form-switch">
+                                                    <input id="type" class="form-check-input" type="checkbox"
+                                                        checked="">
+                                                </label>
+                                            </span>
                                         </label>
                                     </div>
                                 </div>
                             </div>
                         </div>
                     </div>
+                </div>
+                <div class="col-md-6 col-sm-12">
                     <div class="card">
-                        <div class="card-main">
-                            <div class="card-inner">         
-                                <div class="form-group">
-                                    <dev id="custom_config"></dev>
-                                    <p class="form-control-guide"><i class="mdi mdi-information"></i>请参考 <a href="//wiki.sspanel.org/#/setup-custom-config" target="_blank">wiki.sspanel.org/#/setup-custom-config</a> 修改节点自定义配置
-                                    </p>
-                                </div>
-                            </div>
+                        <div class="card-header card-header-light">
+                            <h3 class="card-title">其他信息</h3>
                         </div>
-                    </div>
-                    <div class="card">
-                        <div class="card-main">
-                            <div class="card-inner">
-                                <div class="form-group form-group-label">
-                                    <label class="floating-label" for="sort">节点类型</label>
-                                    <select id="sort" class="form-control maxwidth-edit" name="sort">
-                                        <option value="0">Shadowsocks</option>
-                                        <option value="9">Shadowsocksr 单端口多用户(旧)</option>
-                                        <option value="11">V2Ray</option>
-                                        <option value="14">Trojan</option>
-                                    </select>
-                                </div>
-                                <div class="form-group form-group-label">
-                                    <label class="floating-label" for="status">节点状态</label>
-                                    <input class="form-control maxwidth-edit" id="status" type="text" name="status"
-                                           value="可用">
-                                </div>
-                                
-                                <div class="form-group form-group-label">
-                                    <label class="floating-label" for="info">节点描述</label>
-                                    <input class="form-control maxwidth-edit" id="info" type="text" name="info"
-                                           value="无描述">
+                        <div class="card-body">
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">公有备注</label>
+                                <div class="col">
+                                    <input id="info" type="text" class="form-control" value="">
                                 </div>
-                                <div class="form-group form-group-label">
-                                    <label class="floating-label" for="class">节点等级</label>
-                                    <input class="form-control maxwidth-edit" id="class" type="text" value="0"
-                                           name="class">
-                                    <p class="form-control-guide"><i class="mdi mdi-information"></i>不分级请填0,分级填写相应数字</p>
-                                </div>
-                                <div class="form-group form-group-label">
-                                    <label class="floating-label" for="group">节点群组</label>
-                                    <input class="form-control maxwidth-edit" id="group" type="text" value="0"
-                                           name="group">
-                                    <p class="form-control-guide"><i class="mdi mdi-information"></i>分组为数字,不分组请填0</p>
+                            </div>
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">等级</label>
+                                <div class="col">
+                                    <input id="node_class" type="text" class="form-control" value="">
                                 </div>
-                                <div class="form-group form-group-label">
-                                    <label class="floating-label" for="node_bandwidth_limit">节点流量上限(GB)</label>
-                                    <input class="form-control maxwidth-edit" id="node_bandwidth_limit" type="text"
-                                           value="0" name="node_bandwidth_limit">
-                                    <p class="form-control-guide"><i class="mdi mdi-information"></i>不设上限请填0</p>
+                            </div>
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">组别</label>
+                                <div class="col">
+                                    <input id="node_group" type="text" class="form-control" value="">
                                 </div>
-                                <div class="form-group form-group-label">
-                                    <label class="floating-label" for="bandwidthlimit_resetday">节点流量上限清空日</label>
-                                    <input class="form-control maxwidth-edit" id="bandwidthlimit_resetday" type="text"
-                                           value="1" name="bandwidthlimit_resetday">
+                            </div>
+                            <div class="hr-text">
+                                <span>流量设置</span>
+                            </div>
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">可用流量 (GB)</label>
+                                <div class="col">
+                                    <input id="node_bandwidth_limit" type="text" class="form-control"
+                                        value="">
                                 </div>
-                                <div class="form-group form-group-label">
-                                    <label class="floating-label" for="node_speedlimit">节点限速(Mbps)</label>
-                                    <input class="form-control maxwidth-edit" id="node_speedlimit" type="text" value="0"
-                                           name="node_speedlimit">
-                                    <p class="form-control-guide"><i class="mdi mdi-information"></i>不限速填0,对于每个用户端口生效</p>
+                            </div>
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">流量重置日</label>
+                                <div class="col">
+                                    <input id="bandwidthlimit_resetday" type="text" class="form-control"
+                                        value="">
                                 </div>
                             </div>
-                        </div>
-                    </div>
-                    <div class="card">
-                        <div class="card-main">
-                            <div class="card-inner">
-                                <div class="form-group">
-                                    <div class="row">
-                                        <div class="col-md-10 col-md-push-1">
-                                            <button id="submit" type="submit"
-                                                    class="btn btn-block btn-brand waves-attach waves-light">添加
-                                            </button>
-                                        </div>
-                                    </div>
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">速率限制 (Mbps)</label>
+                                <div class="col">
+                                    <input id="node_speedlimit" type="text" class="form-control"
+                                        value="">
                                 </div>
                             </div>
                         </div>
                     </div>
-                </form>
-                {include file='dialog.tpl'}
+                </div>
+            </div>
         </div>
     </div>
-</main>
-
-{include file='admin/footer.tpl'}
+</div>
 
 <script>
     const container = document.getElementById('custom_config');
@@ -151,65 +174,32 @@
         modes: ['code', 'tree'],
     };
     const editor = new JSONEditor(container, options);
-{literal}
-    $('#main_form').validate({
-        ignore: ".jsoneditor *",
-        rules: {
-            name: {required: true},
-            rate: {required: true},
-            info: {required: true},
-            group: {required: true},
-            status: {required: true},
-            node_speedlimit: {required: true},
-            sort: {required: true},
-            node_bandwidth_limit: {required: true},
-            bandwidthlimit_resetday: {required: true}
-        },
-        submitHandler: () => {
-            if ($$.getElementById('type').checked) {
-                var type = 1;
-            } else {
-                var type = 0;
-            }
-{/literal}
-            $.ajax({
-                type: "POST",
-                url: "/admin/node",
-                dataType: "json",
-                data: {
-                    name: $$getValue('name'),
-                    server: $$getValue('server'),
-                    custom_config: JSON.stringify(editor.get()),
-                    node_ip: $$getValue('node_ip'),
-                    rate: $$getValue('rate'),
-                    info: $$getValue('info'),
-                    type,
-                    group: $$getValue('group'),
-                    status: $$getValue('status'),
-                    node_speedlimit: $$getValue('node_speedlimit'),
-                    sort: $$getValue('sort'),
-                    class: $$getValue('class'),
-                    node_bandwidth_limit: $$getValue('node_bandwidth_limit'),
-                    bandwidthlimit_resetday: $$getValue('bandwidthlimit_resetday'),
-                    mu_only: $$getValue('mu_only')
-                },
-                success: data => {
-                    if (data.ret) {
-                        $("#result").modal();
-                        $$.getElementById('msg').innerHTML = data.msg;
-                        window.setTimeout("location.href=top.document.referrer", {$config['jump_delay']});
-                    } else {
-                        $("#result").modal();
-                        $$.getElementById('msg').innerHTML = data.msg;
-                    }
-                },
-                error: (jqXHR) => {
-                    $("#result").modal();
-                    $$.getElementById('msg').innerHTML = `发生错误:${
-                            jqXHR.status
-                            }`;
+    editor.set({$node->custom_config})
+
+    $("#create-node").click(function() {
+        $.ajax({
+            url: '/node',
+            type: 'POST',
+            dataType: "json",
+            data: {
+                {foreach $update_field as $key}
+                {$key}: $('#{$key}').val(),
+                {/foreach}
+                type: $("#type").is(":checked"),
+                custom_config: JSON.stringify(editor.get()),
+            },
+            success: function(data) {
+                if (data.ret == 1) {
+                    $('#success-message').text(data.msg);
+                    $('#success-dialog').modal('show');
+                    window.setTimeout("location.href=top.document.referrer", {$config['jump_delay']});
+                } else {
+                    $('#fail-message').text(data.msg);
+                    $('#fail-dialog').modal('show');
                 }
-            });
-        }
+            }
+        })
     });
 </script>
+
+{include file='admin/tabler_footer.tpl'}

+ 209 - 229
resources/views/tabler/admin/node/edit.tpl

@@ -1,283 +1,263 @@
-{include file='admin/main.tpl'}
+{include file='admin/tabler_header.tpl'}
 
 <script src="//cdn.jsdelivr.net/npm/[email protected]/dist/jsoneditor.min.js"></script>
 <link href="//cdn.jsdelivr.net/npm/[email protected]/dist/jsoneditor.min.css" rel="stylesheet" type="text/css">
 
-<main class="content">
-    <div class="content-header ui-content-header">
-        <div class="container">
-            <h1 class="content-heading">编辑节点 #{$node->id}</h1>
+<div class="page-wrapper">
+    <div class="container-xl">
+        <div class="page-header d-print-none text-white">
+            <div class="row align-items-center">
+                <div class="col">
+                    <h2 class="page-title">
+                        <span class="home-title">节点 #{$node->id}</span>
+                    </h2>
+                    <div class="page-pretitle my-3">
+                        <span class="home-subtitle">编辑节点信息</span>
+                    </div>
+                </div>
+                <div class="col-auto ms-auto d-print-none">
+                    <div class="btn-list">
+                        <a id="save-node" href="#" class="btn btn-primary d-none d-sm-inline-block">
+                            <i class="icon ti ti-device-floppy"></i>
+                            保存
+                        </a>
+                        <a id="save-node" href="#" class="btn btn-primary d-sm-none btn-icon">
+                            <i class="icon ti ti-device-floppy"></i>
+                        </a>
+                    </div>
+                </div>
+            </div>
         </div>
     </div>
-    <div class="container">
-        <div class="col-lg-12 col-sm-12">
-            <section class="content-inner margin-top-no">
-                <form id="main_form">
+    <div class="page-body">
+        <div class="container-xl">
+            <div class="row row-deck row-cards">
+                <div class="col-md-6 col-sm-12">
                     <div class="card">
-                        <div class="card-main">
-                            <div class="card-inner">
-                                <div class="form-group form-group-label">
-                                    <label class="floating-label" for="name">节点名称</label>
-                                    <input class="form-control maxwidth-edit" id="name" name="name" type="text"
-                                           value="{$node->name}">
+                        <div class="card-header card-header-light">
+                            <h3 class="card-title">基础信息</h3>
+                        </div>
+                        <div class="card-body">
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">名称</label>
+                                <div class="col">
+                                    <input id="name" type="text" class="form-control" value="{$node->name}">
                                 </div>
-                                <div class="form-group form-group-label">
-                                    <label class="floating-label" for="server">节点地址</label>
-                                    <input class="form-control maxwidth-edit" id="server" name="server" type="text"
-                                           value="{$node->server}">
-                                    <p class="form-control-guide"><i class="mdi mdi-information"></i>如果填写为域名,“节点IP”会自动设置为解析的IP</p>
+                            </div>
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">连接地址</label>
+                                <div class="col">
+                                    <textarea id="server" class="col form-control" rows="5">{$node->server}</textarea>
                                 </div>
-                                <div class="form-group form-group-label">
-                                    <label class="floating-label" for="server">节点IP</label>
-                                    <input class="form-control maxwidth-edit" id="node_ip" name="node_ip" type="text"
-                                           value="{$node->node_ip}">
-                                    <p class="form-control-guide"><i class="mdi mdi-information"></i>如果“节点地址”填写为域名,则此处的值会被忽视
-                                    </p>
+                            </div>
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">服务器IP</label>
+                                <div class="col">
+                                    <input id="node_ip" type="text" class="form-control" value="{$node->node_ip}">
                                 </div>
-                                <div class="form-group form-group-label">
-                                    <label class="floating-label" for="rate">流量比例</label>
-                                    <input class="form-control maxwidth-edit" id="rate" name="rate" type="text"
-                                           value="{$node->traffic_rate}">
+                            </div>
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">流量倍率</label>
+                                <div class="col">
+                                    <input id="traffic_rate" type="text" class="form-control"
+                                        value="{$node->traffic_rate}">
                                 </div>
-                                <div class="form-group form-group-label">
-                                    <label for="mu_only">
-                                        <label class="floating-label" for="sort">单端口多用户启用</label>
-                                        <select id="mu_only" class="form-control maxwidth-edit" name="is_multi_user">
-                                            <option value="-1" {if $node->mu_only==-1}selected{/if}>只启用普通端口</option>
-                                            <option value="1" {if $node->mu_only==1}selected{/if}>只启用单端口多用户</option>
-                                        </select>
-                                    </label>
+                            </div>
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">接入类型</label>
+                                <div class="col">
+                                    <select id="sort" class="col form-select">
+                                        <option value="14">Trojan</option>
+                                        <option value="11">V2Ray</option>
+                                        <option value="0">Shadowsocks</option>
+                                        <option value="9">ShadowsocksR 单端口多用户(旧)</option>
+                                    </select>
                                 </div>
-                                <div class="form-group form-group-label">
-                                    <div class="checkbox switch">
-                                        <label for="type">
-                                            <input {if $node->type==1}checked{/if} class="access-hide" id="type"
-                                                   name="type" type="checkbox"><span class="switch-toggle"></span>是否显示
-                                        </label>
-                                    </div>
+                            </div>
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">单端口多用户</label>
+                                <div class="col">
+                                    <select id="mu_only" class="col form-select">
+                                        <option value="-1">只启用普通端口</option>
+                                        <option value="1">只启用单端口多用户</option>
+                                    </select>
                                 </div>
                             </div>
-                        </div>
-                    </div>
-                    <div class="card">
-                        <div class="card-main">
-                            <div class="card-inner">         
-                                <div class="form-group form-group-label">
-                                    <div class="row">
-                                        <div class="col-lg-8 col-sm-6">
-                                            <label class="floating-label" for="password">节点通讯密钥</label>
-                                            <input class="form-control maxwidth-edit" id="password" name="password" type="text"
-                                            value="{$node->password}" disabled>
-                                        </div>
-                                        <div class="col-lg-2 col-sm-3">
-                                            <button type="button" class="btn btn-block btn-brand waves-attach waves-light copy-text" data-clipboard-text="{$node->password}">复制
-                                            </button>
-                                        </div>
-                                        <div class="col-lg-2 col-sm-3">
-                                            <button type="button" class="btn btn-block btn-brand waves-attach waves-light" id="reset_node_password">重置
-                                            </button>
-                                        </div>
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">自定义配置</label>
+                                <dev id="custom_config"></dev>
+                                <label class="form-label col-3 col-form-label">
+                                    请参考 <a href="//wiki.sspanel.org/#/setup-custom-config" target="_blank">wiki.sspanel.org/#/setup-custom-config</a> 修改节点自定义配置
+                                </label>
+                            </div>
+                            <div class="hr-text">
+                                <span>高级选项</span>
+                            </div>
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">节点通讯密钥</label>
+                                <input type="text" class="form-control" id="password" value="{$node->password}" disabled="">
+                                <div class="row">
+                                    <div class="col">
+                                        <button id="reset-node-password" class="btn btn-red">重置</button>
+                                    </div>
+                                    <div class="col">
+                                        <button id="copy-password" class="btn btn-primary copy-text">复制</button>
                                     </div>
-                                    <p class="form-control-guide"><i class="mdi mdi-information"></i>通讯密钥用于 gRPC API 鉴权,如需更改请点击重置
-                                    </p>
                                 </div>
+                                <label class="form-label col-3 col-form-label">
+                                    通讯密钥用于 gRPC API 鉴权,如需更改请点击重置
+                                </label>
                             </div>
-                        </div>
-                    </div>
-                    <div class="card">
-                        <div class="card-main">
-                            <div class="card-inner">         
-                                <div class="form-group">
-                                    <dev id="custom_config"></dev>
-                                    <p class="form-control-guide"><i class="mdi mdi-information"></i>请参考 <a href="//wiki.sspanel.org/#/setup-custom-config" target="_blank">wiki.sspanel.org/#/setup-custom-config</a> 修改节点自定义配置
-                                    </p>
+                            <div class="mb-3">
+                                <div class="divide-y">
+                                    <div>
+                                        <label class="row">
+                                            <span class="col">显示此节点</span>
+                                            <span class="col-auto">
+                                                <label class="form-check form-check-single form-switch">
+                                                    <input id="type" class="form-check-input" type="checkbox"
+                                                        {if $node->type == 1}checked="" {/if}>
+                                                </label>
+                                            </span>
+                                        </label>
+                                    </div>
                                 </div>
                             </div>
                         </div>
                     </div>
+                </div>
+                <div class="col-md-6 col-sm-12">
                     <div class="card">
-                        <div class="card-main">
-                            <div class="card-inner">
-                                <div class="form-group form-group-label">
-                                    <label class="floating-label" for="sort">节点类型</label>
-                                    <select id="sort" class="form-control maxwidth-edit" name="sort">
-                                        <option value="0" {if $node->sort==0}selected{/if}>Shadowsocks</option>
-                                        <option value="9" {if $node->sort==9}selected{/if}>Shadowsocksr 单端口多用户(旧)</option>
-                                        <option value="11" {if $node->sort==11}selected{/if}>V2Ray</option>
-                                        <option value="14" {if $node->sort==14}selected{/if}>Trojan</option>
-                                    </select>
+                        <div class="card-header card-header-light">
+                            <h3 class="card-title">其他信息</h3>
+                        </div>
+                        <div class="card-body">
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">公有备注</label>
+                                <div class="col">
+                                    <input id="info" type="text" class="form-control" value="{$node->info}">
                                 </div>
-                                <div class="form-group form-group-label">
-                                    <label class="floating-label" for="status">节点状态</label>
-                                    <input class="form-control maxwidth-edit" id="status" name="status" type="text"
-                                           value="{$node->status}">
-                                </div> 
-                                <div class="form-group form-group-label">
-                                    <label class="floating-label" for="info">节点描述</label>
-                                    <input class="form-control maxwidth-edit" id="info" name="info" type="text"
-                                           value="{$node->info}">
+                            </div>
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">等级</label>
+                                <div class="col">
+                                    <input id="node_class" type="text" class="form-control" value="{$node->node_class}">
                                 </div>
-                                <div class="form-group form-group-label">
-                                    <label class="floating-label" for="class">节点等级</label>
-                                    <input class="form-control maxwidth-edit" id="class" name="class" type="text"
-                                           value="{$node->node_class}">
-                                    <p class="form-control-guide"><i class="mdi mdi-information"></i>不分级请填0,分级填写相应数字</p>
+                            </div>
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">组别</label>
+                                <div class="col">
+                                    <input id="node_group" type="text" class="form-control" value="{$node->node_group}">
                                 </div>
-                                <div class="form-group form-group-label">
-                                    <label class="floating-label" for="group">节点群组</label>
-                                    <input class="form-control maxwidth-edit" id="group" name="group" type="text"
-                                           value="{$node->node_group}">
-                                    <p class="form-control-guide"><i class="mdi mdi-information"></i>分组为数字,不分组请填0</p>
+                            </div>
+                            <div class="hr-text">
+                                <span>流量设置</span>
+                            </div>
+                            <!-- 避免除0 -->
+                            {if $node->node_bandwidth !== 0 && $node->node_bandwidth_limit !== 0}
+                            <div class="mb-3">
+                                <div class="progress mb-2">
+                                    <div class="progress-bar"
+                                        style="width: {round($node->node_bandwidth / $node->node_bandwidth_limit * 100, 2)}%"
+                                        role="progressbar" aria-valuenow="{round($node->node_bandwidth / $node->node_bandwidth_limit * 100, 2)}"aria-valuemin="0" aria-valuemax="100"
+                                        aria-label="{round($node->node_bandwidth / $node->node_bandwidth_limit * 100, 2)} Complete">
+                                        <span class="visually-hidden">{round($node->node_bandwidth / $node->node_bandwidth_limit * 100, 2)}Complete</span>
+                                    </div>
                                 </div>
-                                <div class="form-group form-group-label">
-                                    <label class="floating-label" for="node_bandwidth_limit">节点流量上限(GB)</label>
-                                    <input class="form-control maxwidth-edit" id="node_bandwidth_limit"
-                                           name="node_bandwidth_limit" type="text"
-                                           value="{$node->node_bandwidth_limit/1024/1024/1024}">
-                                    <p class="form-control-guide"><i class="mdi mdi-information"></i>不设上限请填0</p>
+                            </div>
+                            {/if}
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">已用流量 (GB)</label>
+                                <div class="col">
+                                    <input id="node_bandwidth" type="text" class="form-control"
+                                        value="{round($node->node_bandwidth / 1073741824, 2)}">
                                 </div>
-                                <div class="form-group form-group-label">
-                                    <label class="floating-label" for="bandwidthlimit_resetday">节点流量上限清空日</label>
-                                    <input class="form-control maxwidth-edit" id="bandwidthlimit_resetday"
-                                           name="bandwidthlimit_resetday" type="text"
-                                           value="{$node->bandwidthlimit_resetday}">
+                            </div>
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">可用流量 (GB)</label>
+                                <div class="col">
+                                    <input id="node_bandwidth_limit" type="text" class="form-control"
+                                        value="{round($node->node_bandwidth_limit / 1073741824, 2)}">
                                 </div>
-                                <div class="form-group form-group-label">
-                                    <label class="floating-label" for="node_speedlimit">节点限速(Mbps)</label>
-                                    <input class="form-control maxwidth-edit" id="node_speedlimit"
-                                           name="node_speedlimit" type="text" value="{$node->node_speedlimit}">
-                                    <p class="form-control-guide"><i class="mdi mdi-information"></i>不限速填0,对于每个用户端口生效</p>
+                            </div>
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">流量重置日</label>
+                                <div class="col">
+                                    <input id="bandwidthlimit_resetday" type="text" class="form-control"
+                                        value="{$node->bandwidthlimit_resetday}">
                                 </div>
                             </div>
-                        </div>
-                    </div>
-                    <div class="card">
-                        <div class="card-main">
-                            <div class="card-inner">
-                                <div class="form-group">
-                                    <div class="row">
-                                        <div class="col-md-10 col-md-push-1">
-                                            <button id="submit" type="submit"
-                                                    class="btn btn-block btn-brand waves-attach waves-light">修改
-                                            </button>
-                                        </div>
-                                    </div>
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">速率限制 (Mbps)</label>
+                                <div class="col">
+                                    <input id="node_speedlimit" type="text" class="form-control"
+                                        value="{$node->node_speedlimit}">
                                 </div>
                             </div>
                         </div>
                     </div>
-                </form>
-                {include file='dialog.tpl'}
-            </section>
+                </div>
+            </div>
         </div>
     </div>
-</main>
-
-{include file='admin/footer.tpl'}
+</div>
 
 <script>
     $(function () {
         new ClipboardJS('.copy-text');
     });
     $(".copy-text").click(function () {
-        $("#result").modal();
-        $$.getElementById('msg').innerHTML = '已复制到您的剪贴板。';
+        $('#success-message').text('已复制到您的剪贴板。');
+        $('#success-dialog').modal('show');
     });
-</script>
-<script>
+
     const container = document.getElementById('custom_config');
     var options = {
         modes: ['code', 'tree'],
     };
     const editor = new JSONEditor(container, options);
-    editor.set({$node->custom_config})
-{literal}
-    $('#main_form').validate({
-        ignore: ".jsoneditor *",
-        rules: {
-            name: {required: true},
-            server: {required: true},
-            rate: {required: true},
-            info: {required: true},
-            group: {required: true},
-            status: {required: true},
-            node_speedlimit: {required: true},
-            sort: {required: true},
-            node_bandwidth_limit: {required: true},
-            bandwidthlimit_resetday: {required: true}
-        },
-        submitHandler: () => {
-            if ($$.getElementById('type').checked) {
-                var type = 1;
-            } else {
-                var type = 0;
-            }
-{/literal}
-            $.ajax({
-                type: "PUT",
-                url: "/admin/node/{$node->id}",
-                dataType: "json",
-                data: {
-                    name: $$getValue('name'),
-                    server: $$getValue('server'),
-                    custom_config: JSON.stringify(editor.get()),
-                    node_ip: $$getValue('node_ip'),
-                    rate: $$getValue('rate'),
-                    info: $$getValue('info'),
-                    type,
-                    group: $$getValue('group'),
-                    status: $$getValue('status'),
-                    sort: $$getValue('sort'),
-                    node_speedlimit: $$getValue('node_speedlimit'),
-                    class: $$getValue('class'),
-                    node_bandwidth_limit: $$getValue('node_bandwidth_limit'),
-                    bandwidthlimit_resetday: $$getValue('bandwidthlimit_resetday'),
-                    mu_only: $$getValue('mu_only')
-                },
-                success: (data) => {
-                    if (data.ret) {
-                        $("#result").modal();
-                        $$.getElementById('msg').innerHTML = data.msg;
-                        window.setTimeout("location.href=top.document.referrer", {$config['jump_delay']});
 
-                    } else {
-                        $("#result").modal();
-                        $$.getElementById('msg').innerHTML = data.msg;
-                    }
-                },
-                error: (jqXHR) => {
-                    $("#result").modal();
-                    $$.getElementById('msg').innerHTML = `发生错误:${
-                        jqXHR.status
-                        }`;
+    $("#reset-node-password").click(function() {
+        $.ajax({
+            url: '/admin/node/{$node->id}/password_reset',
+            type: 'POST',
+            dataType: "json",
+            success: function(data) {
+                if (data.ret == 1) {
+                    $('#success-message').text(data.msg);
+                    $('#success-dialog').modal('show');
+                } else {
+                    $('#fail-message').text(data.msg);
+                    $('#fail-dialog').modal('show');
                 }
-            });
-        }
+            }
+        })
     });
-</script>
-<script>
-    $(document).ready(function () {
-        $("#reset_node_password").click(function () {
-            $.ajax({
-                type: "POST",
-                url: "/admin/node/{$node->id}/password_reset",
-                dataType: "json",
-                data: {},
-                success: (data) => {
-                    if (data.ret) {
-                        $("#result").modal();
-                        $$.getElementById('msg').innerHTML = data.msg;
-                        window.setTimeout("location.href='/admin/node/{$node->id}/edit'", {$config['jump_delay']});
-                    } else {
-                        $("#result").modal();
-                        $$.getElementById('msg').innerHTML = data.msg;
-                    }
-                },
-                error: (jqXHR) => {
-                    $("#result").modal();
-                    $$.getElementById('msg').innerHTML = data.msg;
+
+    $("#save-node").click(function() {
+        $.ajax({
+            url: '/admin/node/{$node->id}',
+            type: 'PUT',
+            dataType: "json",
+            data: {
+                {foreach $update_field as $key}
+                {$key}: $('#{$key}').val(),
+                {/foreach}
+                type: $("#type").is(":checked"),
+                custom_config: JSON.stringify(editor.get()),
+            },
+            success: function(data) {
+                if (data.ret == 1) {
+                    $('#success-message').text(data.msg);
+                    $('#success-dialog').modal('show');
+                    window.setTimeout("location.href=top.document.referrer", {$config['jump_delay']});
+                } else {
+                    $('#fail-message').text(data.msg);
+                    $('#fail-dialog').modal('show');
                 }
-            })
+            }
         })
-    })
+    });
 </script>
+
+{include file='admin/tabler_footer.tpl'}

+ 146 - 81
resources/views/tabler/admin/node/index.tpl

@@ -1,97 +1,162 @@
-{include file='admin/main.tpl'}
+{include file='admin/tabler_header.tpl'}
 
-<main class="content">
-    <div class="content-header ui-content-header">
-        <div class="container">
-            <h1 class="content-heading">节点列表</h1>
-        </div>
-    </div>
-    <div class="container">
-        <div class="col-lg-12 col-sm-12">
-            <section class="content-inner margin-top-no">
-                <div class="card">
-                    <div class="card-main">
-                        <div class="card-inner">
-                            <p>系统中所有节点的列表。</p>
-                            <p>显示表项:
-                                {include file='table/checkbox.tpl'}
-                            </p>
-                        </div>
+<div class="page-wrapper">
+    <div class="container-xl">
+        <div class="page-header d-print-none text-white">
+            <div class="row align-items-center">
+                <div class="col">
+                    <h2 class="page-title">
+                        <span class="home-title">节点列表</span>
+                    </h2>
+                    <div class="page-pretitle my-3">
+                        <span class="home-subtitle">
+                            系统中所有节点的列表
+                        </span>
                     </div>
                 </div>
-                <div class="table-responsive">
-                    {include file='table/table.tpl'}
-                </div>
-                <div class="fbtn-container">
-                    <div class="fbtn-inner">
-                        <a class="fbtn fbtn-lg fbtn-brand-accent waves-attach waves-circle waves-light"
-                           href="/admin/node/create">+</a>
+                <div class="col-auto ms-auto d-print-none">
+                    <div class="btn-list">
+                        <a href="/admin/node/create" class="btn btn-primary d-none d-sm-inline-block" data-bs-toggle="modal"
+                            data-bs-target="#create-dialog">
+                            <i class="icon ti ti-plus"></i>
+                            创建
+                        </a>
+                        <a href="#" class="btn btn-primary d-sm-none btn-icon" data-bs-toggle="modal"
+                            data-bs-target="#create-dialog">
+                            <i class="icon ti ti-plus"></i>
+                        </a>
                     </div>
                 </div>
-                <div aria-hidden="true" class="modal modal-va-middle fade" id="delete_modal" role="dialog"
-                     tabindex="-1">
-                    <div class="modal-dialog modal-xs">
-                        <div class="modal-content">
-                            <div class="modal-heading">
-                                <a class="modal-close" data-dismiss="modal">×</a>
-                                <h2 class="modal-title">确认要删除?</h2>
-                            </div>
-                            <div class="modal-inner">
-                                <p>请您确认。</p>
-                            </div>
-                            <div class="modal-footer">
-                                <p class="text-right">
-                                    <button class="btn btn-flat btn-brand-accent waves-attach waves-effect"
-                                            data-dismiss="modal" type="button">取消
-                                    </button>
-                                    <button class="btn btn-flat btn-brand-accent waves-attach" data-dismiss="modal"
-                                            id="delete_input" type="button">确定
-                                    </button>
-                                </p>
-                            </div>
+            </div>
+        </div>
+    </div>
+    <div class="page-body">
+        <div class="container-xl">
+            <div class="row row-deck row-cards">
+                <div class="col-12">
+                    <div class="card">
+                        <div class="table-responsive">
+                            <table id="data_table" class="table card-table table-vcenter text-nowrap datatable">
+                                <thead>
+                                    <tr>
+                                        {foreach $details['field'] as $key => $value}
+                                            <th>{$value}</th>
+                                        {/foreach}
+                                    </tr>
+                                </thead>
+                            </table>
                         </div>
                     </div>
                 </div>
-                {include file='dialog.tpl'}
-            </section>
+            </div>
         </div>
     </div>
-</main>
-
-{include file='admin/footer.tpl'}
 
-<script>
-    function delete_modal_show(id) {
-        deleteid = id;
-        $("#delete_modal").modal();
-    }
-    {include file='table/js_1.tpl'}
-    window.addEventListener('load', () => {
-        {include file='table/js_2.tpl'}
-        function delete_id() {
-            $.ajax({
-                type: "DELETE",
-                url: "/admin/node",
-                dataType: "json",
-                data: {
-                    id: deleteid
+    <script>
+        var table = $('#data_table').DataTable({
+            $('#data_table').DataTable({
+                ajax: {
+                    url: '/admin/node/ajax',
+                    type: 'POST',
+                    dataSrc: 'nodes'
                 },
-                success: data => {
-                    if (data.ret) {
-                        $("#result").modal();
-                        $$.getElementById('msg').innerHTML = data.msg;
-                        {include file='table/js_delete.tpl'}
-                    } else {
-                        $("#result").modal();
-                        $$.getElementById('msg').innerHTML = data.msg;
+                "autoWidth":false,
+                'iDisplayLength': 25,
+                'scrollX': true,
+                'order': [
+                    [1, 'desc']
+                ],
+                columns: [
+                    {foreach $details['field'] as $key => $value}
+                    { data: '{$key}' },
+                    {/foreach}
+                ],
+                "columnDefs":[
+                    { targets:[0],orderable:false },
+                ],
+                "dom": "<'row px-3 py-3'<'col-sm-12 col-md-6'l><'col-sm-12 col-md-6'f>>" +
+                    "<'row'<'col-sm-12'tr>>" +
+                    "<'row card-footer d-flex align-items-center'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
+                language: {
+                    "sProcessing": "处理中...",
+                    "sLengthMenu": "显示 _MENU_ 条",
+                    "sZeroRecords": "没有匹配结果",
+                    "sInfo": "第 _START_ 至 _END_ 项结果,共 _TOTAL_ 项",
+                    "sInfoEmpty": "第 0 至 0 项结果,共 0 项",
+                    "sInfoFiltered": "(在 _MAX_ 项中查找)",
+                    "sInfoPostFix": "",
+                    "sSearch": "<i class=\"ti ti-search\"></i> ",
+                    "sUrl": "",
+                    "sEmptyTable": "表中数据为空",
+                    "sLoadingRecords": "载入中...",
+                    "sInfoThousands": ",",
+                    "oPaginate": {
+                        "sFirst": "首页",
+                        "sPrevious": "<i class=\"ti ti-arrow-left\"></i>",
+                        "sNext": "<i class=\"ti ti-arrow-right\"></i>",
+                        "sLast": "末页"
+                    },
+                    "oAria": {
+                        "sSortAscending": ": 以升序排列此列",
+                        "sSortDescending": ": 以降序排列此列"
                     }
-                },
-                error: jqXHR => {
-                    $("#result").modal();
-                    $$.getElementById('msg').innerHTML = `${ldelim}data.msg{rdelim} 发生错误了。`;
                 }
             });
+        });
+
+        function loadTable() {
+            table;
         }
-        $$.getElementById('delete_input').addEventListener('click', delete_id);
-    })
-</script>
+
+        function deleteNode(node_id) {
+            $('#notice-message').text('确定删除此节点?');
+            $('#notice-dialog').modal('show');
+            $('#notice-confirm').on('click', function() {
+                $.ajax({
+                    url: "/admin/node/" + node_id,
+                    type: 'DELETE',
+                    dataType: "json",
+                    success: function(data) {
+                        if (data.ret == 1) {
+                            $('#success-message').text(data.msg);
+                            $('#success-dialog').modal('show');
+                            reloadTableAjax();
+                        } else {
+                            $('#fail-message').text(data.msg);
+                            $('#fail-dialog').modal('show');
+                        }
+                    }
+                })
+            });
+        };
+
+        function copyNode(node_id) {
+            $('#notice-message').text('确定复制此节点?');
+            $('#notice-dialog').modal('show');
+            $('#notice-confirm').on('click', function() {
+                $.ajax({
+                    url: "/admin/node/" + node_id + "/copy",
+                    type: 'POST',
+                    dataType: "json",
+                    success: function(data) {
+                        if (data.ret == 1) {
+                            $('#success-message').text(data.msg);
+                            $('#success-dialog').modal('show');
+                            reloadTableAjax();
+                        } else {
+                            $('#fail-message').text(data.msg);
+                            $('#fail-dialog').modal('show');
+                        }
+                    }
+                })
+            });
+        };
+
+        function reloadTableAjax() {
+            table.ajax.reload(null, false);
+        }
+
+        loadTable();
+    </script>
+
+{include file='admin/tabler_footer.tpl'}

+ 2 - 2
resources/views/tabler/admin/ticket/index.tpl

@@ -99,7 +99,7 @@
         }
 
         function closeTicket(ticket_id) {
-            $('#notice-message').text('确定关闭此工单');
+            $('#notice-message').text('确定关闭此工单');
             $('#notice-dialog').modal('show');
             $('#notice-confirm').on('click', function () {
                 $.ajax({
@@ -121,7 +121,7 @@
         };
 
         function deleteTicket(ticket_id) {
-            $('#notice-message').text('确定删除此工单');
+            $('#notice-message').text('确定删除此工单');
             $('#notice-dialog').modal('show');
             $('#notice-confirm').on('click', function() {
                 $.ajax({

+ 1 - 1
resources/views/tabler/admin/user/edit.tpl

@@ -319,7 +319,7 @@
             dataType: "json",
             data: {
                 {foreach $update_field as $key}
-                    {$key}: $('#{$key}').val(),
+                {$key}: $('#{$key}').val(),
                 {/foreach}
                 is_admin: $("#is_admin").is(":checked"),
                 is_banned: $("#enable").is(":checked"),

+ 1 - 1
resources/views/tabler/admin/user/index.tpl

@@ -189,7 +189,7 @@
         });
 
         function deleteUser(user_id) {
-            $('#notice-message').text('确定删除此用户');
+            $('#notice-message').text('确定删除此用户');
             $('#notice-dialog').modal('show');
             $('#notice-confirm').on('click', function() {
                 $.ajax({

+ 83 - 76
src/Controllers/Admin/NodeController.php

@@ -17,6 +17,38 @@ use Slim\Http\Response;
 
 final class NodeController extends BaseController
 {
+    public static $details = [
+        'field' => [
+            'op' => '操作',
+            'id' => '节点ID',
+            'name' => '名称',
+            'server' => '地址',
+            'sort' => '类型',
+            'traffic_rate' => '倍率',
+            'node_class' => '等级',
+            'node_group' => '组别',
+            'node_bandwidth_limit' => '流量限制',
+            'node_bandwidth' => '已用流量',
+            'bandwidthlimit_resetday' => '重置日',
+        ],
+    ];
+
+    public static $update_field = [
+        'name',
+        'server',
+        'mu_only',
+        'traffic_rate',
+        'info',
+        'node_group',
+        'node_speedlimit',
+        'status',
+        'sort',
+        'node_ip',
+        'node_class',
+        'node_bandwidth_limit',
+        'bandwidthlimit_resetday',
+    ];
+
     /**
      * 后台节点页面
      *
@@ -24,34 +56,9 @@ final class NodeController extends BaseController
      */
     public function index(Request $request, Response $response, array $args): ResponseInterface
     {
-        $table_config = [];
-        $table_config['total_column'] = [
-            'op' => '操作',
-            'id' => 'ID',
-            'name' => '节点名称',
-            'type' => '显示与隐藏',
-            'sort' => '类型',
-            'server' => '节点地址',
-            'outaddress' => '出口地址',
-            'node_ip' => '节点IP',
-            'info' => '节点信息',
-            'status' => '状态',
-            'traffic_rate' => '流量比率',
-            'node_group' => '节点群组',
-            'node_class' => '节点等级',
-            'node_speedlimit' => '节点限速/Mbps',
-            'node_bandwidth' => '已走流量/GB',
-            'node_bandwidth_limit' => '流量限制/GB',
-            'bandwidthlimit_resetday' => '流量重置日',
-            'node_heartbeat' => '上一次活跃时间',
-            'mu_only' => '只启用单端口多用户',
-        ];
-        $table_config['default_show_column'] = ['op', 'id', 'name', 'sort'];
-        $table_config['ajax_url'] = 'node/ajax';
-
         return $response->write(
             $this->view()
-                ->assign('table_config', $table_config)
+                ->assign('details', self::$details)
                 ->display('admin/node/index.tpl')
         );
     }
@@ -65,6 +72,7 @@ final class NodeController extends BaseController
     {
         return $response->write(
             $this->view()
+                ->assign('update_field', self::$update_field)
                 ->display('admin/node/create.tpl')
         );
     }
@@ -80,10 +88,10 @@ final class NodeController extends BaseController
         $node->name = $request->getParam('name');
         $node->server = trim($request->getParam('server'));
         $node->mu_only = $request->getParam('mu_only');
-        $node->traffic_rate = $request->getParam('rate');
+        $node->traffic_rate = $request->getParam('traffic_rate');
         $node->info = $request->getParam('info');
-        $node->type = $request->getParam('type');
-        $node->node_group = $request->getParam('group');
+        $node->type = ($request->getParam('type') === 'true') ? 1 : 0;
+        $node->node_group = $request->getParam('node_group');
         $node->node_speedlimit = $request->getParam('node_speedlimit');
         $node->status = $request->getParam('status');
         $node->sort = $request->getParam('sort');
@@ -111,7 +119,7 @@ final class NodeController extends BaseController
             ]);
         }
 
-        $node->node_class = $request->getParam('class');
+        $node->node_class = $request->getParam('node_class');
         $node->node_bandwidth_limit = $request->getParam('node_bandwidth_limit') * 1024 * 1024 * 1024;
         $node->bandwidthlimit_resetday = $request->getParam('bandwidthlimit_resetday');
         $node->password = Tools::genRandomChar(32);
@@ -162,6 +170,7 @@ final class NodeController extends BaseController
         return $response->write(
             $this->view()
                 ->assign('node', $node)
+                ->assign('update_field', self::$update_field)
                 ->display('admin/node/edit.tpl')
         );
     }
@@ -182,7 +191,7 @@ final class NodeController extends BaseController
         $node->traffic_rate = $request->getParam('rate');
         $node->info = $request->getParam('info');
         $node->node_speedlimit = $request->getParam('node_speedlimit');
-        $node->type = $request->getParam('type');
+        $node->type = ($request->getParam('type') === 'true') ? 1 : 0;
         $node->sort = $request->getParam('sort');
 
         if ($request->getParam('custom_config') !== null) {
@@ -298,6 +307,34 @@ final class NodeController extends BaseController
         ]);
     }
 
+    public function copy($request, $response, $args)
+    {
+        try {
+            $old_node_id = $request->getParam('id');
+            $old_node = Node::find($old_node_id);
+            $new_node = new Node();
+            // https://laravel.com/docs/9.x/eloquent#replicating-models
+            $new_node = $old_node->replicate([
+                'node_bandwidth',
+                'bandwidthlimit_resetday',
+            ]);
+            $new_node->name .= ' (副本)';
+            $new_node->node_bandwidth = 0;
+            $new_node->bandwidthlimit_resetday = date('d');
+            $new_node->save();
+        } catch (\Exception $e) {
+            return $response->withJson([
+                'ret' => 0,
+                'msg' => $e->getMessage(),
+            ]);
+        }
+
+        return $response->withJson([
+            'ret' => 1,
+            'msg' => '复制成功',
+        ]);
+    }
+
     /**
      * 后台节点页面 AJAX
      *
@@ -305,54 +342,24 @@ final class NodeController extends BaseController
      */
     public function ajax(Request $request, Response $response, array $args): ResponseInterface
     {
-        $query = Node::getTableDataFromAdmin(
-            $request,
-            static function (&$order_field): void {
-                if (\in_array($order_field, ['op'])) {
-                    $order_field = 'id';
-                }
-                if (\in_array($order_field, ['outaddress'])) {
-                    $order_field = 'server';
-                }
-            }
-        );
-
-        $data = [];
-        foreach ($query['datas'] as $value) {
-            /** @var Node $value */
-            $tempdata = [
-                'op' => <<<EOF
-                    <a class="btn btn-brand" href="/admin/node/{$value->id}/edit">编辑</a>
-                    <a class="btn btn-brand-accent" id="delete" value="{$value->id}" href="javascript:void(0);" onClick="delete_modal_show('{$value->id}')">删除</a>
-                EOF,
-                'id' => $value->id,
-                'name' => $value->name,
-                'type' => $value->type(),
-                'sort' => $value->sort(),
-                'server' => $value->server,
-                'outaddress' => $value->getOutAddress(),
-                'node_ip' => $value->node_ip,
-                'info' => $value->info,
-                'status' => $value->status,
-                'traffic_rate' => $value->traffic_rate,
-                'node_group' => $value->node_group,
-                'node_class' => $value->node_class,
-                'node_speedlimit' => $value->node_speedlimit,
-                'node_bandwidth' => Tools::flowToGB($value->node_bandwidth),
-                'node_bandwidth_limit' => Tools::flowToGB($value->node_bandwidth_limit),
-                'bandwidthlimit_resetday' => $value->bandwidthlimit_resetday,
-                'node_heartbeat' => $value->nodeHeartbeat(),
-                'mu_only' => $value->muOnly(),
-            ];
-
-            $data[] = $tempdata;
+        $nodes = Node::orderBy('id', 'desc')->get();
+
+        foreach ($nodes as $node) {
+            $node->op = '<button type="button" class="btn btn-red" id="delete-node" 
+            onclick="deleteNode(' . $node->id . ')">删除</button><button type="button" class="btn btn-orange" id="copy-node" 
+            onclick="copyNode(' . $node->id . ')">复制</button>
+            <a class="btn btn-blue" href="/admin/node/' . $node->id . '/edit">编辑</a>';
+            $node->type = $node->type();
+            $node->sort = $node->sort();
+            $node->outaddress = $node->getOutAddress();
+            $node->node_bandwidth = Tools::flowToGB($node->node_bandwidth);
+            $node->node_bandwidth_limit = Tools::flowToGB($node->node_bandwidth_limit);
+            $node->node_heartbeat = $node->nodeHeartbeat();
+            $node->mu_only = $node->muOnly();
         }
 
         return $response->withJson([
-            'draw' => $request->getParam('draw'),
-            'recordsTotal' => Node::count(),
-            'recordsFiltered' => $query['count'],
-            'data' => $data,
+            'nodes' => $nodes,
         ]);
     }
 }

+ 1 - 1
src/Controllers/Admin/TicketController.php

@@ -18,7 +18,7 @@ final class TicketController extends BaseController
     [
         'field' => [
             'op' => '操作',
-            'id' => 'ID',
+            'id' => '工单ID',
             'title' => '主题',
             'status' => '工单状态',
             'type' => '工单类型',

+ 1 - 1
src/Controllers/Node/FuncController.php

@@ -60,7 +60,7 @@ final class FuncController extends BaseController
             'data' => [],
         ]);
     }
-    
+
     /**
      * @param array     $args
      */