Initial commit: rdgen with GitHub/Gitea dual build backend
This commit is contained in:
@@ -0,0 +1,674 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}客户端定制 · RustDesk 客户端定制平台{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-head">
|
||||
<h1 class="page-title">定制 RustDesk 客户端</h1>
|
||||
<p class="page-subtitle">填写以下配置并提交,系统将通过 GitHub Actions 自动构建专属客户端,通常需要 30~45 分钟,可在「我的构建」中随时查看进度。</p>
|
||||
</div>
|
||||
|
||||
<form id="myForm" action="/generator" method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
{# ---------- 平台与版本 ---------- #}
|
||||
<div class="card">
|
||||
<div class="card-header"><svg class="icon"><use href="#i-monitor"/></svg> 目标平台与版本</div>
|
||||
<div class="card-body">
|
||||
<div class="platform-grid">
|
||||
<div class="platform-card active" data-platform="windows">
|
||||
<svg class="brand-icon"><use href="#b-windows"/></svg>
|
||||
<div class="platform-name">Windows</div>
|
||||
<div class="platform-tag">64 位</div>
|
||||
</div>
|
||||
<div class="platform-card" data-platform="windows-x86">
|
||||
<svg class="brand-icon"><use href="#b-windows"/></svg>
|
||||
<div class="platform-name">Windows</div>
|
||||
<div class="platform-tag">32 位</div>
|
||||
</div>
|
||||
<div class="platform-card" data-platform="linux">
|
||||
<svg class="brand-icon"><use href="#b-linux"/></svg>
|
||||
<div class="platform-name">Linux</div>
|
||||
<div class="platform-tag">deb / rpm 等</div>
|
||||
</div>
|
||||
<div class="platform-card" data-platform="android">
|
||||
<svg class="brand-icon"><use href="#b-android"/></svg>
|
||||
<div class="platform-name">Android</div>
|
||||
<div class="platform-tag">APK</div>
|
||||
</div>
|
||||
<div class="platform-card" data-platform="macos">
|
||||
<svg class="brand-icon"><use href="#b-apple"/></svg>
|
||||
<div class="platform-name">macOS</div>
|
||||
<div class="platform-tag">dmg</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ form.platform }}
|
||||
|
||||
<div class="grid-2" style="margin-top:20px">
|
||||
<div class="field" style="margin-bottom:0">
|
||||
<label for="{{ form.version.id_for_label }}">RustDesk 版本</label>
|
||||
{{ form.version }}
|
||||
{% if version_source == 'github' %}
|
||||
<div class="version-note is-live">
|
||||
<svg class="icon"><use href="#b-github"/></svg>
|
||||
版本列表来自 GitHub 官方仓库 rustdesk/rustdesk,跟随官方发版自动更新
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="version-note is-fallback">
|
||||
<svg class="icon"><use href="#i-alert"/></svg>
|
||||
官方版本列表暂时获取失败,当前显示内置版本列表;保存配置或配置代理后会自动重试
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.version.help_text %}<div class="help-text">{{ form.version.help_text }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="field field-check-control">
|
||||
<label class="check-item">
|
||||
{{ form.delayFix }} <span>修复使用第三方 API 时的连接延迟问题</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ---------- 配置方案:账号绑定 / 载入 / 另存 / 导入导出 ---------- #}
|
||||
<div class="card">
|
||||
<div class="card-header"><svg class="icon"><use href="#i-save"/></svg> 配置方案(保存 · 载入 · 导入导出)</div>
|
||||
<div class="card-body">
|
||||
<p class="field-hint" style="margin:0 0 14px">
|
||||
把当前配置「另存为方案」后与账号绑定,下次直接选择方案即可填表创建构建任务;
|
||||
也可导出 JSON 本地备份、从 JSON 文件导入,或前往
|
||||
<a href="{% url 'saved_configs' %}">配置文件管理页</a>统一维护。
|
||||
</p>
|
||||
<div class="profile-row">
|
||||
<select id="profileSelect" class="form-control">
|
||||
<option value="">— 选择已保存的配置方案 —</option>
|
||||
{% for p in profiles %}
|
||||
<option value="{{ p.id }}">{{ p.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="button" class="btn btn-secondary" id="loadProfileBtn">
|
||||
<svg class="icon"><use href="#i-zap"/></svg> 载入方案
|
||||
</button>
|
||||
<a class="btn btn-secondary" href="{% url 'saved_configs' %}">
|
||||
<svg class="icon"><use href="#i-list"/></svg> 管理配置文件
|
||||
</a>
|
||||
</div>
|
||||
<div class="profile-row">
|
||||
<input type="text" id="profileName" class="form-control" maxlength="100"
|
||||
placeholder="方案名称,如:公司标准-Windows 版">
|
||||
<button type="button" class="btn btn-primary" id="saveProfileBtn">
|
||||
<svg class="icon"><use href="#i-save"/></svg> 另存当前配置为方案
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="saveFormData()">
|
||||
<svg class="icon"><use href="#i-file-down"/></svg> 导出为 JSON 文件
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="loadFormData()">
|
||||
<svg class="icon"><use href="#i-file-up"/></svg> 从 JSON 文件导入
|
||||
</button>
|
||||
<input type="file" id="fileInput" style="display:none;" accept=".json,application/json">
|
||||
</div>
|
||||
<div id="profileMsg"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-2">
|
||||
{# ---------- 常规 ---------- #}
|
||||
<div class="card">
|
||||
<div class="card-header"><svg class="icon"><use href="#i-settings"/></svg> 常规设置</div>
|
||||
<div class="card-body">
|
||||
<div class="field">
|
||||
<label for="{{ form.exename.id_for_label }}">{{ form.exename.label }}</label>
|
||||
{{ form.exename }}
|
||||
<span id="filenameError" class="inline-error"></span>
|
||||
{{ form.exename.errors }}
|
||||
{% if form.exename.help_text %}<div class="field-hint">{{ form.exename.help_text }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.appname.id_for_label }}">{{ form.appname.label }}</label>
|
||||
{{ form.appname }}
|
||||
{{ form.appname.errors }}
|
||||
{% if form.appname.help_text %}<div class="field-hint">{{ form.appname.help_text }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="field radio-group">
|
||||
<label class="field-label">{{ form.direction.label }}</label>
|
||||
{{ form.direction }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.installation.id_for_label }}">{{ form.installation.label }}</label>
|
||||
{{ form.installation }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.settings.id_for_label }}">{{ form.settings.label }}</label>
|
||||
{{ form.settings }}
|
||||
</div>
|
||||
<div class="field" style="margin-bottom:0">
|
||||
<label for="{{ form.androidappid.id_for_label }}">{{ form.androidappid.label }}</label>
|
||||
{{ form.androidappid }}
|
||||
{% if form.androidappid.help_text %}<div class="field-hint">{{ form.androidappid.help_text }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ---------- 自定义服务器 ---------- #}
|
||||
<div class="card">
|
||||
<div class="card-header"><svg class="icon"><use href="#i-server"/></svg> 自定义服务器</div>
|
||||
<div class="card-body">
|
||||
<div class="grid-2">
|
||||
<div class="field">
|
||||
<label for="{{ form.serverIP.id_for_label }}">{{ form.serverIP.label }}</label>
|
||||
{{ form.serverIP }}
|
||||
{% if form.serverIP.help_text %}<div class="field-hint">{{ form.serverIP.help_text }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.serverPort.id_for_label }}">{{ form.serverPort.label }}</label>
|
||||
{{ form.serverPort }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.apiServer.id_for_label }}">{{ form.apiServer.label }}</label>
|
||||
{{ form.apiServer }}
|
||||
{% if form.apiServer.help_text %}<div class="field-hint">{{ form.apiServer.help_text }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.key.id_for_label }}">{{ form.key.label }}</label>
|
||||
{{ form.key }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.urlLink.id_for_label }}">{{ form.urlLink.label }}</label>
|
||||
{{ form.urlLink }}
|
||||
{% if form.urlLink.help_text %}<div class="field-hint">{{ form.urlLink.help_text }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.downloadLink.id_for_label }}">{{ form.downloadLink.label }}</label>
|
||||
{{ form.downloadLink }}
|
||||
{% if form.downloadLink.help_text %}<div class="field-hint">{{ form.downloadLink.help_text }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="field" style="margin-bottom:0">
|
||||
<label for="{{ form.compname.id_for_label }}">{{ form.compname.label }}</label>
|
||||
{{ form.compname }}
|
||||
{{ form.compname.errors }}
|
||||
{% if form.compname.help_text %}<div class="field-hint">{{ form.compname.help_text }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-2">
|
||||
{# ---------- 安全 ---------- #}
|
||||
<div class="card">
|
||||
<div class="card-header"><svg class="icon"><use href="#i-shield"/></svg> 安全设置</div>
|
||||
<div class="card-body">
|
||||
<div class="field">
|
||||
<label for="{{ form.passApproveMode.id_for_label }}">{{ form.passApproveMode.label }}</label>
|
||||
{{ form.passApproveMode }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.permanentPassword.id_for_label }}">{{ form.permanentPassword.label }}</label>
|
||||
{{ form.permanentPassword }}
|
||||
{% if form.permanentPassword.help_text %}<div class="field-hint">{{ form.permanentPassword.help_text }}</div>{% endif %}
|
||||
</div>
|
||||
<div id="passwordRequirement" class="inline-warning">使用「隐藏连接窗口」功能时,请先设置固定访问密码。</div>
|
||||
<label class="check-item" for="{{ form.denyLan.id_for_label }}">{{ form.denyLan }} <span>禁止局域网发现</span></label>
|
||||
<label class="check-item" for="{{ form.enableDirectIP.id_for_label }}">{{ form.enableDirectIP }} <span>允许直接 IP 直连</span></label>
|
||||
<label class="check-item" for="{{ form.autoClose.id_for_label }}">{{ form.autoClose }} <span>用户无操作时自动断开入站会话</span></label>
|
||||
<label class="check-item" for="{{ form.hidecm.id_for_label }}">{{ form.hidecm }} <span>允许在远程画面中隐藏连接窗口</span></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ---------- 外观 ---------- #}
|
||||
<div class="card">
|
||||
<div class="card-header"><svg class="icon"><use href="#i-image"/></svg> 外观定制</div>
|
||||
<div class="card-body">
|
||||
{{ form.iconbase64.as_hidden }}
|
||||
{{ form.logobase64.as_hidden }}
|
||||
{{ form.privacybase64.as_hidden }}
|
||||
<div class="field">
|
||||
<label for="{{ form.iconfile.id_for_label }}">{{ form.iconfile.label }}</label>
|
||||
{{ form.iconfile }}
|
||||
{{ form.iconfile.errors }}
|
||||
{% if form.iconfile.help_text %}<div class="field-hint">{{ form.iconfile.help_text }}</div>{% endif %}
|
||||
<div id="icon-preview" class="image-preview"></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.logofile.id_for_label }}">{{ form.logofile.label }}</label>
|
||||
{{ form.logofile }}
|
||||
{% if form.logofile.help_text %}<div class="field-hint">{{ form.logofile.help_text }}</div>{% endif %}
|
||||
<div id="logo-preview" class="image-preview"></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.privacyfile.id_for_label }}">{{ form.privacyfile.label }}</label>
|
||||
{{ form.privacyfile }}
|
||||
{% if form.privacyfile.help_text %}<div class="field-hint">{{ form.privacyfile.help_text }}</div>{% endif %}
|
||||
<div id="privacy-preview" class="image-preview photo"></div>
|
||||
</div>
|
||||
<div class="grid-2">
|
||||
<div class="field" style="margin-bottom:0">
|
||||
<label for="{{ form.theme.id_for_label }}">主题</label>
|
||||
{{ form.theme }}
|
||||
</div>
|
||||
<div class="field" style="margin-bottom:0">
|
||||
<label for="{{ form.themeDorO.id_for_label }}">主题生效方式</label>
|
||||
{{ form.themeDorO }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-hint">「默认」仅设定初始主题,客户端用户可自行更改;「强制锁定」后用户无法更改主题。</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-2">
|
||||
{# ---------- 权限 ---------- #}
|
||||
<div class="card">
|
||||
<div class="card-header"><svg class="icon"><use href="#i-lock"/></svg> 权限设置</div>
|
||||
<div class="card-body">
|
||||
<div class="field">
|
||||
<label for="{{ form.permissionsDorO.id_for_label }}">权限生效方式</label>
|
||||
{{ form.permissionsDorO }}
|
||||
<div class="field-hint">「默认」用户仍可自行修改;「强制锁定」后用户无法修改以下权限。</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.permissionsType.id_for_label }}">{{ form.permissionsType.label }}</label>
|
||||
{{ form.permissionsType }}
|
||||
</div>
|
||||
<div class="check-grid">
|
||||
<label class="check-item" for="{{ form.enableKeyboard.id_for_label }}">{{ form.enableKeyboard }} <span>键盘 / 鼠标控制</span></label>
|
||||
<label class="check-item" for="{{ form.enableClipboard.id_for_label }}">{{ form.enableClipboard }} <span>剪贴板同步</span></label>
|
||||
<label class="check-item" for="{{ form.enableFileTransfer.id_for_label }}">{{ form.enableFileTransfer }} <span>文件传输</span></label>
|
||||
<label class="check-item" for="{{ form.enableAudio.id_for_label }}">{{ form.enableAudio }} <span>音频传输</span></label>
|
||||
<label class="check-item" for="{{ form.enableTCP.id_for_label }}">{{ form.enableTCP }} <span>TCP 隧道</span></label>
|
||||
<label class="check-item" for="{{ form.enableRemoteRestart.id_for_label }}">{{ form.enableRemoteRestart }} <span>远程重启</span></label>
|
||||
<label class="check-item" for="{{ form.enableRecording.id_for_label }}">{{ form.enableRecording }} <span>会话录制</span></label>
|
||||
<label class="check-item" for="{{ form.enableBlockingInput.id_for_label }}">{{ form.enableBlockingInput }} <span>屏蔽被控端键鼠</span></label>
|
||||
<label class="check-item" for="{{ form.enableRemoteModi.id_for_label }}">{{ form.enableRemoteModi }} <span>允许修改远程配置</span></label>
|
||||
<label class="check-item" for="{{ form.enablePrinter.id_for_label }}">{{ form.enablePrinter }} <span>远程打印机</span></label>
|
||||
<label class="check-item" for="{{ form.enableCamera.id_for_label }}">{{ form.enableCamera }} <span>远程摄像头</span></label>
|
||||
<label class="check-item" for="{{ form.enableTerminal.id_for_label }}">{{ form.enableTerminal }} <span>远程终端</span></label>
|
||||
</div>
|
||||
|
||||
<div class="subsection-title"><svg class="icon"><use href="#i-zap"/></svg> 定制功能</div>
|
||||
<label class="check-item" for="{{ form.xOffline.id_for_label }}">{{ form.xOffline }} <span>通讯录中离线设备显示 X 标记</span></label>
|
||||
<label class="check-item" for="{{ form.removeNewVersionNotif.id_for_label }}">{{ form.removeNewVersionNotif }} <span>移除新版本更新通知</span></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ---------- 其他 / 高级 ---------- #}
|
||||
<div class="card">
|
||||
<div class="card-header"><svg class="icon"><use href="#i-sliders"/></svg> 其他与高级设置</div>
|
||||
<div class="card-body">
|
||||
<label class="check-item" for="{{ form.removeWallpaper.id_for_label }}">{{ form.removeWallpaper }} <span>入站会话期间移除桌面壁纸</span></label>
|
||||
|
||||
<p class="field-hint" style="margin:18px 0 10px">
|
||||
高级设置项参考:
|
||||
<a href="https://rustdesk.com/docs/en/self-host/client-configuration/advanced-settings/" target="_blank" rel="noopener">RustDesk 官方文档
|
||||
<svg class="icon" style="font-size:12px"><use href="#i-external"/></svg>
|
||||
</a>
|
||||
</p>
|
||||
<div class="field">
|
||||
<label for="{{ form.defaultManual.id_for_label }}">{{ form.defaultManual.label }}</label>
|
||||
{{ form.defaultManual }}
|
||||
{% if form.defaultManual.help_text %}<div class="field-hint">{{ form.defaultManual.help_text }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="field" style="margin-bottom:0">
|
||||
<label for="{{ form.overrideManual.id_for_label }}">{{ form.overrideManual.label }}</label>
|
||||
{{ form.overrideManual }}
|
||||
{% if form.overrideManual.help_text %}<div class="field-hint">{{ form.overrideManual.help_text }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body" style="text-align:center">
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" style="min-width:240px">
|
||||
<svg class="icon"><use href="#i-send"/></svg> 开始生成自定义客户端
|
||||
</button>
|
||||
<p class="field-hint" style="margin-top:12px">提交后将自动跳转到构建进度页,构建期间可关闭页面,稍后在「我的构建」中回来下载。</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block head_extra %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
/* ---------- 平台卡片选择 ---------- */
|
||||
const platformSelect = document.getElementById('id_platform');
|
||||
document.querySelectorAll('.platform-card').forEach(card => {
|
||||
card.addEventListener('click', function () {
|
||||
document.querySelectorAll('.platform-card').forEach(c => c.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
platformSelect.value = this.dataset.platform;
|
||||
});
|
||||
});
|
||||
// 回显当前选中平台
|
||||
const current = platformSelect.value;
|
||||
document.querySelectorAll('.platform-card').forEach(c => {
|
||||
if (c.dataset.platform === current) {
|
||||
document.querySelectorAll('.platform-card').forEach(x => x.classList.remove('active'));
|
||||
c.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
/* ---------- 图片预览 ---------- */
|
||||
function previewImage(input, previewContainerId) {
|
||||
const box = document.getElementById(previewContainerId);
|
||||
if (input.files && input.files[0]) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
box.innerHTML = '';
|
||||
const img = document.createElement('img');
|
||||
img.src = e.target.result;
|
||||
box.appendChild(img);
|
||||
};
|
||||
reader.readAsDataURL(input.files[0]);
|
||||
}
|
||||
}
|
||||
document.getElementById("{{ form.iconfile.id_for_label }}").addEventListener('change', e => previewImage(e.target, 'icon-preview'));
|
||||
document.getElementById("{{ form.logofile.id_for_label }}").addEventListener('change', e => previewImage(e.target, 'logo-preview'));
|
||||
document.getElementById("{{ form.privacyfile.id_for_label }}").addEventListener('change', e => previewImage(e.target, 'privacy-preview'));
|
||||
|
||||
/* ---------- 隐藏连接窗口联动 ---------- */
|
||||
const hidecm = document.getElementById("{{ form.hidecm.id_for_label }}");
|
||||
const passApproveMode = document.getElementById("{{ form.passApproveMode.id_for_label }}");
|
||||
const permanentPassword = document.getElementById("{{ form.permanentPassword.id_for_label }}");
|
||||
hidecm.addEventListener('change', function () {
|
||||
const tip = document.getElementById('passwordRequirement');
|
||||
if (this.checked) {
|
||||
tip.style.display = 'block';
|
||||
permanentPassword.focus();
|
||||
passApproveMode.value = 'password';
|
||||
} else {
|
||||
tip.style.display = 'none';
|
||||
passApproveMode.value = 'password-click';
|
||||
}
|
||||
});
|
||||
|
||||
/* ---------- 权限预设联动 ---------- */
|
||||
const permissionChecks = [
|
||||
"{{ form.enableKeyboard.id_for_label }}", "{{ form.enableClipboard.id_for_label }}",
|
||||
"{{ form.enableFileTransfer.id_for_label }}", "{{ form.enableAudio.id_for_label }}",
|
||||
"{{ form.enableTCP.id_for_label }}", "{{ form.enableRemoteRestart.id_for_label }}",
|
||||
"{{ form.enableRecording.id_for_label }}", "{{ form.enableBlockingInput.id_for_label }}",
|
||||
"{{ form.enableRemoteModi.id_for_label }}", "{{ form.enablePrinter.id_for_label }}",
|
||||
"{{ form.enableCamera.id_for_label }}", "{{ form.enableTerminal.id_for_label }}"
|
||||
].map(id => document.getElementById(id));
|
||||
|
||||
function setChecks(checked, disabled) {
|
||||
permissionChecks.forEach(cb => { cb.checked = checked; cb.disabled = disabled; });
|
||||
}
|
||||
document.getElementById("{{ form.permissionsType.id_for_label }}").addEventListener('change', function () {
|
||||
if (this.value === 'full') {
|
||||
setChecks(true, true);
|
||||
} else if (this.value === 'view') {
|
||||
setChecks(false, true);
|
||||
} else {
|
||||
// 自定义:恢复可编辑,不改变当前勾选
|
||||
permissionChecks.forEach(cb => { cb.disabled = false; });
|
||||
}
|
||||
});
|
||||
|
||||
/* ---------- 重复提交保护 ---------- */
|
||||
document.getElementById('myForm').addEventListener('submit', function () {
|
||||
const btn = document.getElementById('submitBtn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = '正在提交构建任务…';
|
||||
});
|
||||
|
||||
/* ---------- 配置数据收集 / 填充(账号方案、JSON 导入导出共用) ---------- */
|
||||
const form = document.getElementById('myForm');
|
||||
|
||||
async function readFileAsBase64(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => resolve(e.target.result);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
// 导入配置中遇到未知字段时原样保留,避免另存时丢失
|
||||
let carriedUnknownFields = {};
|
||||
|
||||
// 收集当前表单为可序列化 JSON(图片转 base64 data URL)
|
||||
async function collectFormData() {
|
||||
const formData = {};
|
||||
new FormData(form).forEach((value, key) => {
|
||||
// File 对象序列化为 {},需跳过并在下面单独读取
|
||||
if (value instanceof File) return;
|
||||
formData[key] = value;
|
||||
});
|
||||
|
||||
form.querySelectorAll('select').forEach(sel => { formData[sel.name] = sel.value; });
|
||||
form.querySelectorAll('input[type="checkbox"]').forEach(input => {
|
||||
if (input.name) { formData[input.name] = input.checked; }
|
||||
});
|
||||
form.querySelectorAll('input[type="radio"]:checked').forEach(input => {
|
||||
if (input.name) { formData[input.name] = input.value; }
|
||||
});
|
||||
|
||||
// 图片:优先取已预览的 base64,其次读取新选择的文件
|
||||
async function collectImage(fieldName, previewId, fileInputId) {
|
||||
const preview = document.getElementById(previewId);
|
||||
if (preview.firstChild && preview.firstChild.src && preview.firstChild.src.startsWith('data:image/')) {
|
||||
formData[fieldName] = preview.firstChild.src;
|
||||
} else {
|
||||
const file = document.getElementById(fileInputId).files[0];
|
||||
if (file) { formData[fieldName] = await readFileAsBase64(file); }
|
||||
}
|
||||
}
|
||||
await collectImage('iconfile', 'icon-preview', "{{ form.iconfile.id_for_label }}");
|
||||
await collectImage('logofile', 'logo-preview', "{{ form.logofile.id_for_label }}");
|
||||
await collectImage('privacyfile', 'privacy-preview', "{{ form.privacyfile.id_for_label }}");
|
||||
|
||||
for (const k in carriedUnknownFields) {
|
||||
if (!Object.prototype.hasOwnProperty.call(formData, k)) formData[k] = carriedUnknownFields[k];
|
||||
}
|
||||
return formData;
|
||||
}
|
||||
|
||||
// 导出为本地 JSON 文件(沿用配置名称作为文件名)
|
||||
window.saveFormData = async function () {
|
||||
const filenameInput = document.getElementById("{{ form.exename.id_for_label }}");
|
||||
const filename = filenameInput.value.trim();
|
||||
const errorBox = document.getElementById('filenameError');
|
||||
if (!filename) {
|
||||
errorBox.textContent = '请先填写配置名称。';
|
||||
filenameInput.focus();
|
||||
return;
|
||||
}
|
||||
errorBox.textContent = '';
|
||||
|
||||
const formData = await collectFormData();
|
||||
const blob = new Blob([JSON.stringify(formData, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename + '.json';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
function showLoadWarnings(keys) {
|
||||
let banner = document.getElementById('loadWarnings');
|
||||
if (!keys.length) {
|
||||
if (banner) banner.remove();
|
||||
return;
|
||||
}
|
||||
if (!banner) {
|
||||
banner = document.createElement('div');
|
||||
banner.id = 'loadWarnings';
|
||||
banner.className = 'alert alert-warning';
|
||||
form.parentNode.insertBefore(banner, form);
|
||||
}
|
||||
banner.textContent = '以下字段包含无效值,已被忽略:' + keys.join('、') + '。';
|
||||
}
|
||||
|
||||
function fillPreview(key, fieldId, previewId) {
|
||||
// 兼容历史导出中 iconbase64 命名
|
||||
const hidden = document.getElementById(fieldId);
|
||||
if (hidden) hidden.value = key;
|
||||
document.getElementById(previewId).innerHTML =
|
||||
'<img src="' + key + '" alt="预览图">';
|
||||
}
|
||||
|
||||
// 用 JSON 数据填充整个表单,返回被忽略的异常字段名列表
|
||||
function fillFormFromData(data) {
|
||||
carriedUnknownFields = {};
|
||||
const skippedKeys = [];
|
||||
for (const key in data) {
|
||||
const elements = document.querySelectorAll('[name="' + key + '"], [id="' + key + '"]');
|
||||
if (elements.length > 0) {
|
||||
elements.forEach(el => {
|
||||
const v = data[key];
|
||||
if ((typeof v === 'object' && v !== null) || v === '[object Object]') {
|
||||
if (!skippedKeys.includes(key)) skippedKeys.push(key);
|
||||
return;
|
||||
}
|
||||
if (el.type === 'radio') {
|
||||
el.checked = (el.value === String(v));
|
||||
} else if (el.type === 'checkbox') {
|
||||
el.checked = !!v;
|
||||
} else if (el.type !== 'file') {
|
||||
el.value = v;
|
||||
}
|
||||
});
|
||||
} else if (['string', 'number', 'boolean'].includes(typeof v)) {
|
||||
carriedUnknownFields[key] = v;
|
||||
}
|
||||
}
|
||||
|
||||
// 图片字段(兼容 iconfile / iconbase64 两种命名)
|
||||
const imgFields = [
|
||||
['iconfile', 'id_iconbase64', 'icon-preview'],
|
||||
['logofile', 'id_logobase64', 'logo-preview'],
|
||||
['privacyfile', 'id_privacybase64', 'privacy-preview'],
|
||||
];
|
||||
imgFields.forEach(([jsonKey, hiddenId, previewId]) => {
|
||||
const v = data[jsonKey] || data[hiddenId];
|
||||
if (v && typeof v === 'string' && v.startsWith('data:image/')) {
|
||||
fillPreview(v, hiddenId, previewId);
|
||||
}
|
||||
});
|
||||
|
||||
// 同步联动控件状态
|
||||
document.querySelectorAll('.platform-card').forEach(c => {
|
||||
c.classList.toggle('active', c.dataset.platform === platformSelect.value);
|
||||
});
|
||||
document.getElementById("{{ form.permissionsType.id_for_label }}").dispatchEvent(new Event('change'));
|
||||
return skippedKeys;
|
||||
}
|
||||
|
||||
window.loadFormData = function () {
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
fileInput.value = '';
|
||||
fileInput.click();
|
||||
fileInput.onchange = function (event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(e.target.result);
|
||||
} catch (err) {
|
||||
alert('加载失败:配置文件不是有效的 JSON。');
|
||||
return;
|
||||
}
|
||||
showLoadWarnings(fillFormFromData(data));
|
||||
showProfileMsg('已从 JSON 文件导入配置,可直接提交构建或继续修改。', true);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
};
|
||||
|
||||
/* ---------- 账号绑定的配置方案 ---------- */
|
||||
const profileSelect = document.getElementById('profileSelect');
|
||||
const profileName = document.getElementById('profileName');
|
||||
const profileMsg = document.getElementById('profileMsg');
|
||||
const csrfInput = document.querySelector('[name=csrfmiddlewaretoken]').value;
|
||||
|
||||
function showProfileMsg(text, ok) {
|
||||
profileMsg.className = 'profile-msg ' + (ok ? 'is-ok' : 'is-bad');
|
||||
profileMsg.textContent = text;
|
||||
}
|
||||
|
||||
async function loadProfileById(id) {
|
||||
try {
|
||||
const resp = await fetch('{% url "saved_configs" %}' + id + '/data/');
|
||||
if (!resp.ok) {
|
||||
showProfileMsg('方案不存在或已被删除,请刷新页面后重试。', false);
|
||||
profileSelect.value = '';
|
||||
return;
|
||||
}
|
||||
const json = await resp.json();
|
||||
showLoadWarnings(fillFormFromData(json.data));
|
||||
profileSelect.value = id;
|
||||
showProfileMsg('已载入方案「' + json.name + '」,可直接提交构建或继续修改。', true);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
} catch (e) {
|
||||
showProfileMsg('载入方案失败:' + e, false);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('loadProfileBtn').addEventListener('click', function () {
|
||||
const id = profileSelect.value;
|
||||
if (!id) { showProfileMsg('请先在下拉框中选择一个配置方案。', false); return; }
|
||||
loadProfileById(id);
|
||||
});
|
||||
|
||||
document.getElementById('saveProfileBtn').addEventListener('click', async function () {
|
||||
const btn = this;
|
||||
const name = profileName.value.trim();
|
||||
if (!name) {
|
||||
showProfileMsg('请先在左侧输入方案名称。', false);
|
||||
profileName.focus();
|
||||
return;
|
||||
}
|
||||
const payload = { name: name, data: await collectFormData(), overwrite: false };
|
||||
btn.disabled = true;
|
||||
const originalHtml = btn.innerHTML;
|
||||
btn.textContent = '正在保存…';
|
||||
|
||||
async function submit(overwrite) {
|
||||
payload.overwrite = overwrite;
|
||||
const resp = await fetch('{% url "save_config" %}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfInput },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const json = await resp.json().catch(() => ({}));
|
||||
if (resp.ok && json.ok) {
|
||||
let option = profileSelect.querySelector('option[value="' + json.id + '"]');
|
||||
if (!option) {
|
||||
option = document.createElement('option');
|
||||
option.value = json.id;
|
||||
profileSelect.appendChild(option);
|
||||
}
|
||||
option.textContent = name;
|
||||
profileSelect.value = json.id;
|
||||
showProfileMsg(json.overwritten ? '方案「' + name + '」已更新。' : '方案「' + name + '」已保存,下次可直接选择载入。', true);
|
||||
} else if (resp.status === 409 && json.duplicate) {
|
||||
if (window.confirm('已存在同名方案「' + name + '」,是否用当前配置覆盖它?')) {
|
||||
return submit(true);
|
||||
}
|
||||
} else {
|
||||
showProfileMsg(json.message || '保存失败,请稍后重试。', false);
|
||||
}
|
||||
}
|
||||
|
||||
submit(false).catch(e => showProfileMsg('保存失败:' + e, false)).finally(() => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalHtml;
|
||||
});
|
||||
});
|
||||
|
||||
// 从「配置文件管理」页点「载入并构建」跳转过来时自动填充
|
||||
const autoConfigId = new URLSearchParams(window.location.search).get('config');
|
||||
if (autoConfigId) {
|
||||
loadProfileById(autoConfigId);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user