Files
cnbug 319f683a16 Initial commit: 50 shell-script generators with web UI
- 8 categories / 50 generators covering middleware, databases, runtimes,
  systemd services, system tools, network, monitoring, security
- VNC supports XFCE / GNOME / KDE Plasma / MATE / LXQt desktops
- Node.js versions 18-26 (incl. current LTS 24 Krypton and current 26)
- Live preview, copy / download / multi-script bundle in web UI
- Distro-aware (Ubuntu/Debian/CentOS/RHEL/Rocky/Alma/Fedora)
- All 50 generators pass bash -n syntax check
- Zero pip dependencies (Flask stdlib only)
2026-08-04 00:36:35 +08:00

254 lines
10 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ gen.title }} · Shell-Gen{% endblock %}
{% block body %}
<main class="container">
<nav class="breadcrumb">
<a href="{{ url_for('index') }}">← 返回首页</a>
<span class="cat-icon">{{ category_icons.get(gen.category, '📦') }}</span>
<span class="cat-label">{{ category_labels.get(gen.category, gen.category) }}</span>
</nav>
<header class="gen-header">
<div class="gen-icon">{{ gen.icon }}</div>
<div class="gen-meta">
<h1>{{ gen.title }}</h1>
<p>{{ gen.description }}</p>
<div class="gen-tags">
{% for t in gen.tags %}<span class="tag">#{{ t }}</span>{% endfor %}
{% for o in gen.os_support %}<span class="os">{{ o }}</span>{% endfor %}
{% if gen.dangerous %}<span class="dangerous">⚠️ 危险操作</span>{% endif %}
</div>
</div>
</header>
{% if gen.warnings %}
<div class="warnings">
<h3>⚠️ 注意事项</h3>
<ul>
{% for w in gen.warnings %}<li>{{ w }}</li>{% endfor %}
</ul>
</div>
{% endif %}
<div class="grid-2col">
<section class="form-card">
<h2>📝 参数</h2>
<form id="gen-form" data-gen="{{ gen.id }}">
{% for f in gen.fields %}
<div class="field" data-group="{{ f.group or '' }}">
<label for="f-{{ f.name }}">
{{ f.label }}
{% if f.required %}<span class="req">*</span>{% endif %}
</label>
{% if f.type == 'select' %}
<select id="f-{{ f.name }}" name="{{ f.name }}" {% if f.required %}required{% endif %}>
{% for opt in f.options %}<option value="{{ opt }}" {% if opt == f.default %}selected{% endif %}>{{ opt }}</option>{% endfor %}
</select>
{% elif f.type == 'textarea' %}
<textarea id="f-{{ f.name }}" name="{{ f.name }}" rows="4" placeholder="{{ f.placeholder }}" {% if f.required %}required{% endif %}>{{ f.default }}</textarea>
{% elif f.type == 'checkbox' %}
<label class="checkbox">
<input type="checkbox" id="f-{{ f.name }}" name="{{ f.name }}" value="yes"
{% if f.default in ('yes', 'true', '1', 'on') %}checked{% endif %}>
<span>启用</span>
</label>
{% elif f.type == 'number' %}
<input type="number" id="f-{{ f.name }}" name="{{ f.name }}"
value="{{ f.default }}" placeholder="{{ f.placeholder }}"
min="{{ f.min or '' }}" max="{{ f.max or '' }}" step="{{ f.step or '' }}"
{% if f.required %}required{% endif %}>
{% elif f.type == 'password' %}
<input type="password" id="f-{{ f.name }}" name="{{ f.name }}"
value="{{ f.default }}" placeholder="{{ f.placeholder }}"
autocomplete="new-password">
{% else %}
<input type="text" id="f-{{ f.name }}" name="{{ f.name }}"
value="{{ f.default }}" placeholder="{{ f.placeholder }}"
{% if f.required %}required{% endif %}>
{% endif %}
{% if f.help %}<p class="help">{{ f.help }}</p>{% endif %}
</div>
{% endfor %}
<div class="form-actions">
<button type="submit" class="btn btn-primary" id="btn-preview">👁 预览</button>
<button type="button" class="btn" id="btn-download">💾 下载 .sh</button>
<button type="button" class="btn btn-secondary" id="btn-add-bundle">📦 加入套餐</button>
</div>
</form>
</section>
<section class="preview-card">
<div class="preview-header">
<h2>📄 脚本预览</h2>
<div class="preview-actions">
<button type="button" class="btn btn-small" id="btn-copy">📋 复制</button>
<span class="muted" id="line-count">0 行</span>
</div>
</div>
<pre id="script-pre"><code>// 点击「👁 预览」生成脚本</code></pre>
<div class="copy-toast" id="copy-toast">已复制</div>
<div id="err-box" class="err-box hidden"></div>
</section>
</div>
{% if gen.post_steps or gen.verify_steps %}
<section class="after">
{% if gen.post_steps %}
<h3>🛠 安装后操作</h3>
<ol class="post-steps">
{% for s in gen.post_steps %}<li><code>{{ s }}</code></li>{% endfor %}
</ol>
{% endif %}
{% if gen.verify_steps %}
<h3>✅ 验证</h3>
<ol class="verify-steps">
{% for s in gen.verify_steps %}<li><code>{{ s }}</code></li>{% endfor %}
</ol>
{% endif %}
</section>
{% endif %}
</main>
<div class="bundle-drawer" id="bundle-drawer" hidden>
<div class="bundle-header">
<h3>📦 套餐 (multi-script bundle)</h3>
<span id="bundle-count" class="muted">0 项</span>
<button class="btn btn-small" id="btn-bundle-clear">清空</button>
<button class="btn btn-small btn-primary" id="btn-bundle-download">下载 .zip</button>
<button class="btn btn-small" id="btn-bundle-close"></button>
</div>
<ul id="bundle-list" class="bundle-list"></ul>
</div>
<script>
const GEN_ID = {{ gen.id | tojson }};
const BUNDLE_KEY = "shell-gen-bundle";
function collectForm() {
const form = document.getElementById("gen-form");
const fd = new FormData(form);
const params = {};
for (const [k, v] of fd.entries()) {
if (v !== "" || document.querySelector(`[name="${k}"]`).type === "checkbox") {
params[k] = v;
}
}
// unchecked checkboxes won't be in fd
for (const el of form.querySelectorAll('input[type="checkbox"]')) {
params[el.name] = el.checked ? "yes" : "no";
}
return params;
}
async function preview() {
const params = collectForm();
const r = await fetch("/api/preview", {
method: "POST", headers: {"Content-Type": "application/json"},
body: JSON.stringify({generator: GEN_ID, params})
});
const d = await r.json();
const pre = document.getElementById("script-pre");
const err = document.getElementById("err-box");
if (!d.ok) {
err.textContent = d.error;
err.classList.remove("hidden");
pre.textContent = "// 错误: " + d.error;
return;
}
err.classList.add("hidden");
pre.innerHTML = "<code>" + escapeHtml(d.script) + "</code>";
document.getElementById("line-count").textContent = d.script.split("\n").length + " 行";
}
document.getElementById("gen-form").addEventListener("submit", e => { e.preventDefault(); preview(); });
document.getElementById("btn-download").addEventListener("click", async () => {
const params = collectForm();
const r = await fetch("/api/download", {
method: "POST", headers: {"Content-Type": "application/json"},
body: JSON.stringify({generator: GEN_ID, params})
});
if (!r.ok) { alert("下载失败"); return; }
const blob = await r.blob();
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = GEN_ID + ".sh";
a.click();
URL.revokeObjectURL(a.href);
});
document.getElementById("btn-copy").addEventListener("click", () => {
const txt = document.getElementById("script-pre").innerText;
navigator.clipboard.writeText(txt).then(() => {
const t = document.getElementById("copy-toast");
t.classList.add("show");
setTimeout(() => t.classList.remove("show"), 1500);
});
});
// Bundle drawer
function loadBundle() {
try { return JSON.parse(localStorage.getItem(BUNDLE_KEY) || "[]"); } catch { return []; }
}
function saveBundle(b) { localStorage.setItem(BUNDLE_KEY, JSON.stringify(b)); renderBundle(); }
function renderBundle() {
const list = loadBundle();
document.getElementById("bundle-count").textContent = list.length + " 项";
const ul = document.getElementById("bundle-list");
if (list.length === 0) {
ul.innerHTML = '<li class="empty">还没有添加任何生成器,点击「📦 加入套餐」加入。</li>';
return;
}
ul.innerHTML = list.map((it, i) => `
<li>
<span class="b-id">${escapeHtml(it.generator)}</span>
<span class="b-title">${escapeHtml(it.title || '')}</span>
<button class="btn btn-small" data-rm="${i}">移除</button>
</li>
`).join("");
ul.querySelectorAll("[data-rm]").forEach(b => {
b.addEventListener("click", () => {
const idx = parseInt(b.dataset.rm, 10);
const items = loadBundle();
items.splice(idx, 1);
saveBundle(items);
});
});
}
document.getElementById("btn-add-bundle").addEventListener("click", () => {
const items = loadBundle();
if (items.some(it => it.generator === GEN_ID)) return alert("已在套餐中");
const titleMatch = document.querySelector(".gen-header h1")?.textContent || GEN_ID;
items.push({generator: GEN_ID, params: collectForm(), title: titleMatch});
saveBundle(items);
const drawer = document.getElementById("bundle-drawer");
drawer.hidden = false;
});
document.getElementById("btn-bundle-close").addEventListener("click", () => document.getElementById("bundle-drawer").hidden = true);
document.getElementById("btn-bundle-clear").addEventListener("click", () => saveBundle([]));
document.getElementById("btn-bundle-download").addEventListener("click", async () => {
const items = loadBundle();
if (items.length === 0) return alert("套餐为空");
const r = await fetch("/api/bundle", {
method: "POST", headers: {"Content-Type": "application/json"},
body: JSON.stringify({items})
});
if (!r.ok) return alert("打包失败");
const blob = await r.blob();
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = "shell-gen-bundle.zip";
a.click();
URL.revokeObjectURL(a.href);
});
renderBundle();
// initial preview
preview();
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, c => ({
'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'
}[c]));
}
</script>
{% endblock %}