Files
rdgen/rdgenerator/templates/generator.html
T

675 lines
34 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% 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 %}