Initial commit

This commit is contained in:
2025-11-18 03:36:49 +08:00
commit d17c7efb3c
7078 changed files with 831480 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
{% extends 'base.html' %}
{% block title %}添加域名{% endblock %}
{% block content %}
<div class="container" style="max-width: 860px;">
<div class="d-flex justify-content-between align-items-center mb-3">
<h3>添加域名</h3>
<div>
<a class="btn btn-outline-secondary" href="{% url 'domains:list' %}">返回列表</a>
<a class="btn btn-outline-secondary" href="/plans/">查看套餐</a>
</div>
</div>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
{% endfor %}
{% endif %}
<form method="post" class="needs-validation" novalidate>
{% csrf_token %}
{{ form.non_field_errors }}
<div class="mb-3">
<label class="form-label" for="id_name">主域名</label>
{{ form.name }}
</div>
<div class="mb-3">
<label class="form-label" for="id_subdomains">接入子域名(逗号分隔)</label>
{{ form.subdomains }}
<div class="form-text">例如www,static</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label" for="id_origin_host">源站地址</label>
{{ form.origin_host }}
</div>
<div class="col-md-3 mb-3">
<label class="form-label" for="id_origin_protocol">协议</label>
{{ form.origin_protocol }}
</div>
<div class="col-md-3 mb-3">
<label class="form-label" for="id_origin_port">端口</label>
{{ form.origin_port }}
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label" for="id_plan">套餐</label>
{{ form.plan }}
</div>
<div class="col-md-6 mb-3 form-check">
{{ form.enable_websocket }}
<label class="form-check-label">启用 WebSocket</label>
</div>
</div>
<div class="mb-3">
<button type="submit" class="btn btn-primary">创建</button>
<a href="{% url 'domains:list' %}" class="btn btn-secondary">取消</a>
</div>
</form>
<div class="alert alert-info mt-4">
创建成功后系统会自动跳转到域名详情页:
在详情页可查看生成的 CNAME 目标,并可点击“检测 DNS 生效”按钮进行验证。
如未设置套餐将默认使用系统配置的免费额度(可在运营面板调整)。
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,339 @@
{% extends 'base.html' %}
{% block title %}域名详情 - {{ domain.name }}{% endblock %}
{% block extra_head %}
<style>
.sidebar-sticky { position: sticky; top: 80px; }
.list-group .list-group-item.active { background-color: #0d6efd; border-color: #0d6efd; }
.card .badge { font-size: 0.9rem; }
.progress { height: 20px; }
.progress-bar { font-size: 0.8rem; }
@media (max-width: 991.98px){ .sidebar-sticky { position: static; } }
.table td, .table th { vertical-align: middle; }
.text-muted.small { line-height: 1.2; }
</style>
{% endblock %}
{% block content %}
<div class="row g-4">
<div class="col-lg-2 col-md-3">
<div class="sidebar-sticky">
<div class="list-group">
<a class="list-group-item list-group-item-action active" href="#">概览</a>
<a class="list-group-item list-group-item-action" href="#">统计</a>
<a class="list-group-item list-group-item-action" href="#">接入信息</a>
<a class="list-group-item list-group-item-action" href="{% url 'domains:settings' domain_id=domain.id %}">设置</a>
</div>
</div>
</div>
<div class="col-lg-10 col-md-9">
<div class="d-flex justify-content-between align-items-center mb-3">
<h3>域名:{{ domain.name }}</h3>
<div class="btn-group">
<a href="{% url 'domains:list' %}" class="btn btn-outline-secondary">返回列表</a>
<a href="{% url 'domains:upgrade' domain_id=domain.id %}" class="btn btn-primary" onclick="return confirm('升级套餐将影响后续计费,确认继续?');">升级套餐</a>
<a href="{% url 'domains:logs' domain_id=domain.id %}" class="btn btn-outline-primary">访问日志</a>
</div>
</div>
<div class="card mb-4">
<div class="card-header">概览</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-6">
<div>当前套餐:{% if domain.current_plan %}{{ domain.current_plan.name }}{% else %}-{% endif %}</div>
<div>状态:{{ domain.get_status_display }}</div>
</div>
<div class="col-md-6">
<div class="mb-1">本月流量:{{ used_gb }} GB / {{ total_quota_gb }} GB</div>
<div class="progress" role="progressbar" aria-label="Monthly usage" aria-valuenow="{{ progress_pct }}" aria-valuemin="0" aria-valuemax="100">
<div class="progress-bar" style="width: {{ progress_pct }}%">{{ progress_pct }}%</div>
</div>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">统计近30天</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-4">
<div class="fw-semibold">近 7 天合计</div>
<div><span class="badge bg-primary">{{ last7_total_gb|default:0 }} GB</span></div>
<div class="text-muted small">均值:{{ last7_avg_gb|default:0 }} GB/日</div>
</div>
<div class="col-md-4">
<div class="fw-semibold">近 30 天合计</div>
<div><span class="badge bg-info text-dark">{{ last30_total_gb|default:0 }} GB</span></div>
<div class="text-muted small">均值:{{ last30_avg_gb|default:0 }} GB/日</div>
</div>
<div class="col-md-4">
<div class="fw-semibold">峰值日(流量)</div>
<div>
{% if peak_volume_day %}
<span class="badge bg-warning text-dark">{{ peak_volume_day.day }} / {{ peak_volume_day.gb }} GB</span>
{% else %}
<span class="text-muted">无数据</span>
{% endif %}
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-4">
<div class="fw-semibold">峰值日(带宽)</div>
<div>
{% if peak_bandwidth_day %}
<span class="badge bg-secondary">{{ peak_bandwidth_day.day }} / {{ peak_bandwidth_day.mbps }} Mbps</span>
{% else %}
<span class="text-muted">无数据</span>
{% endif %}
</div>
</div>
<div class="col-md-8">
<div class="fw-semibold">请求数 / 状态码分布</div>
<div class="mb-2">
{% if requests_24h_total %}
近 24 小时请求数:<span class="badge bg-primary">{{ requests_24h_total }}</span>
{% else %}
<span class="text-muted">请求数不可用(请在设置启用访问日志)</span>
{% endif %}
</div>
{% if status_bins %}
<div class="d-flex gap-2 flex-wrap">
<span class="badge text-bg-success">2xx {{ status_bins.2xx }}</span>
<span class="badge text-bg-info">3xx {{ status_bins.3xx }}</span>
<span class="badge text-bg-warning text-dark">4xx {{ status_bins.4xx }}</span>
<span class="badge text-bg-danger">5xx {{ status_bins.5xx }}</span>
</div>
{% if status_top %}
<div class="text-muted small mt-1">Top 状态码:
{% for code,count in status_top %}
<span class="me-2"><code>{{ code }}</code> {{ count }}</span>
{% endfor %}
</div>
{% endif %}
{% else %}
<div class="text-muted small">状态码分布不可用。</div>
{% endif %}
</div>
</div>
{% if traffic_rows %}
<canvas id="trafficChart" height="120" class="mb-3" style="width: 100%;"></canvas>
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead>
<tr>
<th>日期</th>
<th>流量GB</th>
<th>峰值带宽Mbps</th>
</tr>
</thead>
<tbody>
{% for r in traffic_rows %}
<tr>
<td>{{ r.day }}</td>
<td>{{ r.gb }}</td>
<td>{% if r.peak_mbps %}{{ r.peak_mbps }}{% else %}<span class="text-muted">-</span>{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-muted">暂无统计数据。</div>
{% endif %}
<div class="small text-muted">说明:统计数据来源于 GoEdge 每日拉取结果,仅供参考。</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">接入信息CNAME</div>
<div class="card-body">
<table class="table table-sm">
<thead>
<tr>
<th>主机名</th>
<th>期望 CNAME 目标</th>
<th>实际解析</th>
<th>状态</th>
</tr>
</thead>
<tbody>
{% if cname_results %}
{% for r in cname_results %}
<tr>
<td>{{ r.hostname }}</td>
<td><code>{{ r.expected }}</code></td>
<td>
{% if r.actual %}
{% for a in r.actual %}<code>{{ a }}</code>{% if not forloop.last %}, {% endif %}{% endfor %}
{% else %}
<span class="text-muted">(无)</span>
{% endif %}
</td>
<td>
{% if r.ok %}
<span class="badge text-bg-success">正确</span>
{% else %}
<span class="badge text-bg-warning">未匹配</span>
{% if r.error %}<div class="text-muted">{{ r.error }}</div>{% endif %}
{% endif %}
</td>
</tr>
{% endfor %}
{% else %}
{% for host, target in domain.cname_targets.items %}
<tr>
<td>{{ host }}</td>
<td><code>{{ target }}</code></td>
<td class="text-muted">(未检测)</td>
<td><span class="badge text-bg-secondary">未知</span></td>
</tr>
{% endfor %}
{% endif %}
</tbody>
</table>
<form method="post" action="{% url 'domains:check_dns' domain_id=domain.id %}" class="mt-2">
{% csrf_token %}
<button type="submit" class="btn btn-primary">立即检测 DNS 生效</button>
</form>
</div>
</div>
<div class="card mb-4">
<div class="card-header">GoEdge 同步状态</div>
<div class="card-body">
{% if goedge_status and not goedge_status.error %}
<div class="row g-3">
<div class="col-md-3">
<div class="fw-semibold">Web ID</div>
<div>{% if goedge_status.webId %}<span class="badge text-bg-secondary">{{ goedge_status.webId }}</span>{% else %}<span class="text-muted">未获取</span>{% endif %}</div>
</div>
<div class="col-md-3">
<div class="fw-semibold">访问日志</div>
{% if goedge_status.accessLog.isOn %}
<span class="badge text-bg-success">已启用</span>
{% elif goedge_status.accessLog.isOn is not none %}
<span class="badge text-bg-secondary">未启用</span>
{% else %}
<span class="text-muted">未知</span>
{% endif %}
{% if goedge_status.accessLog.policyId %}
<div class="text-muted small">PolicyId: {{ goedge_status.accessLog.policyId }}</div>
{% endif %}
</div>
<div class="col-md-3">
<div class="fw-semibold">WebSocket</div>
{% if goedge_status.websocket.isOn %}
<span class="badge text-bg-success">已启用</span>
{% elif goedge_status.websocket.isOn is not none %}
<span class="badge text-bg-secondary">未启用</span>
{% else %}
<span class="text-muted">未知</span>
{% endif %}
</div>
<div class="col-md-3">
<div class="fw-semibold">WAF</div>
{% if goedge_status.firewall.isOn %}
<span class="badge text-bg-success">已启用</span>
{% elif goedge_status.firewall.isOn is not none %}
<span class="badge text-bg-secondary">未启用</span>
{% else %}
<span class="text-muted">未知</span>
{% endif %}
{% if goedge_status.firewall.policyId %}
<div class="text-muted small">PolicyId: {{ goedge_status.firewall.policyId }}</div>
{% endif %}
</div>
<div class="col-md-3">
<div class="fw-semibold">SSL策略</div>
<div>{% if goedge_status.sslPolicy.id %}<span class="badge text-bg-secondary">{{ goedge_status.sslPolicy.id }}</span>{% else %}<span class="text-muted">未获取</span>{% endif %}</div>
</div>
<div class="col-md-3">
<div class="fw-semibold">HTTP/3</div>
{% if goedge_status.sslPolicy.http3Enabled %}
<span class="badge text-bg-success">已启用</span>
{% elif goedge_status.sslPolicy.http3Enabled is not none %}
<span class="badge text-bg-secondary">未启用</span>
{% else %}
<span class="text-muted">未知</span>
{% endif %}
</div>
</div>
{% elif goedge_status and goedge_status.error %}
<div class="alert alert-warning">状态查询失败:{{ goedge_status.error }}</div>
{% else %}
<div class="text-muted">当前域名尚未关联 GoEdge 服务或无可查询信息。</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
var rows = [
{% for r in traffic_rows reversed %}
{ day: '{{ r.day|date:"Y-m-d" }}', gb: {{ r.gb|default:0 }}, peak_mbps: {% if r.peak_mbps %}{{ r.peak_mbps }}{% else %}null{% endif %} },
{% endfor %}
];
if (!rows.length || !window.Chart) return;
var labels = rows.map(function(x){ return x.day; });
var dataGb = rows.map(function(x){ return x.gb || 0; });
var dataMbps = rows.map(function(x){ return x.peak_mbps === null ? null : x.peak_mbps; });
var el = document.getElementById('trafficChart');
if (!el) return;
var ctx = el.getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: '流量GB',
data: dataGb,
borderColor: '#0d6efd',
backgroundColor: 'rgba(13,110,253,0.15)',
fill: true,
tension: 0.3,
yAxisID: 'yGb'
},
{
label: '峰值带宽Mbps',
data: dataMbps,
borderColor: '#6f42c1',
backgroundColor: 'rgba(111,66,193,0.08)',
fill: false,
tension: 0.3,
yAxisID: 'yMbps'
}
]
},
options: {
responsive: true,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { position: 'bottom' },
tooltip: {
callbacks: {
label: function(ctx){
var suffix = ctx.dataset.yAxisID === 'yGb' ? ' GB' : ' Mbps';
return ctx.dataset.label + ': ' + ctx.formattedValue + suffix;
}
}
}
},
scales: {
yGb: { type: 'linear', position: 'left', title: { display: true, text: 'GB' } },
yMbps: { type: 'linear', position: 'right', title: { display: true, text: 'Mbps' }, grid: { drawOnChartArea: false } }
}
}
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,35 @@
{% extends 'base.html' %}
{% block title %}域名列表{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-3">
<h3>域名列表</h3>
<a href="{% url 'domains:add' %}" class="btn btn-primary">添加域名</a>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>域名</th>
<th>套餐</th>
<th>状态</th>
<th>已用/本月</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for d in domains %}
<tr>
<td>{{ d.name }}</td>
<td>{% if d.current_plan %}{{ d.current_plan.name }}{% else %}-{% endif %}</td>
<td>{{ d.get_status_display }}</td>
<td>{% if d.total_quota_gb %}{{ d.used_gb }} GB / {{ d.total_quota_gb }} GB{% else %}-{% endif %}</td>
<td>
<a href="{% url 'domains:detail' domain_id=d.id %}" class="btn btn-sm btn-outline-secondary">详情</a>
</td>
</tr>
{% empty %}
<tr><td colspan="5" class="text-center">暂无域名</td></tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@@ -0,0 +1,86 @@
{% extends 'base.html' %}
{% block content %}
<div class="container py-4">
<h3>访问日志 - {{ domain.name }}</h3>
<form class="row g-3 mb-3" method="get">
<div class="col-md-2">
<label class="form-label">日期(YYYYMMDD)</label>
<input type="text" name="day" value="{{ day }}" class="form-control" placeholder="20251117">
</div>
<div class="col-md-2">
<label class="form-label">开始小时</label>
<input type="text" name="hour_from" value="{{ hour_from }}" class="form-control" placeholder="00">
</div>
<div class="col-md-2">
<label class="form-label">结束小时</label>
<input type="text" name="hour_to" value="{{ hour_to }}" class="form-control" placeholder="23">
</div>
<div class="col-md-2">
<label class="form-label">IP</label>
<input type="text" name="ip" value="{{ ip }}" class="form-control" placeholder="1.2.3.4">
</div>
<div class="col-md-3">
<label class="form-label">关键词</label>
<input type="text" name="keyword" value="{{ keyword }}" class="form-control" placeholder="path or UA">
</div>
<div class="col-md-2">
<label class="form-label">状态码</label>
<input type="text" name="status_code" value="{{ status_code }}" class="form-control" placeholder="200">
</div>
<div class="col-md-1">
<label class="form-label">条数</label>
<input type="number" name="size" value="{{ size }}" class="form-control" min="1" max="200">
</div>
<div class="col-md-2 d-flex align-items-end">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="reverse" value="1" {% if reverse %}checked{% endif %} id="revCheck">
<label class="form-check-label" for="revCheck">反向</label>
</div>
</div>
<div class="col-md-10 d-flex align-items-end justify-content-end gap-2">
<button type="submit" class="btn btn-primary">查询</button>
<a class="btn btn-outline-secondary" href="?day={{ day }}&hour_from={{ hour_from }}&hour_to={{ hour_to }}&ip={{ ip }}&keyword={{ keyword }}&status_code={{ status_code }}&size={{ size }}&reverse={{ reverse|yesno:'1,0' }}&export=csv">导出 CSV</a>
<a class="btn btn-outline-secondary" href="?day={{ day }}&hour_from={{ hour_from }}&hour_to={{ hour_to }}&ip={{ ip }}&keyword={{ keyword }}&status_code={{ status_code }}&size={{ size }}&reverse={{ reverse|yesno:'1,0' }}&export=json">导出 JSON</a>
</div>
</form>
<div class="table-responsive">
<table class="table table-sm table-striped">
<thead>
<tr>
<th>时间</th>
<th>域名</th>
<th>IP</th>
<th>方法</th>
<th>路径</th>
<th>状态码</th>
<th>字节数</th>
<th>UA</th>
</tr>
</thead>
<tbody>
{% for log in logs %}
<tr>
<td>{{ log.timeLocal }}</td>
<td>{{ log.host }}</td>
<td>{{ log.remoteAddr }}</td>
<td>{{ log.method }}</td>
<td>{{ log.requestURI }}</td>
<td>{{ log.status }}</td>
<td>{{ log.bytesSent }}</td>
<td>{{ log.userAgent }}</td>
</tr>
{% empty %}
<tr><td colspan="8" class="text-center">无数据</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% if has_more and request_id %}
<div class="d-flex justify-content-end">
<a class="btn btn-outline-secondary" href="?day={{ day }}&hour_from={{ hour_from }}&hour_to={{ hour_to }}&ip={{ ip }}&keyword={{ keyword }}&status_code={{ status_code }}&size={{ size }}&reverse={{ reverse|yesno:'1,0' }}&request_id={{ request_id }}">下一页</a>
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,185 @@
{% extends 'base.html' %}
{% block title %}功能设置 - {{ domain.name }}{% endblock %}
{% block extra_head %}
<style>
.form-text { font-size: 0.85rem; }
.sidebar-sticky { position: sticky; top: 80px; }
@media (max-width: 991.98px){ .sidebar-sticky { position: static; } }
textarea.json-field { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
.disabled-hint { color: #6c757d; font-size: 0.85rem; }
</style>
{% endblock %}
{% block content %}
<div class="row g-4">
<div class="col-lg-2 col-md-3">
<div class="sidebar-sticky">
<div class="list-group">
<a class="list-group-item list-group-item-action" href="{% url 'domains:detail' domain_id=domain.id %}">概览</a>
<a class="list-group-item list-group-item-action" href="{% url 'domains:detail' domain_id=domain.id %}#stats">统计</a>
<a class="list-group-item list-group-item-action" href="{% url 'domains:detail' domain_id=domain.id %}#cname">接入信息</a>
<a class="list-group-item list-group-item-action active" href="#">设置</a>
</div>
</div>
</div>
<div class="col-lg-10 col-md-9">
<div class="d-flex justify-content-between align-items-center mb-3">
<h3>功能设置:{{ domain.name }}</h3>
<div class="btn-group">
<a href="{% url 'domains:detail' domain_id=domain.id %}" class="btn btn-outline-secondary">返回详情</a>
</div>
</div>
<div class="alert alert-info">
根据当前套餐的功能开关,部分设置可能不可用或被禁用。变更套餐可启用更多功能。
</div>
<form method="post" action="{% url 'domains:settings' domain_id=domain.id %}">
{% csrf_token %}
<div class="card mb-4">
<div class="card-header">安全与网络</div>
<div class="card-body">
<div class="form-check form-switch mb-2">
<input type="checkbox" class="form-check-input" id="id_waf_enabled" name="waf_enabled" {% if form.waf_enabled.value %}checked{% endif %} {% if not plan_features.waf_enabled %}disabled{% endif %}>
<label class="form-check-label" for="id_waf_enabled">启用 WAF</label>
{% if not plan_features.waf_enabled %}<div class="disabled-hint">当前套餐不支持 WAF。</div>{% endif %}
</div>
<div class="form-check form-switch mb-2">
<input type="checkbox" class="form-check-input" id="id_http3_enabled" name="http3_enabled" {% if form.http3_enabled.value %}checked{% endif %} {% if not plan_features.http3_enabled %}disabled{% endif %}>
<label class="form-check-label" for="id_http3_enabled">启用 HTTP/3</label>
{% if not plan_features.http3_enabled %}<div class="disabled-hint">当前套餐不支持 HTTP/3。</div>{% endif %}
</div>
<div class="form-check form-switch mb-2">
<input type="checkbox" class="form-check-input" id="id_websocket_enabled" name="websocket_enabled" {% if form.websocket_enabled.value %}checked{% endif %} {% if not plan_features.websocket_enabled %}disabled{% endif %}>
<label class="form-check-label" for="id_websocket_enabled">启用 WebSocket</label>
{% if not plan_features.websocket_enabled %}<div class="disabled-hint">当前套餐不支持 WebSocket。</div>{% endif %}
</div>
<div class="form-check form-switch mb-2">
<input type="checkbox" class="form-check-input" id="id_logs_enabled" name="logs_enabled" {% if form.logs_enabled.value %}checked{% endif %} {% if not plan_features.logs_enabled %}disabled{% endif %}>
<label class="form-check-label" for="id_logs_enabled">启用日志下载</label>
{% if not plan_features.logs_enabled %}<div class="disabled-hint">当前套餐不支持实时/离线日志。</div>{% endif %}
</div>
<div class="form-check form-switch mb-2">
<input type="checkbox" class="form-check-input" id="id_redirect_https_enabled" name="redirect_https_enabled" {% if form.redirect_https_enabled.value %}checked{% endif %}>
<label class="form-check-label" for="id_redirect_https_enabled">强制 HTTP→HTTPS 跳转</label>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">缓存与页面规则</div>
<div class="card-body">
<div class="mb-2">
<div class="btn-group">
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="applyCacheTemplate('static')">套用静态资源缓存</button>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="applyPageTemplate('redirect_https')">套用强制 HTTPS 跳转</button>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="applyPageTemplate('rewrite_www')">套用 www→非www 重写</button>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="downloadJson('id_cache_rules_json')">导出缓存 JSON</button>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="downloadJson('id_page_rules_json')">导出页面规则 JSON</button>
</div>
</div>
<div class="mb-3">
<label for="id_cache_rules_json" class="form-label">缓存规则JSON</label>
<textarea class="form-control json-field" id="id_cache_rules_json" name="cache_rules_json" rows="6">{{ form.cache_rules_json.value }}</textarea>
<div class="form-text">示例:{"rules": [{"path": "/images/*", "ttl": 3600}]}</div>
</div>
<div class="mb-3">
<label for="id_page_rules_json" class="form-label">页面规则JSON</label>
<textarea class="form-control json-field" id="id_page_rules_json" name="page_rules_json" rows="6">{{ form.page_rules_json.value }}</textarea>
<div class="form-text">示例:{"rules": [{"match": "example.com/*", "action": "cache_bypass"}]}</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">IP 控制</div>
<div class="card-body">
<div class="mb-3">
<label for="id_ip_whitelist" class="form-label">IP 白名单(逗号分隔)</label>
<input type="text" class="form-control" id="id_ip_whitelist" name="ip_whitelist" value="{{ form.ip_whitelist.value }}">
<div class="form-text">示例1.2.3.4, 10.0.0.1</div>
</div>
<div class="mb-3">
<label for="id_ip_blacklist" class="form-label">IP 黑名单(逗号分隔)</label>
<input type="text" class="form-control" id="id_ip_blacklist" name="ip_blacklist" value="{{ form.ip_blacklist.value }}">
<div class="form-text">示例203.0.113.0/24</div>
</div>
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary" onclick="return confirm('保存设置会立即影响后端配置,确认提交?');">保存设置</button>
<a href="{% url 'domains:detail' domain_id=domain.id %}" class="btn btn-outline-secondary">取消</a>
</div>
</form>
<div class="card mt-4">
<div class="card-header">套餐功能可用性</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<ul class="list-group list-group-flush">
<li class="list-group-item">WAF{% if plan_features.waf_enabled %}<span class="text-success">可用</span>{% else %}<span class="text-muted">不可用</span>{% endif %}</li>
<li class="list-group-item">HTTP/3{% if plan_features.http3_enabled %}<span class="text-success">可用</span>{% else %}<span class="text-muted">不可用</span>{% endif %}</li>
<li class="list-group-item">WebSocket{% if plan_features.websocket_enabled %}<span class="text-success">可用</span>{% else %}<span class="text-muted">不可用</span>{% endif %}</li>
</ul>
</div>
<div class="col-md-6">
<ul class="list-group list-group-flush">
<li class="list-group-item">日志下载:{% if plan_features.logs_enabled %}<span class="text-success">可用</span>{% else %}<span class="text-muted">不可用</span>{% endif %}</li>
<li class="list-group-item">自定义规则:{% if plan_features.page_rules_limit %}<span class="text-success">上限 {{ plan_features.page_rules_limit }}</span>{% else %}<span class="text-muted">不可用</span>{% endif %}</li>
</ul>
</div>
</div>
<div class="small text-muted mt-2">提示:套餐功能由管理员在“套餐管理”中配置。</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
function applyCacheTemplate(name){
var el = document.getElementById('id_cache_rules_json');
if(!el) return;
var tpl = {};
if(name === 'static'){
tpl = {
rules: [
{ path: '/static/*', ttl: 3600 },
{ path: '/images/*', ttl: 3600 },
{ path: '/css/*', ttl: 1800 },
{ path: '/js/*', ttl: 1800 }
]
};
}
el.value = JSON.stringify(tpl, null, 2);
}
function applyPageTemplate(name){
var el = document.getElementById('id_page_rules_json');
if(!el) return;
var tpl = {};
if(name === 'redirect_https'){
tpl = { redirectToHTTPS: { isOn: true } };
var chk = document.getElementById('id_redirect_https_enabled');
if(chk) chk.checked = true;
}
if(name === 'rewrite_www'){
tpl = { rewriteRules: [{ pattern: '^www\\.(.*)$', replace: '$1', flags: 'R=301' }] };
}
el.value = JSON.stringify(tpl, null, 2);
}
function downloadJson(textareaId){
var el = document.getElementById(textareaId);
if(!el) return;
var blob = new Blob([el.value || '{}'], {type: 'application/json'});
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = textareaId + '.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
</script>
{% endblock %}

View File

@@ -0,0 +1,78 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>升级套餐 - {{ domain.name }}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
<div class="container-fluid">
<a class="navbar-brand" href="#">PyGoEdge 面板</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light" href="/domains/">域名</a>
<a class="btn btn-outline-light" href="/plans/">套餐</a>
<a class="btn btn-outline-light" href="/admin/">Admin</a>
</div>
</div>
</nav>
<div class="container" style="max-width: 860px;">
<div class="d-flex justify-content-between align-items-center mb-3">
<h3>升级套餐:{{ domain.name }}</h3>
<a href="{% url 'domains:detail' domain_id=domain.id %}" class="btn btn-outline-secondary">返回详情</a>
</div>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
{% endfor %}
{% endif %}
<form method="post" onsubmit="return confirm('确认将该域名升级到所选套餐?');">
{% csrf_token %}
<div class="alert alert-warning">提示:功能开关将立即按新套餐生效;计费按默认规则在下一个周期计算(当前周期不追溯调整)。</div>
<table class="table table-striped">
<thead>
<tr>
<th>选择</th>
<th>名称</th>
<th>月费(/域名)</th>
<th>包含流量GB</th>
<th>超量单价(元/GB</th>
<th>功能</th>
</tr>
</thead>
<tbody>
{% for p in plans %}
<tr>
<td>
<input type="radio" name="plan_id" value="{{ p.id }}" required {% if domain.current_plan and domain.current_plan.id == p.id %}disabled{% endif %} />
</td>
<td>{{ p.name }}</td>
<td>{{ p.base_price_per_domain }}</td>
<td>{{ p.included_traffic_gb_per_domain }}</td>
<td>{{ p.overage_price_per_gb }}</td>
<td>
{% if p.features %}
<span class="badge bg-secondary me-1">WAF: {% if p.features.waf_enabled %}✔{% else %}✖{% endif %}</span>
<span class="badge bg-secondary me-1">HTTP/3: {% if p.features.http3_enabled %}✔{% else %}✖{% endif %}</span>
<span class="badge bg-secondary me-1">日志: {% if p.features.logs_enabled %}✔{% else %}✖{% endif %}</span>
{% else %}
-
{% endif %}
</td>
</tr>
{% empty %}
<tr><td colspan="6" class="text-center">暂无可升级的公开套餐</td></tr>
{% endfor %}
</tbody>
</table>
<button type="submit" class="btn btn-primary">确认升级</button>
</form>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>