201 lines
10 KiB
HTML
201 lines
10 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>后台管理 - 云价眼</title>
|
|
<link rel="stylesheet" href="/static/css/style.css">
|
|
<link rel="stylesheet" href="/static/css/admin.css">
|
|
</head>
|
|
<body class="admin-page">
|
|
<header class="admin-header">
|
|
<h1>后台管理</h1>
|
|
<nav>
|
|
<a href="{{ url_for('admin_providers') }}">厂商管理</a>
|
|
<a href="{{ url_for('admin_users') }}">用户管理</a>
|
|
<a href="{{ url_for('admin_forum_posts') }}">帖子管理</a>
|
|
<a href="{{ url_for('admin_forum_comments') }}">评论管理</a>
|
|
<a href="{{ url_for('admin_forum_categories') }}">论坛分类</a>
|
|
<a href="{{ url_for('admin_forum_reports') }}">举报审核</a>
|
|
<a href="{{ url_for('admin_forum_tracking') }}">埋点看板</a>
|
|
<a href="#plan-form" id="nav-add-plan">+ 添加配置</a>
|
|
<a href="{{ url_for('admin_export_excel') }}">导出 Excel</a>
|
|
<a href="{{ url_for('admin_import') }}">导入 Excel</a>
|
|
<a href="{{ url_for('index') }}">查看前台</a>
|
|
<a href="{{ url_for('admin_logout') }}">退出</a>
|
|
</nav>
|
|
</header>
|
|
<main class="admin-main">
|
|
{% if request.args.get('msg') %}
|
|
<p class="hint success-msg">{{ request.args.get('msg') }}</p>
|
|
{% endif %}
|
|
<section class="dashboard-section">
|
|
<h2>社区管理</h2>
|
|
<p>可在后台直接完成用户、帖子、评论、分类与举报处理。</p>
|
|
<div class="quick-link-grid">
|
|
<a href="{{ url_for('admin_users') }}">用户管理</a>
|
|
<a href="{{ url_for('admin_forum_posts') }}">帖子管理</a>
|
|
<a href="{{ url_for('admin_forum_comments') }}">评论管理</a>
|
|
<a href="{{ url_for('admin_forum_categories') }}">分类管理</a>
|
|
<a href="{{ url_for('admin_forum_reports') }}">举报审核</a>
|
|
<a href="{{ url_for('admin_forum_tracking') }}">埋点看板</a>
|
|
</div>
|
|
</section>
|
|
<section class="dashboard-section">
|
|
<h2>厂商</h2>
|
|
<p>先添加厂商,再在厂商下添加多条配置。</p>
|
|
<ul class="provider-list">
|
|
{% for p in providers %}
|
|
<li class="provider-item">
|
|
<a href="{{ url_for('admin_provider_detail', provider_id=p.id) }}">{{ p.name }}</a>
|
|
<span class="provider-meta">({{ p.plans.count() }} 条配置)</span>
|
|
</li>
|
|
{% else %}
|
|
<li>暂无厂商,<a href="{{ url_for('admin_provider_new') }}">添加厂商</a></li>
|
|
{% endfor %}
|
|
</ul>
|
|
</section>
|
|
<section class="dashboard-section" id="plan-form-section">
|
|
<h2 id="plan-form-title">添加配置</h2>
|
|
{% if providers %}
|
|
{% include "admin/_plan_form_inline.html" with context %}
|
|
{% else %}
|
|
<p class="error">请先 <a href="{{ url_for('admin_provider_new') }}">添加厂商</a>。</p>
|
|
{% endif %}
|
|
</section>
|
|
<section class="dashboard-section">
|
|
<h2>全部配置</h2>
|
|
<table class="admin-table">
|
|
<thead>
|
|
<tr>
|
|
<th>厂商</th>
|
|
<th>国家/区域</th>
|
|
<th>配置</th>
|
|
<th>vCPU</th>
|
|
<th>内存</th>
|
|
<th>流量</th>
|
|
<th>月付</th>
|
|
<th>涨跌趋势</th>
|
|
<th>官网链接</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for plan in plans %}
|
|
<tr data-plan-id="{{ plan.id }}">
|
|
<td>{% if plan.provider_id %}<a href="{{ url_for('admin_provider_detail', provider_id=plan.provider_id) }}">{{ plan.provider_name }}</a>{% else %}{{ plan.provider_name }}{% endif %}</td>
|
|
<td>{{ plan.countries or plan.region or '—' }}</td>
|
|
<td>{{ plan.display_name }}</td>
|
|
<td>{{ plan.vcpu if plan.vcpu is not none else '—' }}</td>
|
|
<td>{{ plan.memory_gb ~ ' GB' if plan.memory_gb is not none else '—' }}</td>
|
|
<td>{{ plan.traffic or '—' }}</td>
|
|
<td>{% if plan.price_cny is not none %}¥{{ plan.price_cny }}{% elif plan.price_usd is not none %}${{ plan.price_usd }}{% else %}—{% endif %}</td>
|
|
<td>
|
|
{% set trend = plan_trends.get(plan.id) %}
|
|
{% if trend %}
|
|
<div class="price-trend trend-{{ trend.direction }}">
|
|
<span class="trend-delta">{{ trend.delta_text }}</span>
|
|
<span class="trend-meta">{{ trend.meta_text }}</span>
|
|
</div>
|
|
{% else %}
|
|
—
|
|
{% endif %}
|
|
</td>
|
|
<td>{% if plan.official_url %}<a href="{{ plan.official_url }}" target="_blank" rel="noopener">链接</a>{% else %}—{% endif %}</td>
|
|
<td>
|
|
<button type="button" class="btn-inline btn-edit" data-plan-id="{{ plan.id }}">编辑</button>
|
|
<button type="button" class="btn-inline btn-copy" data-plan-id="{{ plan.id }}">复制</button>
|
|
<form method="post" action="{{ url_for('admin_plan_delete', plan_id=plan.id) }}" style="display:inline;" onsubmit="return confirm('确定删除?');">
|
|
<button type="submit" class="btn-delete">删除</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr><td colspan="10">暂无配置。</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
</main>
|
|
<script>
|
|
(function() {
|
|
var form = document.getElementById('plan-form');
|
|
if (!form) return;
|
|
var planIdInput = document.getElementById('plan_id');
|
|
var formTitle = document.getElementById('plan-form-title');
|
|
var submitBtn = document.getElementById('plan-form-submit');
|
|
var cancelBtn = document.getElementById('plan-form-cancel');
|
|
var formActionNew = "{{ url_for('admin_plan_new') }}";
|
|
|
|
function setFormAddMode() {
|
|
planIdInput.value = '';
|
|
form.action = formActionNew;
|
|
formTitle.textContent = '添加配置';
|
|
submitBtn.textContent = '保存';
|
|
form.querySelectorAll('input:not([type="hidden"]), select').forEach(function(el) {
|
|
if (el.name && !el.name.startsWith('from_')) {
|
|
if (el.type === 'checkbox') el.checked = false;
|
|
else el.value = '';
|
|
}
|
|
});
|
|
document.getElementById('countries').value = '';
|
|
}
|
|
|
|
function fillForm(data) {
|
|
document.getElementById('provider_id').value = data.provider_id || '';
|
|
document.getElementById('countries').value = data.countries || '';
|
|
form.querySelectorAll('#country-tags input[type=checkbox]').forEach(function(cb) { cb.checked = false; });
|
|
(data.countries || '').split(',').forEach(function(v) {
|
|
var t = v.trim();
|
|
if (!t) return;
|
|
form.querySelectorAll('.country-tags input[value="' + t + '"]').forEach(function(cb) { cb.checked = true; });
|
|
});
|
|
document.getElementById('vcpu').value = data.vcpu != null ? data.vcpu : '';
|
|
document.getElementById('memory_gb').value = data.memory_gb != null ? data.memory_gb : '';
|
|
document.getElementById('storage_gb').value = data.storage_gb != null ? data.storage_gb : '';
|
|
document.getElementById('bandwidth_mbps').value = data.bandwidth_mbps != null ? data.bandwidth_mbps : '';
|
|
document.getElementById('traffic').value = data.traffic || '';
|
|
document.getElementById('price_cny').value = data.price_cny != null ? data.price_cny : '';
|
|
document.getElementById('price_usd').value = data.price_usd != null ? data.price_usd : '';
|
|
document.getElementById('currency').value = data.currency || 'CNY';
|
|
document.getElementById('official_url').value = data.official_url || '';
|
|
}
|
|
|
|
function loadPlan(planId, isCopy) {
|
|
fetch('/admin/api/plan/' + planId).then(function(r) { return r.json(); }).then(function(data) {
|
|
fillForm(data);
|
|
if (isCopy) {
|
|
planIdInput.value = '';
|
|
form.action = formActionNew;
|
|
formTitle.textContent = '添加配置(已复制)';
|
|
submitBtn.textContent = '保存';
|
|
} else {
|
|
planIdInput.value = planId;
|
|
form.action = '/admin/plan/' + planId + '/edit';
|
|
formTitle.textContent = '编辑配置';
|
|
submitBtn.textContent = '保存修改';
|
|
}
|
|
document.getElementById('plan-form-section').scrollIntoView({ behavior: 'smooth' });
|
|
});
|
|
}
|
|
|
|
document.querySelectorAll('.btn-edit').forEach(function(btn) {
|
|
btn.addEventListener('click', function() { loadPlan(this.getAttribute('data-plan-id'), false); });
|
|
});
|
|
document.querySelectorAll('.btn-copy').forEach(function(btn) {
|
|
btn.addEventListener('click', function() { loadPlan(this.getAttribute('data-plan-id'), true); });
|
|
});
|
|
if (cancelBtn) cancelBtn.addEventListener('click', function() { setFormAddMode(); });
|
|
document.querySelectorAll('#country-tags input[type=checkbox]').forEach(function(cb) {
|
|
cb.addEventListener('change', function() {
|
|
var v = [];
|
|
document.querySelectorAll('#country-tags input:checked').forEach(function(c) { v.push(c.value); });
|
|
document.getElementById('countries').value = v.join(',');
|
|
});
|
|
});
|
|
form.action = formActionNew;
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|