add:初次提交
This commit is contained in:
486
lj360/lj360_config.js
Normal file
486
lj360/lj360_config.js
Normal file
@@ -0,0 +1,486 @@
|
||||
// ===================== 常量配置 =====================
|
||||
const CONFIG_DIR = "/home/ztl/LJ360/config";
|
||||
const LOGO_DIR = CONFIG_DIR + "/logos";
|
||||
const FLASH_SCRIPT = "/home/ztl/LJ360/bash/flash_logo.sh";
|
||||
|
||||
// ===================== 工具函数 =====================
|
||||
function runCmd(args, callback) {
|
||||
const proc = cockpit.spawn(args, { superuser: "require", err: "out" });
|
||||
let output = "";
|
||||
proc.stream(data => { output += data; });
|
||||
proc.then(() => callback(output, null));
|
||||
proc.catch(err => callback(output, err));
|
||||
}
|
||||
|
||||
function setLogoOutput(text) {
|
||||
const box = document.getElementById("logo-output");
|
||||
if (box) {
|
||||
box.textContent = text;
|
||||
box.scrollTop = box.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
function appendLogoOutput(text) {
|
||||
const box = document.getElementById("logo-output");
|
||||
if (box) {
|
||||
box.textContent += text;
|
||||
box.scrollTop = box.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
function showUploadStatus(msg, type) {
|
||||
const el = document.getElementById("upload-status");
|
||||
if (!el) return;
|
||||
el.textContent = msg;
|
||||
el.className = "upload-status " + type;
|
||||
el.classList.remove("hidden");
|
||||
if (type === "success") {
|
||||
setTimeout(() => el.classList.add("hidden"), 4000);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 标签页切换 =====================
|
||||
function initTabs() {
|
||||
const tabItems = document.querySelectorAll(".pf-c-tabs__item");
|
||||
tabItems.forEach(item => {
|
||||
const btn = item.querySelector(".pf-c-tabs__link");
|
||||
btn.addEventListener("click", () => {
|
||||
// 切换激活样式
|
||||
tabItems.forEach(i => i.classList.remove("pf-m-current"));
|
||||
item.classList.add("pf-m-current");
|
||||
|
||||
// 切换内容面板
|
||||
const targetId = item.getAttribute("data-tab");
|
||||
document.querySelectorAll(".tab-panel").forEach(panel => {
|
||||
panel.classList.add("hidden");
|
||||
});
|
||||
const target = document.getElementById(targetId);
|
||||
if (target) target.classList.remove("hidden");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ===================== 确保目录存在 =====================
|
||||
function ensureDir(callback) {
|
||||
runCmd(["mkdir", "-p", LOGO_DIR], (out, err) => {
|
||||
if (err) {
|
||||
setLogoOutput("❌ 无法创建配置目录: " + (err.message || err));
|
||||
} else {
|
||||
if (callback) callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ===================== Logo 列表 =====================
|
||||
let logoFiles = []; // 缓存当前文件列表
|
||||
|
||||
function refreshLogoList() {
|
||||
const listEl = document.getElementById("logo-list");
|
||||
const selectEl = document.getElementById("flash-logo-select");
|
||||
|
||||
listEl.innerHTML = '<div class="logo-list-empty"><i class="fa fa-spinner fa-spin"></i> 正在加载...</div>';
|
||||
|
||||
ensureDir(() => {
|
||||
runCmd(["bash", "-c", `ls -lh "${LOGO_DIR}"/*.bmp 2>/dev/null || echo "__EMPTY__"`], (out, err) => {
|
||||
logoFiles = [];
|
||||
|
||||
if (err || !out || out.trim() === "__EMPTY__" || out.trim() === "") {
|
||||
listEl.innerHTML = '<div class="logo-list-empty"><i class="fa fa-inbox"></i> 暂无 Logo 文件,请上传</div>';
|
||||
selectEl.innerHTML = '<option value="">-- 暂无可用文件 --</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析 ls -lh 输出
|
||||
const lines = out.trim().split("\n").filter(l => l && !l.startsWith("total"));
|
||||
lines.forEach(line => {
|
||||
// ls -lh 格式: -rw-r--r-- 1 root root 1.2M May 1 12:00 /path/to/file.bmp
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts.length >= 9) {
|
||||
const filePath = parts.slice(8).join(" ");
|
||||
const fileName = filePath.split("/").pop();
|
||||
const fileSize = parts[4];
|
||||
const fileDate = parts[5] + " " + parts[6] + " " + parts[7];
|
||||
logoFiles.push({ path: filePath, name: fileName, size: fileSize, date: fileDate });
|
||||
}
|
||||
});
|
||||
|
||||
if (logoFiles.length === 0) {
|
||||
listEl.innerHTML = '<div class="logo-list-empty"><i class="fa fa-inbox"></i> 暂无 Logo 文件,请上传</div>';
|
||||
selectEl.innerHTML = '<option value="">-- 暂无可用文件 --</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
// 渲染列表
|
||||
listEl.innerHTML = "";
|
||||
selectEl.innerHTML = '<option value="">-- 请选择 Logo 文件 --</option>';
|
||||
|
||||
logoFiles.forEach(file => {
|
||||
// 列表项
|
||||
const item = document.createElement("div");
|
||||
item.className = "logo-list-item";
|
||||
item.innerHTML = `
|
||||
<div class="logo-item-thumb" id="thumb-${CSS.escape(file.name)}">
|
||||
<span class="logo-item-thumb-placeholder"><i class="fa fa-image"></i></span>
|
||||
</div>
|
||||
<div class="logo-item-info">
|
||||
<div class="logo-item-name" title="${file.name}">${file.name}</div>
|
||||
<div class="logo-item-meta">${file.size} · ${file.date}</div>
|
||||
</div>
|
||||
<div class="logo-item-actions">
|
||||
<button class="pf-c-button pf-m-secondary btn-sm btn-preview" data-path="${file.path}" data-name="${file.name}">
|
||||
<i class="fa fa-eye"></i> 预览
|
||||
</button>
|
||||
<button class="pf-c-button pf-m-secondary btn-sm btn-select-flash" data-name="${file.name}">
|
||||
<i class="fa fa-bolt"></i> 选为烧写
|
||||
</button>
|
||||
<button class="pf-c-button pf-m-secondary btn-sm btn-delete" data-path="${file.path}" data-name="${file.name}">
|
||||
<i class="fa fa-trash-o"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
listEl.appendChild(item);
|
||||
|
||||
// 下拉选项
|
||||
const opt = document.createElement("option");
|
||||
opt.value = file.name;
|
||||
opt.textContent = file.name + " (" + file.size + ")";
|
||||
selectEl.appendChild(opt);
|
||||
});
|
||||
|
||||
// 绑定列表按钮事件
|
||||
bindListItemEvents();
|
||||
|
||||
// 加载缩略图(读取文件转 base64 显示)
|
||||
logoFiles.forEach(file => loadThumb(file));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function bindListItemEvents() {
|
||||
// 预览按钮
|
||||
document.querySelectorAll(".btn-preview").forEach(btn => {
|
||||
btn.addEventListener("click", function() {
|
||||
const path = this.getAttribute("data-path");
|
||||
const name = this.getAttribute("data-name");
|
||||
previewLogoFile(path, name);
|
||||
});
|
||||
});
|
||||
|
||||
// 选为烧写按钮
|
||||
document.querySelectorAll(".btn-select-flash").forEach(btn => {
|
||||
btn.addEventListener("click", function() {
|
||||
const name = this.getAttribute("data-name");
|
||||
const sel = document.getElementById("flash-logo-select");
|
||||
if (sel) {
|
||||
sel.value = name;
|
||||
updateFlashButton();
|
||||
// 滚动到烧写区域
|
||||
sel.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 删除按钮
|
||||
document.querySelectorAll(".btn-delete").forEach(btn => {
|
||||
btn.addEventListener("click", function() {
|
||||
const path = this.getAttribute("data-path");
|
||||
const name = this.getAttribute("data-name");
|
||||
if (confirm("确定要删除 " + name + " 吗?此操作不可恢复。")) {
|
||||
deleteLogo(path);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ===================== 缩略图加载 =====================
|
||||
function loadThumb(file) {
|
||||
// 使用 cockpit.file 读取 BMP 文件,转为 base64 显示
|
||||
const fileHandle = cockpit.file(file.path, { binary: true });
|
||||
fileHandle.read().then(content => {
|
||||
fileHandle.close();
|
||||
if (!content) return;
|
||||
// 将 Uint8Array 转为 base64
|
||||
const b64 = uint8ToBase64(content);
|
||||
const dataUrl = "data:image/bmp;base64," + b64;
|
||||
|
||||
// 设置缩略图
|
||||
const thumbEl = document.getElementById("thumb-" + CSS.escape(file.name));
|
||||
if (thumbEl) {
|
||||
thumbEl.innerHTML = `<img src="${dataUrl}" alt="${file.name}" style="max-width:100%;max-height:100%;object-fit:contain;">`;
|
||||
}
|
||||
}).catch(() => {
|
||||
fileHandle.close();
|
||||
});
|
||||
}
|
||||
|
||||
// ===================== 预览 Logo =====================
|
||||
function previewLogoFile(path, name) {
|
||||
setLogoOutput("⏳ 正在读取 " + name + " ...");
|
||||
const imgEl = document.getElementById("logo-preview-img");
|
||||
const placeholder = document.getElementById("logo-placeholder");
|
||||
|
||||
const fileHandle = cockpit.file(path, { binary: true });
|
||||
fileHandle.read().then(content => {
|
||||
fileHandle.close();
|
||||
if (!content || content.length === 0) {
|
||||
setLogoOutput("❌ 文件内容为空或读取失败");
|
||||
return;
|
||||
}
|
||||
const b64 = uint8ToBase64(content);
|
||||
const dataUrl = "data:image/bmp;base64," + b64;
|
||||
|
||||
imgEl.src = dataUrl;
|
||||
imgEl.classList.remove("hidden");
|
||||
placeholder.classList.add("hidden");
|
||||
|
||||
setLogoOutput("✅ 已预览:" + name + "\n路径:" + path + "大小:" + content.length + " 字节");
|
||||
}).catch(err => {
|
||||
fileHandle.close();
|
||||
setLogoOutput("❌ 读取文件失败:" + (err.message || err));
|
||||
});
|
||||
}
|
||||
|
||||
// 读取当前设备 Logo(从分区)
|
||||
function readCurrentLogo() {
|
||||
setLogoOutput("⏳ 正在从设备分区读取 Logo...\n命令: dd if=/dev/disk/by-partlabel/logo of=/tmp/current_logo_preview.bmp bs=512 count=100 2>&1");
|
||||
const imgEl = document.getElementById("logo-preview-img");
|
||||
const placeholder = document.getElementById("logo-placeholder");
|
||||
|
||||
// 先用 dd 从分区读出前半段(第一个 BMP,即 splash logo)
|
||||
runCmd(["bash", "-c",
|
||||
"dd if=/dev/disk/by-partlabel/logo of=/tmp/current_logo_preview.bmp bs=512 2>/dev/null && echo OK"
|
||||
], (out, err) => {
|
||||
if (err && !out.includes("OK")) {
|
||||
setLogoOutput("❌ 读取分区失败: " + (err.message || err) + "" + out +"提示:确认 /dev/disk/by-partlabel/logo 分区存在");
|
||||
return;
|
||||
}
|
||||
|
||||
// 读取保存的临时文件显示预览
|
||||
const fileHandle = cockpit.file("/tmp/current_logo_preview.bmp", { binary: true });
|
||||
fileHandle.read().then(content => {
|
||||
fileHandle.close();
|
||||
if (!content || content.length === 0) {
|
||||
setLogoOutput("❌ 读取到的分区内容为空");
|
||||
return;
|
||||
}
|
||||
const b64 = uint8ToBase64(content);
|
||||
const dataUrl = "data:image/bmp;base64," + b64;
|
||||
|
||||
imgEl.src = dataUrl;
|
||||
imgEl.classList.remove("hidden");
|
||||
placeholder.classList.add("hidden");
|
||||
|
||||
setLogoOutput("✅ 已读取设备当前 Logo\n分区:/dev/disk/by-partlabel/logo大小:" + content.length + " 字节");
|
||||
}).catch(err => {
|
||||
fileHandle.close();
|
||||
setLogoOutput("❌ 读取临时文件失败:" + (err.message || err));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ===================== 上传 Logo =====================
|
||||
function handleFileUpload(file) {
|
||||
if (!file) return;
|
||||
|
||||
// 检查扩展名
|
||||
if (!file.name.toLowerCase().endsWith(".bmp")) {
|
||||
showUploadStatus("❌ 仅支持 .bmp 格式文件", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
showUploadStatus("⏳ 正在上传 " + file.name + " ...", "uploading");
|
||||
setLogoOutput("⏳ 正在上传文件:" + file.name);
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
const arrayBuffer = e.target.result;
|
||||
const uint8 = new Uint8Array(arrayBuffer);
|
||||
const destPath = LOGO_DIR + "/" + file.name;
|
||||
|
||||
ensureDir(() => {
|
||||
// 使用 cockpit.file 写入
|
||||
const fileHandle = cockpit.file(destPath, { binary: true, superuser: "require" });
|
||||
fileHandle.replace(uint8).then(() => {
|
||||
fileHandle.close();
|
||||
showUploadStatus("✅ 上传成功:" + file.name, "success");
|
||||
setLogoOutput("✅ 文件已保存到:" + destPath + "大小:" + uint8.length + " 字节");
|
||||
refreshLogoList();
|
||||
}).catch(err => {
|
||||
fileHandle.close();
|
||||
showUploadStatus("❌ 上传失败:" + (err.message || err), "error");
|
||||
setLogoOutput("❌ 写入文件失败:" + (err.message || err));
|
||||
});
|
||||
});
|
||||
};
|
||||
reader.onerror = function() {
|
||||
showUploadStatus("❌ 文件读取失败", "error");
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
}
|
||||
|
||||
// ===================== 删除 Logo =====================
|
||||
function deleteLogo(path) {
|
||||
setLogoOutput("⏳ 正在删除:" + path);
|
||||
runCmd(["rm", "-f", path], (out, err) => {
|
||||
if (err) {
|
||||
setLogoOutput("❌ 删除失败:" + (err.message || err));
|
||||
} else {
|
||||
setLogoOutput("✅ 已删除:" + path);
|
||||
refreshLogoList();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ===================== 烧写 Logo =====================
|
||||
function updateFlashButton() {
|
||||
const sel = document.getElementById("flash-logo-select");
|
||||
const btn = document.getElementById("btn-flash-logo");
|
||||
if (sel && btn) {
|
||||
btn.disabled = !sel.value;
|
||||
}
|
||||
}
|
||||
|
||||
function flashLogo() {
|
||||
const sel = document.getElementById("flash-logo-select");
|
||||
if (!sel || !sel.value) return;
|
||||
|
||||
const logoName = sel.value;
|
||||
const logoPath = LOGO_DIR + "/" + logoName;
|
||||
|
||||
// 检查是否存在 logo_kernel.bmp
|
||||
const kernelPath = LOGO_DIR + "/logo_kernel.bmp";
|
||||
|
||||
setLogoOutput("⏳ 准备烧写...\n主 Logo:" + logoPath + "内核 Logo:" + kernelPath);
|
||||
|
||||
// 检查文件是否存在
|
||||
runCmd(["bash", "-c", `test -f "${logoPath}" && echo "logo_ok" || echo "logo_missing"`], (out1) => {
|
||||
if (out1.trim() !== "logo_ok") {
|
||||
setLogoOutput("❌ 找不到文件:" + logoPath);
|
||||
return;
|
||||
}
|
||||
|
||||
runCmd(["bash", "-c", `test -f "${kernelPath}" && echo "kernel_ok" || echo "kernel_missing"`], (out2) => {
|
||||
const hasKernel = out2.trim() === "kernel_ok";
|
||||
|
||||
if (!hasKernel) {
|
||||
setLogoOutput("⚠️ 未找到 logo_kernel.bmp(" + kernelPath + ")\n将仅烧写主 Logo,跳过内核 Logo 合并步骤。\n\n⏳ 开始烧写...");
|
||||
} else {
|
||||
setLogoOutput("✅ 文件检查通过\n⏳ 开始合并并烧写...\n");
|
||||
}
|
||||
|
||||
doFlash(logoPath, hasKernel ? kernelPath : null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function doFlash(logoPath, kernelPath) {
|
||||
// 构建烧写命令(与原始命令一致)
|
||||
let cmd;
|
||||
if (kernelPath) {
|
||||
cmd = [
|
||||
"bash", "-c",
|
||||
`set -e && ` +
|
||||
`cat "${logoPath}" > /tmp/logo && ` +
|
||||
`truncate -s %512 /tmp/logo && ` +
|
||||
`cat "${kernelPath}" >> /tmp/logo && ` +
|
||||
`cat /tmp/logo > /dev/disk/by-partlabel/logo && ` +
|
||||
`echo "FLASH_OK"`
|
||||
];
|
||||
} else {
|
||||
cmd = [
|
||||
"bash", "-c",
|
||||
`set -e && ` +
|
||||
`cat "${logoPath}" > /tmp/logo && ` +
|
||||
`truncate -s %512 /tmp/logo && ` +
|
||||
`cat /tmp/logo > /dev/disk/by-partlabel/logo && ` +
|
||||
`echo "FLASH_OK"`
|
||||
];
|
||||
}
|
||||
|
||||
runCmd(cmd, (out, err) => {
|
||||
if (err || !out.includes("FLASH_OK")) {
|
||||
setLogoOutput(
|
||||
"❌ 烧写失败!\n" +
|
||||
"错误信息:" + (err ? (err.message || err) : "未知错误") + "" +
|
||||
(out ? "\n输出:\n" + out : "") +
|
||||
"\n\n请检查:\n" +
|
||||
" 1. /dev/disk/by-partlabel/logo 分区是否存在" +
|
||||
" 2. 是否有足够权限写入分区" +
|
||||
" 3. BMP 文件格式是否正确"
|
||||
);
|
||||
} else {
|
||||
setLogoOutput(
|
||||
"✅ 烧写成功!\n" +
|
||||
"主 Logo:" + logoPath + "" +
|
||||
(kernelPath ? "内核 Logo:" + kernelPath + "" : "") +
|
||||
"目标分区:/dev/disk/by-partlabel/logo\n" +
|
||||
"重启设备后新 Logo 将生效。"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ===================== 拖拽上传 =====================
|
||||
function initDragDrop() {
|
||||
const area = document.getElementById("upload-area");
|
||||
if (!area) return;
|
||||
|
||||
area.addEventListener("dragover", e => {
|
||||
e.preventDefault();
|
||||
area.classList.add("drag-over");
|
||||
});
|
||||
|
||||
area.addEventListener("dragleave", () => {
|
||||
area.classList.remove("drag-over");
|
||||
});
|
||||
|
||||
area.addEventListener("drop", e => {
|
||||
e.preventDefault();
|
||||
area.classList.remove("drag-over");
|
||||
const file = e.dataTransfer.files[0];
|
||||
if (file) handleFileUpload(file);
|
||||
});
|
||||
|
||||
// 点击选择文件
|
||||
const fileInput = document.getElementById("logo-file-input");
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener("change", function() {
|
||||
if (this.files[0]) handleFileUpload(this.files[0]);
|
||||
this.value = ""; // 重置,允许重复选同一文件
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== Base64 工具 =====================
|
||||
function uint8ToBase64(uint8Array) {
|
||||
let binary = "";
|
||||
const chunkSize = 8192;
|
||||
for (let i = 0; i < uint8Array.length; i += chunkSize) {
|
||||
const chunk = uint8Array.subarray(i, i + chunkSize);
|
||||
binary += String.fromCharCode.apply(null, chunk);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
// ===================== 页面初始化 =====================
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
initTabs();
|
||||
initDragDrop();
|
||||
refreshLogoList();
|
||||
|
||||
// 读取当前 Logo 按钮
|
||||
const btnReadLogo = document.getElementById("btn-read-logo");
|
||||
if (btnReadLogo) btnReadLogo.addEventListener("click", readCurrentLogo);
|
||||
|
||||
// 刷新列表按钮
|
||||
const btnRefreshList = document.getElementById("btn-refresh-list");
|
||||
if (btnRefreshList) btnRefreshList.addEventListener("click", refreshLogoList);
|
||||
|
||||
// 烧写按钮
|
||||
const btnFlash = document.getElementById("btn-flash-logo");
|
||||
if (btnFlash) btnFlash.addEventListener("click", flashLogo);
|
||||
|
||||
// 下拉选择变化
|
||||
const flashSel = document.getElementById("flash-logo-select");
|
||||
if (flashSel) flashSel.addEventListener("change", updateFlashButton);
|
||||
});
|
||||
Reference in New Issue
Block a user