mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-05 05:36:07 +08:00
1121 lines
37 KiB
HTML
1121 lines
37 KiB
HTML
<style>
|
||
.clicd-fw-panel {
|
||
font-size: 14px;
|
||
color: #1f2937
|
||
}
|
||
|
||
.clicd-fw-section {
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 6px;
|
||
background: #fff;
|
||
padding: 12px;
|
||
margin-top: 8px
|
||
}
|
||
|
||
.clicd-fw-title {
|
||
font-weight: 600;
|
||
margin: 18px 0 8px
|
||
}
|
||
|
||
.clicd-fw-title-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
margin: 18px 0 8px
|
||
}
|
||
|
||
.clicd-fw-title-row .clicd-fw-title {
|
||
margin: 0
|
||
}
|
||
|
||
.clicd-fw-muted {
|
||
color: #6b7280
|
||
}
|
||
|
||
.clicd-fw-settings {
|
||
margin-bottom: 22px
|
||
}
|
||
|
||
.clicd-fw-settings-head {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
margin-bottom: 26px
|
||
}
|
||
|
||
.clicd-fw-settings-title {
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
color: #111827;
|
||
line-height: 1.35
|
||
}
|
||
|
||
.clicd-fw-settings-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-top: 12px
|
||
}
|
||
|
||
.clicd-fw-save-hint {
|
||
color: #6b7280;
|
||
font-size: 13px
|
||
}
|
||
|
||
.clicd-fw-toggle {
|
||
position: relative;
|
||
display: inline-flex;
|
||
width: 56px;
|
||
height: 30px;
|
||
flex: 0 0 auto;
|
||
cursor: pointer
|
||
}
|
||
|
||
.clicd-fw-toggle input {
|
||
opacity: 0;
|
||
width: 0;
|
||
height: 0
|
||
}
|
||
|
||
.clicd-fw-toggle-slider {
|
||
position: absolute;
|
||
inset: 0;
|
||
background: #cfd5df;
|
||
border-radius: 999px;
|
||
transition: .25s
|
||
}
|
||
|
||
.clicd-fw-toggle-slider:before {
|
||
content: "";
|
||
position: absolute;
|
||
width: 22px;
|
||
height: 22px;
|
||
border-radius: 50%;
|
||
background: #fff;
|
||
top: 4px;
|
||
left: 4px;
|
||
transition: .25s;
|
||
box-shadow: 0 1px 3px rgba(15, 23, 42, .18)
|
||
}
|
||
|
||
.clicd-fw-toggle input:checked+.clicd-fw-toggle-slider {
|
||
background: #10b981
|
||
}
|
||
|
||
.clicd-fw-toggle input:checked+.clicd-fw-toggle-slider:before {
|
||
transform: translateX(26px)
|
||
}
|
||
|
||
.clicd-fw-toggle-sm {
|
||
width: 36px;
|
||
height: 20px
|
||
}
|
||
|
||
.clicd-fw-toggle-sm .clicd-fw-toggle-slider:before {
|
||
width: 16px;
|
||
height: 16px;
|
||
top: 2px;
|
||
left: 2px
|
||
}
|
||
|
||
.clicd-fw-toggle-sm input:checked+.clicd-fw-toggle-slider:before {
|
||
transform: translateX(16px)
|
||
}
|
||
|
||
.clicd-fw-status {
|
||
font-size: 13px;
|
||
color: #6b7280;
|
||
line-height: 1.5
|
||
}
|
||
|
||
.clicd-fw-field label {
|
||
display: block;
|
||
color: #6b7280;
|
||
font-size: 12px;
|
||
margin-bottom: 4px
|
||
}
|
||
|
||
.clicd-fw-input,
|
||
.clicd-fw-select {
|
||
width: 100%;
|
||
height: 34px;
|
||
border: 1px solid #d1d5db;
|
||
border-radius: 4px;
|
||
padding: 6px 8px;
|
||
box-sizing: border-box
|
||
}
|
||
|
||
.clicd-fw-input:focus,
|
||
.clicd-fw-select:focus {
|
||
border-color: #2563eb;
|
||
outline: none
|
||
}
|
||
|
||
.clicd-fw-field-help {
|
||
color: #9ca3af;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
margin-top: 6px
|
||
}
|
||
|
||
.clicd-fw-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
align-items: end
|
||
}
|
||
|
||
.clicd-fw-btn {
|
||
height: 34px;
|
||
border: 1px solid #2563eb;
|
||
background: #2563eb;
|
||
color: #fff;
|
||
border-radius: 4px;
|
||
padding: 0 12px;
|
||
cursor: pointer;
|
||
font-size: 13px
|
||
}
|
||
|
||
.clicd-fw-btn[disabled] {
|
||
opacity: .6;
|
||
cursor: not-allowed
|
||
}
|
||
|
||
.clicd-fw-btn-secondary {
|
||
border-color: #d1d5db;
|
||
background: #fff;
|
||
color: #374151
|
||
}
|
||
|
||
.clicd-fw-btn-danger {
|
||
border-color: #dc2626;
|
||
background: #dc2626;
|
||
color: #fff
|
||
}
|
||
|
||
.clicd-fw-btn-dark {
|
||
border-color: #000;
|
||
background: #000;
|
||
color: #fff
|
||
}
|
||
|
||
.clicd-fw-btn-sm {
|
||
height: 30px;
|
||
padding: 0 10px;
|
||
font-size: 12px
|
||
}
|
||
|
||
.clicd-fw-rules {
|
||
margin-top: 8px;
|
||
overflow-x: auto;
|
||
background: #fff
|
||
}
|
||
|
||
.clicd-fw-table {
|
||
width: 100%;
|
||
min-width: 920px;
|
||
border-collapse: collapse;
|
||
table-layout: fixed
|
||
}
|
||
|
||
.clicd-fw-table th,
|
||
.clicd-fw-table td {
|
||
padding: 13px 14px;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
text-align: left;
|
||
vertical-align: middle;
|
||
color: #111827;
|
||
font-size: 14px;
|
||
white-space: nowrap
|
||
}
|
||
|
||
.clicd-fw-table th {
|
||
background: #f8fafc;
|
||
color: #6b7280;
|
||
font-weight: 600
|
||
}
|
||
|
||
.clicd-fw-table tbody tr:hover {
|
||
background: #f9fafb
|
||
}
|
||
|
||
.clicd-fw-table td:nth-child(8) {
|
||
white-space: normal;
|
||
word-break: break-word
|
||
}
|
||
|
||
.clicd-fw-table th:last-child,
|
||
.clicd-fw-table td:last-child {
|
||
text-align: right
|
||
}
|
||
|
||
.clicd-fw-rule-direction {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
font-weight: 600
|
||
}
|
||
|
||
.clicd-fw-direction-in {
|
||
background: #dbeafe;
|
||
color: #1d4ed8
|
||
}
|
||
|
||
.clicd-fw-direction-out {
|
||
background: #fef3c7;
|
||
color: #92400e
|
||
}
|
||
|
||
.clicd-fw-rule-action {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
font-weight: 600
|
||
}
|
||
|
||
.clicd-fw-action-ACCEPT {
|
||
background: #d1fae5;
|
||
color: #065f46
|
||
}
|
||
|
||
.clicd-fw-action-DROP {
|
||
background: #fee2e2;
|
||
color: #991b1b
|
||
}
|
||
|
||
.clicd-fw-rule-network {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 3px 8px;
|
||
border-radius: 4px;
|
||
background: #f3f4f6;
|
||
color: #374151;
|
||
font-size: 12px;
|
||
font-weight: 600
|
||
}
|
||
|
||
.clicd-fw-rule-ops {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
justify-content: flex-end
|
||
}
|
||
|
||
.clicd-fw-icon-btn {
|
||
width: 30px;
|
||
height: 30px;
|
||
border: 0;
|
||
border-radius: 4px;
|
||
background: transparent;
|
||
color: #9ca3af;
|
||
cursor: pointer;
|
||
font-size: 17px;
|
||
line-height: 30px;
|
||
padding: 0;
|
||
text-align: center
|
||
}
|
||
|
||
.clicd-fw-icon-btn:hover {
|
||
background: #f3f4f6;
|
||
color: #374151
|
||
}
|
||
|
||
.clicd-fw-message {
|
||
border: 1px solid #bfdbfe;
|
||
background: #eff6ff;
|
||
color: #1d4ed8;
|
||
border-radius: 6px;
|
||
padding: 10px 12px;
|
||
margin-bottom: 12px;
|
||
display: none
|
||
}
|
||
|
||
.clicd-fw-message.error {
|
||
border-color: #fecaca;
|
||
background: #fef2f2;
|
||
color: #b91c1c
|
||
}
|
||
|
||
.clicd-fw-debug {
|
||
margin-top: 12px;
|
||
border: 1px dashed #d1d5db;
|
||
border-radius: 6px;
|
||
background: #f9fafb;
|
||
padding: 10px;
|
||
color: #374151;
|
||
white-space: pre-wrap;
|
||
font-size: 12px;
|
||
display: none
|
||
}
|
||
|
||
.clicd-fw-modal-mask {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(15, 23, 42, .42);
|
||
display: none;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 9999;
|
||
padding: 16px
|
||
}
|
||
|
||
.clicd-fw-modal {
|
||
width: min(420px, 100%);
|
||
background: #fff;
|
||
border-radius: 6px;
|
||
border: 1px solid #e5e7eb;
|
||
box-shadow: 0 18px 48px rgba(15, 23, 42, .22);
|
||
padding: 16px
|
||
}
|
||
|
||
.clicd-fw-rule-modal {
|
||
width: min(560px, 100%);
|
||
max-height: calc(100vh - 48px);
|
||
padding: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden
|
||
}
|
||
|
||
.clicd-fw-modal-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
padding: 20px 26px;
|
||
border-bottom: 1px solid #e5e7eb
|
||
}
|
||
|
||
.clicd-fw-modal-close {
|
||
width: 28px;
|
||
height: 28px;
|
||
border: 0;
|
||
background: transparent;
|
||
color: #6b7280;
|
||
cursor: pointer;
|
||
font-size: 28px;
|
||
line-height: 24px;
|
||
padding: 0
|
||
}
|
||
|
||
.clicd-fw-rule-modal-body {
|
||
padding: 24px 26px 8px;
|
||
overflow-y: auto
|
||
}
|
||
|
||
.clicd-fw-rule-modal-body .clicd-fw-field {
|
||
margin-bottom: 16px
|
||
}
|
||
|
||
.clicd-fw-rule-modal-body .clicd-fw-input,
|
||
.clicd-fw-rule-modal-body .clicd-fw-select {
|
||
height: 48px;
|
||
border-radius: 6px;
|
||
padding: 0 14px;
|
||
font-size: 15px
|
||
}
|
||
|
||
.clicd-fw-rule-modal-body .clicd-fw-field label {
|
||
color: #4b5563;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
margin-bottom: 8px
|
||
}
|
||
|
||
.clicd-fw-modal-title {
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
color: #111827;
|
||
margin-bottom: 8px
|
||
}
|
||
|
||
.clicd-fw-modal-header .clicd-fw-modal-title {
|
||
margin-bottom: 0
|
||
}
|
||
|
||
.clicd-fw-modal-body {
|
||
font-size: 14px;
|
||
color: #4b5563;
|
||
line-height: 1.6;
|
||
margin-bottom: 14px
|
||
}
|
||
|
||
.clicd-fw-modal-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 12px
|
||
}
|
||
|
||
.clicd-fw-rule-modal .clicd-fw-modal-actions {
|
||
padding: 6px 26px 24px
|
||
}
|
||
|
||
.clicd-fw-rule-modal .clicd-fw-modal-actions .clicd-fw-btn {
|
||
height: 44px;
|
||
min-width: 76px;
|
||
font-size: 14px
|
||
}
|
||
|
||
.clicd-fw-default-action-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 6px;
|
||
background: #fff;
|
||
padding: 12px 16px
|
||
}
|
||
|
||
.clicd-fw-default-title {
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
color: #111827;
|
||
line-height: 1.35
|
||
}
|
||
|
||
.clicd-fw-default-desc {
|
||
color: #6b7280;
|
||
font-size: 13px;
|
||
line-height: 1.5
|
||
}
|
||
|
||
.clicd-fw-default-action-row select {
|
||
width: auto;
|
||
min-width: 126px;
|
||
height: 38px;
|
||
border: 1px solid #d1d5db;
|
||
border-radius: 4px;
|
||
padding: 0 12px;
|
||
font-size: 14px;
|
||
color: #111827;
|
||
background: #fff
|
||
}
|
||
|
||
.clicd-fw-network-note {
|
||
margin-top: 14px;
|
||
border: 1px solid #bfdbfe;
|
||
border-radius: 6px;
|
||
background: #eff6ff;
|
||
color: #1d4ed8;
|
||
padding: 12px 16px;
|
||
line-height: 1.6
|
||
}
|
||
|
||
.clicd-fw-network-note-title {
|
||
font-weight: 700;
|
||
margin-bottom: 2px
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
.clicd-fw-default-action-row,
|
||
.clicd-fw-settings-actions {
|
||
align-items: stretch;
|
||
flex-direction: column
|
||
}
|
||
|
||
.clicd-fw-default-action-row select,
|
||
.clicd-fw-settings-actions .clicd-fw-btn {
|
||
width: 100%
|
||
}
|
||
|
||
.clicd-fw-title-row {
|
||
align-items: stretch;
|
||
flex-direction: column
|
||
}
|
||
|
||
.clicd-fw-title-row .clicd-fw-btn {
|
||
width: 100%
|
||
}
|
||
|
||
.clicd-fw-modal-header,
|
||
.clicd-fw-rule-modal-body,
|
||
.clicd-fw-rule-modal .clicd-fw-modal-actions {
|
||
padding-left: 18px;
|
||
padding-right: 18px
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<div class="clicd-fw-panel" id="clicd-fw-panel" data-service-id="{$service_id}" data-area-key="{$area_key}">
|
||
<div class="clicd-fw-message" id="clicd-fw-message"></div>
|
||
|
||
<div class="clicd-fw-settings">
|
||
<div class="clicd-fw-settings-head">
|
||
<div>
|
||
<div class="clicd-fw-settings-title">防火墙</div>
|
||
<div class="clicd-fw-status" id="clicd-fw-status-text">加载中...</div>
|
||
</div>
|
||
<label class="clicd-fw-toggle">
|
||
<input type="checkbox" id="clicd-fw-enabled">
|
||
<span class="clicd-fw-toggle-slider"></span>
|
||
</label>
|
||
</div>
|
||
<div class="clicd-fw-default-action-row">
|
||
<div>
|
||
<div class="clicd-fw-default-title">默认动作</div>
|
||
<div class="clicd-fw-default-desc">没有命中下方规则时如何处理</div>
|
||
</div>
|
||
<select class="clicd-fw-select" id="clicd-fw-default-action">
|
||
<option value="DROP">未匹配拒绝</option>
|
||
<option value="ACCEPT">未匹配放行</option>
|
||
</select>
|
||
</div>
|
||
<div class="clicd-fw-network-note">
|
||
<div class="clicd-fw-network-note-title">网络范围</div>
|
||
<div>可配置:IPv4(NAT)。 IPv4 规则覆盖 IPv4 NAT 端口映射。 NAT 入站端口按容器内部端口匹配,不是宿主机公网端口。</div>
|
||
</div>
|
||
<div class="clicd-fw-settings-actions">
|
||
<span class="clicd-fw-save-hint" id="clicd-fw-save-hint">当前设置已保存</span>
|
||
<button class="clicd-fw-btn" type="button" id="clicd-fw-save-settings" disabled>保存设置</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="clicd-fw-title-row">
|
||
<div class="clicd-fw-title">防火墙规则</div>
|
||
<button class="clicd-fw-btn clicd-fw-btn-dark" type="button" data-clicd-fw-action="open-add-rule">+ 添加规则</button>
|
||
</div>
|
||
<div id="clicd-fw-rules" class="clicd-fw-rules">
|
||
<div class="clicd-fw-section clicd-fw-muted">加载中...</div>
|
||
</div>
|
||
|
||
<pre class="clicd-fw-debug" id="clicd-fw-debug"></pre>
|
||
|
||
<div class="clicd-fw-modal-mask" id="clicd-fw-add-modal">
|
||
<div class="clicd-fw-modal clicd-fw-rule-modal">
|
||
<div class="clicd-fw-modal-header">
|
||
<div class="clicd-fw-modal-title" id="clicd-fw-rule-modal-title">添加规则</div>
|
||
<button class="clicd-fw-modal-close" type="button" id="clicd-fw-add-close" aria-label="关闭">×</button>
|
||
</div>
|
||
<div class="clicd-fw-rule-modal-body">
|
||
<div class="clicd-fw-field">
|
||
<label>网络</label>
|
||
<select class="clicd-fw-select" id="clicd-fw-add-network">
|
||
<option value="ipv4">IPv4(NAT)</option>
|
||
<option value="ipv6">IPv6</option>
|
||
<option value="all">全部</option>
|
||
</select>
|
||
</div>
|
||
<div class="clicd-fw-field">
|
||
<label>方向</label>
|
||
<select class="clicd-fw-select" id="clicd-fw-add-direction">
|
||
<option value="in">入站 (Inbound)</option>
|
||
<option value="out">出站 (Outbound)</option>
|
||
</select>
|
||
</div>
|
||
<div class="clicd-fw-field">
|
||
<label>协议</label>
|
||
<select class="clicd-fw-select" id="clicd-fw-add-protocol">
|
||
<option value="tcp">TCP</option>
|
||
<option value="udp">UDP</option>
|
||
</select>
|
||
</div>
|
||
<div class="clicd-fw-field">
|
||
<label>端口</label>
|
||
<input class="clicd-fw-input" id="clicd-fw-add-port" type="text" placeholder="如: 22 或 80,443 或 8000-9000">
|
||
<div class="clicd-fw-field-help">NAT 入站填容器内部端口,例如公网 22023 -> 容器 22,这里填 22</div>
|
||
</div>
|
||
<div class="clicd-fw-field">
|
||
<label>来源 IP</label>
|
||
<input class="clicd-fw-input" id="clicd-fw-add-source-ip" type="text" placeholder="如: 192.168.1.0/24">
|
||
<div class="clicd-fw-field-help">留空为任意 IPv4,支持 CIDR: 192.168.1.0/24</div>
|
||
</div>
|
||
<div class="clicd-fw-field">
|
||
<label>动作</label>
|
||
<select class="clicd-fw-select" id="clicd-fw-add-action">
|
||
<option value="DROP">拒绝 (DROP)</option>
|
||
<option value="ACCEPT">放行 (ACCEPT)</option>
|
||
</select>
|
||
</div>
|
||
<div class="clicd-fw-field">
|
||
<label>描述</label>
|
||
<input class="clicd-fw-input" id="clicd-fw-add-desc" type="text" placeholder="规则描述">
|
||
</div>
|
||
</div>
|
||
<div class="clicd-fw-modal-actions">
|
||
<button class="clicd-fw-btn clicd-fw-btn-secondary" type="button" id="clicd-fw-add-cancel">取消</button>
|
||
<button class="clicd-fw-btn clicd-fw-btn-dark" type="button" id="clicd-fw-add-confirm">确定</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="clicd-fw-modal-mask" id="clicd-fw-delete-modal">
|
||
<div class="clicd-fw-modal">
|
||
<div class="clicd-fw-modal-title">确认删除</div>
|
||
<div class="clicd-fw-modal-body" id="clicd-fw-delete-text">确认删除该规则?</div>
|
||
<div class="clicd-fw-modal-actions">
|
||
<button class="clicd-fw-btn clicd-fw-btn-secondary" type="button"
|
||
id="clicd-fw-delete-cancel">取消</button>
|
||
<button class="clicd-fw-btn clicd-fw-btn-danger" type="button" id="clicd-fw-delete-confirm">删除</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
(function () {
|
||
var panel = document.getElementById('clicd-fw-panel');
|
||
if (!panel || panel.getAttribute('data-bound') === '1') return;
|
||
panel.setAttribute('data-bound', '1');
|
||
|
||
var message = document.getElementById('clicd-fw-message');
|
||
var debugBox = document.getElementById('clicd-fw-debug');
|
||
var rulesContainer = document.getElementById('clicd-fw-rules');
|
||
var enabledCheckbox = document.getElementById('clicd-fw-enabled');
|
||
var defaultActionSelect = document.getElementById('clicd-fw-default-action');
|
||
var statusText = document.getElementById('clicd-fw-status-text');
|
||
var saveSettingsButton = document.getElementById('clicd-fw-save-settings');
|
||
var saveHint = document.getElementById('clicd-fw-save-hint');
|
||
var addModal = document.getElementById('clicd-fw-add-modal');
|
||
var addClose = document.getElementById('clicd-fw-add-close');
|
||
var addCancel = document.getElementById('clicd-fw-add-cancel');
|
||
var addConfirm = document.getElementById('clicd-fw-add-confirm');
|
||
var ruleModalTitle = document.getElementById('clicd-fw-rule-modal-title');
|
||
var deleteModal = document.getElementById('clicd-fw-delete-modal');
|
||
var deleteText = document.getElementById('clicd-fw-delete-text');
|
||
var deleteCancel = document.getElementById('clicd-fw-delete-cancel');
|
||
var deleteConfirm = document.getElementById('clicd-fw-delete-confirm');
|
||
var pendingDeleteRule = null;
|
||
var currentRules = [];
|
||
var ruleModalMode = 'add';
|
||
var editingRuleIndex = null;
|
||
var settingsDirty = false;
|
||
var busy = false;
|
||
|
||
function showMessage(type, text) {
|
||
message.className = 'clicd-fw-message' + (type === 'error' ? ' error' : '');
|
||
message.style.display = 'block';
|
||
message.textContent = text || '';
|
||
}
|
||
|
||
function showDebug(data) {
|
||
debugBox.style.display = 'block';
|
||
debugBox.textContent = JSON.stringify(data || {}, null, 2);
|
||
}
|
||
|
||
function endpoint() {
|
||
return "{$MODULE_CUSTOM_API}";
|
||
}
|
||
|
||
function serviceId() {
|
||
return panel.getAttribute('data-service-id') || '';
|
||
}
|
||
|
||
function updateSaveState() {
|
||
if (saveSettingsButton) {
|
||
saveSettingsButton.disabled = busy || !settingsDirty;
|
||
}
|
||
if (saveHint) {
|
||
saveHint.textContent = settingsDirty ? '有未保存更改,点击保存设置后生效' : '当前设置已保存';
|
||
}
|
||
}
|
||
|
||
function setBusy(value) {
|
||
busy = !!value;
|
||
panel.querySelectorAll('button, input, select').forEach(function (el) { el.disabled = busy; });
|
||
updateSaveState();
|
||
}
|
||
|
||
function markDirty(text) {
|
||
settingsDirty = true;
|
||
updateStatusText();
|
||
updateSaveState();
|
||
showMessage('success', text || '设置已修改,点击保存设置后生效');
|
||
}
|
||
|
||
function clearDirty() {
|
||
settingsDirty = false;
|
||
updateSaveState();
|
||
}
|
||
|
||
function escapeHtml(value) {
|
||
return String(value)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
function portDisplay(port) { return port || '所有'; }
|
||
function sourceIpDisplay(ip) { return ip || '任意'; }
|
||
function networkLabel(net) {
|
||
return net === 'ipv6' ? 'IPv6' : net === 'all' ? 'ALL' : 'IPv4';
|
||
}
|
||
|
||
function renderRules(rules) {
|
||
currentRules = Array.isArray(rules) ? rules : [];
|
||
if (currentRules.length === 0) {
|
||
rulesContainer.innerHTML = '<div class="clicd-fw-section clicd-fw-muted">暂无防火墙规则</div>';
|
||
return;
|
||
}
|
||
var rows = currentRules.map(function (rule, idx) {
|
||
var dir = (rule.direction || 'in').toLowerCase();
|
||
var proto = (rule.protocol || 'tcp').toLowerCase();
|
||
var rawAction = (rule.action || 'ACCEPT').toUpperCase();
|
||
var action = rawAction === 'ACCEPT' ? 'ACCEPT' : 'DROP';
|
||
var network = (rule.network || 'ipv4').toLowerCase();
|
||
var port = escapeHtml(portDisplay(rule.port));
|
||
var srcIp = escapeHtml(sourceIpDisplay(rule.source_ip));
|
||
var desc = escapeHtml(rule.description || '');
|
||
var enabled = rule.enabled !== false;
|
||
var enabledChecked = enabled ? 'checked' : '';
|
||
var dirClass = dir === 'in' ? 'clicd-fw-direction-in' : 'clicd-fw-direction-out';
|
||
var actionClass = 'clicd-fw-action-' + action;
|
||
return '<tr data-rule-index="' + idx + '">' +
|
||
'<td><label class="clicd-fw-toggle clicd-fw-toggle-sm"><input type="checkbox" class="clicd-fw-rule-enabled" ' + enabledChecked + '><span class="clicd-fw-toggle-slider"></span></label></td>' +
|
||
'<td><span class="clicd-fw-rule-network">' + networkLabel(network) + '</span></td>' +
|
||
'<td><span class="clicd-fw-rule-direction ' + dirClass + '">' + (dir === 'in' ? '入站' : '出站') + '</span></td>' +
|
||
'<td>' + escapeHtml(proto.toUpperCase()) + '</td>' +
|
||
'<td>' + port + '</td>' +
|
||
'<td>' + srcIp + '</td>' +
|
||
'<td><span class="clicd-fw-rule-action ' + actionClass + '">' + (action === 'ACCEPT' ? '放行' : '拒绝') + '</span></td>' +
|
||
'<td>' + (desc || '-') + '</td>' +
|
||
'<td><div class="clicd-fw-rule-ops">' +
|
||
'<button class="clicd-fw-icon-btn" type="button" data-clicd-fw-action="edit-rule" title="编辑" aria-label="编辑">✎</button>' +
|
||
'<button class="clicd-fw-icon-btn" type="button" data-clicd-fw-action="delete-rule" title="删除" aria-label="删除">⌫</button>' +
|
||
'</div></td>' +
|
||
'</tr>';
|
||
}).join('');
|
||
|
||
rulesContainer.innerHTML = '<table class="clicd-fw-table">' +
|
||
'<thead><tr>' +
|
||
'<th style="width:88px">状态</th>' +
|
||
'<th style="width:120px">网络</th>' +
|
||
'<th style="width:120px">方向</th>' +
|
||
'<th style="width:110px">协议</th>' +
|
||
'<th style="width:110px">端口</th>' +
|
||
'<th>来源/目标 IP</th>' +
|
||
'<th style="width:120px">动作</th>' +
|
||
'<th>描述</th>' +
|
||
'<th style="width:110px;text-align:right">操作</th>' +
|
||
'</tr></thead><tbody>' + rows + '</tbody></table>';
|
||
|
||
rulesContainer.querySelectorAll('.clicd-fw-rule-enabled').forEach(function (toggle, idx) {
|
||
toggle.addEventListener('change', function () {
|
||
var rule = currentRules[idx];
|
||
if (!rule) return;
|
||
rule.enabled = toggle.checked;
|
||
markDirty('规则开关已修改,点击保存设置后生效');
|
||
});
|
||
});
|
||
}
|
||
|
||
function setRuleField(id, value) {
|
||
var field = document.getElementById(id);
|
||
if (field) {
|
||
field.value = value == null ? '' : value;
|
||
}
|
||
}
|
||
|
||
function getRuleField(id, fallback) {
|
||
var field = document.getElementById(id);
|
||
if (!field) return fallback || '';
|
||
return field.value;
|
||
}
|
||
|
||
function getRuleFormPayload(baseRule) {
|
||
var base = baseRule || {};
|
||
return {
|
||
id: base.id || '',
|
||
network: getRuleField('clicd-fw-add-network', 'ipv4'),
|
||
direction: getRuleField('clicd-fw-add-direction', 'in'),
|
||
protocol: getRuleField('clicd-fw-add-protocol', 'tcp'),
|
||
port: getRuleField('clicd-fw-add-port', ''),
|
||
source_ip: getRuleField('clicd-fw-add-source-ip', ''),
|
||
action: getRuleField('clicd-fw-add-action', 'DROP'),
|
||
description: getRuleField('clicd-fw-add-desc', ''),
|
||
enabled: baseRule ? base.enabled !== false : true
|
||
};
|
||
}
|
||
|
||
function clearAddForm() {
|
||
setRuleField('clicd-fw-add-network', 'ipv4');
|
||
setRuleField('clicd-fw-add-direction', 'in');
|
||
setRuleField('clicd-fw-add-protocol', 'tcp');
|
||
setRuleField('clicd-fw-add-port', '');
|
||
setRuleField('clicd-fw-add-source-ip', '');
|
||
setRuleField('clicd-fw-add-action', 'DROP');
|
||
setRuleField('clicd-fw-add-desc', '');
|
||
}
|
||
|
||
function fillRuleForm(rule) {
|
||
var item = rule || {};
|
||
setRuleField('clicd-fw-add-network', item.network || 'ipv4');
|
||
setRuleField('clicd-fw-add-direction', item.direction || 'in');
|
||
setRuleField('clicd-fw-add-protocol', item.protocol || 'tcp');
|
||
setRuleField('clicd-fw-add-port', item.port || '');
|
||
setRuleField('clicd-fw-add-source-ip', item.source_ip || '');
|
||
setRuleField('clicd-fw-add-action', item.action || 'DROP');
|
||
setRuleField('clicd-fw-add-desc', item.description || '');
|
||
}
|
||
|
||
function saveFirewall() {
|
||
var enabled = enabledCheckbox.checked;
|
||
var defaultAction = defaultActionSelect.value;
|
||
var rules = currentRules.map(function (r) {
|
||
return {
|
||
id: r.id || '',
|
||
network: r.network || 'ipv4',
|
||
direction: r.direction || 'in',
|
||
protocol: r.protocol || 'tcp',
|
||
port: r.port || '',
|
||
source_ip: r.source_ip || '',
|
||
action: r.action || 'ACCEPT',
|
||
description: r.description || '',
|
||
enabled: r.enabled !== false
|
||
};
|
||
});
|
||
|
||
var body = new URLSearchParams();
|
||
body.set('id', serviceId());
|
||
body.set('func', 'firewallUpdate');
|
||
body.set('enabled', enabled ? 'true' : 'false');
|
||
body.set('default_action', defaultAction);
|
||
body.set('rules', JSON.stringify(rules));
|
||
|
||
setBusy(true);
|
||
fetch(endpoint(), {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||
'Authorization': 'JWT {$Think.get.jwt}'
|
||
},
|
||
credentials: 'same-origin',
|
||
body: body.toString()
|
||
})
|
||
.then(function (res) { return res.text(); })
|
||
.then(function (text) {
|
||
var data;
|
||
try { data = JSON.parse(text); } catch (e) { data = { status: 'error', msg: '非 JSON 响应: ' + text }; }
|
||
showDebug((data.data && data.data.debug) || data.debug || data);
|
||
if (data.status === 200 || data.status === 'success') {
|
||
showMessage('success', data.msg || '防火墙设置已更新');
|
||
if (data.data && Object.prototype.hasOwnProperty.call(data.data, 'enabled')) {
|
||
enabledCheckbox.checked = data.data.enabled === true || data.data.enabled === 'true' || data.data.enabled === 1;
|
||
}
|
||
if (data.data && data.data.default_action && defaultActionSelect) {
|
||
defaultActionSelect.value = data.data.default_action;
|
||
}
|
||
if (data.data && Array.isArray(data.data.rules)) {
|
||
currentRules = data.data.rules;
|
||
renderRules(currentRules);
|
||
}
|
||
clearDirty();
|
||
updateStatusText();
|
||
} else {
|
||
showMessage('error', data.msg || '更新失败');
|
||
}
|
||
})
|
||
.catch(function (e) {
|
||
showMessage('error', e.message || '请求失败');
|
||
showDebug({ error: String(e) });
|
||
})
|
||
.finally(function () {
|
||
setBusy(false);
|
||
});
|
||
}
|
||
|
||
function loadFirewall() {
|
||
var body = new URLSearchParams();
|
||
body.set('id', serviceId());
|
||
body.set('func', 'firewallList');
|
||
|
||
setBusy(true);
|
||
fetch(endpoint(), {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||
'Authorization': 'JWT {$Think.get.jwt}'
|
||
},
|
||
credentials: 'same-origin',
|
||
body: body.toString()
|
||
})
|
||
.then(function (res) { return res.text(); })
|
||
.then(function (text) {
|
||
var data;
|
||
try { data = JSON.parse(text); } catch (e) { data = { status: 'error', msg: '非 JSON 响应: ' + text }; }
|
||
showDebug((data.data && data.data.debug) || data.debug || data);
|
||
if (data.status === 200 || data.status === 'success') {
|
||
if (data.data) {
|
||
enabledCheckbox.checked = data.data.enabled === true || data.data.enabled === 'true' || data.data.enabled === 1;
|
||
if (data.data.default_action && defaultActionSelect) {
|
||
defaultActionSelect.value = data.data.default_action;
|
||
}
|
||
currentRules = Array.isArray(data.data.rules) ? data.data.rules : [];
|
||
renderRules(currentRules);
|
||
clearDirty();
|
||
updateStatusText();
|
||
}
|
||
} else {
|
||
showMessage('error', data.msg || '获取防火墙设置失败');
|
||
rulesContainer.innerHTML = '<div class="clicd-fw-section clicd-fw-muted">加载失败</div>';
|
||
}
|
||
})
|
||
.catch(function (e) {
|
||
showMessage('error', e.message || '请求失败');
|
||
showDebug({ error: String(e) });
|
||
rulesContainer.innerHTML = '<div class="clicd-fw-section clicd-fw-muted">加载失败</div>';
|
||
})
|
||
.finally(function () {
|
||
setBusy(false);
|
||
});
|
||
}
|
||
|
||
function updateStatusText() {
|
||
if (enabledCheckbox.checked) {
|
||
statusText.textContent = '已启用,未匹配规则的流量将被' + (defaultActionSelect.value === 'DROP' ? '拒绝' : '放行');
|
||
} else {
|
||
statusText.textContent = '未启用时不接管该容器流量';
|
||
}
|
||
}
|
||
|
||
function openDeleteModal(idx, desc) {
|
||
pendingDeleteRule = idx;
|
||
if (deleteText) {
|
||
deleteText.textContent = '确认删除规则: ' + (desc || '未命名规则') + ' ?';
|
||
}
|
||
if (deleteModal) {
|
||
deleteModal.style.display = 'flex';
|
||
}
|
||
}
|
||
|
||
function closeDeleteModal() {
|
||
pendingDeleteRule = null;
|
||
if (deleteModal) {
|
||
deleteModal.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
function openRuleModal(mode, idx) {
|
||
ruleModalMode = mode === 'edit' ? 'edit' : 'add';
|
||
editingRuleIndex = ruleModalMode === 'edit' ? idx : null;
|
||
if (ruleModalTitle) {
|
||
ruleModalTitle.textContent = ruleModalMode === 'edit' ? '编辑规则' : '添加规则';
|
||
}
|
||
if (ruleModalMode === 'edit' && currentRules[idx]) {
|
||
fillRuleForm(currentRules[idx]);
|
||
} else {
|
||
clearAddForm();
|
||
}
|
||
if (addModal) {
|
||
addModal.style.display = 'flex';
|
||
}
|
||
var portInput = document.getElementById('clicd-fw-add-port');
|
||
if (portInput) {
|
||
setTimeout(function () { portInput.focus(); }, 0);
|
||
}
|
||
}
|
||
|
||
function closeAddModal() {
|
||
ruleModalMode = 'add';
|
||
editingRuleIndex = null;
|
||
if (addModal) {
|
||
addModal.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
panel.addEventListener('click', function (event) {
|
||
var button = event.target.closest('[data-clicd-fw-action]');
|
||
if (!button) return;
|
||
var action = button.getAttribute('data-clicd-fw-action');
|
||
|
||
if (action === 'open-add-rule') {
|
||
openRuleModal('add');
|
||
return;
|
||
}
|
||
|
||
var item = button.closest('[data-rule-index]');
|
||
if (!item) return;
|
||
var idx = parseInt(item.getAttribute('data-rule-index'), 10);
|
||
if (isNaN(idx) || !currentRules[idx]) return;
|
||
|
||
if (action === 'edit-rule') {
|
||
openRuleModal('edit', idx);
|
||
return;
|
||
}
|
||
|
||
if (action === 'delete-rule') {
|
||
var desc = currentRules[idx].description || (currentRules[idx].protocol + '/' + (currentRules[idx].port || 'all'));
|
||
openDeleteModal(idx, desc);
|
||
return;
|
||
}
|
||
});
|
||
|
||
enabledCheckbox.addEventListener('change', function () {
|
||
updateStatusText();
|
||
markDirty('防火墙开关已修改,点击保存设置后生效');
|
||
});
|
||
|
||
defaultActionSelect.addEventListener('change', function () {
|
||
updateStatusText();
|
||
markDirty('默认动作已修改,点击保存设置后生效');
|
||
});
|
||
|
||
if (saveSettingsButton) {
|
||
saveSettingsButton.addEventListener('click', saveFirewall);
|
||
}
|
||
|
||
if (addClose) {
|
||
addClose.addEventListener('click', closeAddModal);
|
||
}
|
||
if (addCancel) {
|
||
addCancel.addEventListener('click', closeAddModal);
|
||
}
|
||
if (addModal) {
|
||
addModal.addEventListener('click', function (event) {
|
||
if (event.target === addModal) closeAddModal();
|
||
});
|
||
}
|
||
if (addConfirm) {
|
||
addConfirm.addEventListener('click', function () {
|
||
if (ruleModalMode === 'edit' && editingRuleIndex !== null && currentRules[editingRuleIndex]) {
|
||
currentRules[editingRuleIndex] = getRuleFormPayload(currentRules[editingRuleIndex]);
|
||
renderRules(currentRules);
|
||
closeAddModal();
|
||
markDirty('规则已更新,点击保存设置后生效');
|
||
return;
|
||
}
|
||
currentRules.push(getRuleFormPayload(null));
|
||
renderRules(currentRules);
|
||
closeAddModal();
|
||
clearAddForm();
|
||
markDirty('规则已添加,点击保存设置后生效');
|
||
});
|
||
}
|
||
|
||
if (deleteCancel) {
|
||
deleteCancel.addEventListener('click', closeDeleteModal);
|
||
}
|
||
if (deleteModal) {
|
||
deleteModal.addEventListener('click', function (event) {
|
||
if (event.target === deleteModal) closeDeleteModal();
|
||
});
|
||
}
|
||
if (deleteConfirm) {
|
||
deleteConfirm.addEventListener('click', function () {
|
||
if (pendingDeleteRule === null) return;
|
||
var idx = pendingDeleteRule;
|
||
closeDeleteModal();
|
||
if (idx >= 0 && idx < currentRules.length) {
|
||
currentRules.splice(idx, 1);
|
||
renderRules(currentRules);
|
||
markDirty('规则已删除,点击保存设置后生效');
|
||
}
|
||
});
|
||
}
|
||
|
||
updateSaveState();
|
||
loadFirewall();
|
||
})();
|
||
</script>
|