Compare commits
2 Commits
36292c3e4d
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 130f2a29ce | |||
| 67f7a95fef |
BIN
apps/apps.css.gz
Normal file
BIN
apps/apps.min.js.gz
Normal file
BIN
apps/default.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
apps/index.html.gz
Normal file
25
apps/manifest.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"version": "215",
|
||||
"requires": {
|
||||
"cockpit": "122"
|
||||
},
|
||||
|
||||
"tools": {
|
||||
"index": {
|
||||
"label": "Applications",
|
||||
"keywords": [
|
||||
{
|
||||
"matches": ["plugin", "apps", "addon", "add-on", "install", "extension"]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
"content-security-policy": "img-src *",
|
||||
|
||||
"config": {
|
||||
"appstream_config_packages": [ "appstream" ],
|
||||
"appstream_data_packages": []
|
||||
}
|
||||
|
||||
}
|
||||
BIN
apps/po.ca.js.gz
Normal file
BIN
apps/po.cs.js.gz
Normal file
BIN
apps/po.de.js.gz
Normal file
BIN
apps/po.es.js.gz
Normal file
BIN
apps/po.fr.js.gz
Normal file
BIN
apps/po.it.js.gz
Normal file
BIN
apps/po.ja.js.gz
Normal file
BIN
apps/po.js.gz
Normal file
BIN
apps/po.ko.js.gz
Normal file
BIN
apps/po.nl.js.gz
Normal file
BIN
apps/po.pl.js.gz
Normal file
BIN
apps/po.pt_BR.js.gz
Normal file
BIN
apps/po.ru.js.gz
Normal file
BIN
apps/po.sv.js.gz
Normal file
BIN
apps/po.uk.js.gz
Normal file
BIN
apps/po.zh_CN.js.gz
Normal file
BIN
apps/po.zh_TW.js.gz
Normal file
BIN
base1/cockpit.min.css.gz
Normal file
BIN
base1/cockpit.min.js.gz
Normal file
BIN
base1/fonts/fontawesome.woff
Normal file
BIN
base1/fonts/glyphicons.woff
Normal file
BIN
base1/fonts/patternfly.woff
Normal file
BIN
base1/jquery.min.js.gz
Normal file
14
base1/manifest.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"version": "@VERSION@",
|
||||
"priority": -1,
|
||||
"bridges": [
|
||||
{
|
||||
"match": { "superuser": "require" },
|
||||
"problem": "access-denied"
|
||||
},
|
||||
{
|
||||
"match": { "superuser": true },
|
||||
"problem": "access-denied"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
base1/mustache.min.js.gz
Normal file
BIN
base1/patternfly.min.css.gz
Normal file
26
branding/debian/branding.css
Normal file
@@ -0,0 +1,26 @@
|
||||
body.login-pf {
|
||||
background: url("bg-plain.jpg") no-repeat 50% 0;
|
||||
background-size: cover;
|
||||
background-color: #101010;
|
||||
}
|
||||
|
||||
#badge {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background-image: url("logo.png");
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
#brand {
|
||||
font-size: 18pt;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
#brand:before {
|
||||
content: "${NAME}";
|
||||
}
|
||||
|
||||
#index-brand:before {
|
||||
content: "${NAME}";
|
||||
}
|
||||
1
branding/debian/favicon.ico
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../pixmaps/debian-logo.png
|
||||
1
branding/debian/logo.png
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../pixmaps/debian-logo.png
|
||||
BIN
branding/default/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
branding/default/bg-login.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
branding/default/bg-plain.jpg
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
branding/default/brand-large.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
33
branding/default/branding.css
Normal file
@@ -0,0 +1,33 @@
|
||||
|
||||
.login-note {
|
||||
color: transparent;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body.login-pf {
|
||||
background: url("bg-login.jpg") no-repeat 50% 0;
|
||||
background-size: cover;
|
||||
background-color: #101010;
|
||||
}
|
||||
|
||||
#badge {
|
||||
width: 73px;
|
||||
height: 69px;
|
||||
background-image: url("logo.png");
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
#brand {
|
||||
width: 121px;
|
||||
height: 18px;
|
||||
background-image: url("brand-large.png");
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
#index-brand {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#index-brand:before {
|
||||
content: "Cockpit";
|
||||
}
|
||||
BIN
branding/default/favicon.ico
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
branding/default/logo.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
30
branding/kubernetes/branding.css
Normal file
@@ -0,0 +1,30 @@
|
||||
body.login-pf {
|
||||
background: url("bg-plain.jpg") no-repeat 50% 0;
|
||||
background-size: cover;
|
||||
background-color: #101010;
|
||||
}
|
||||
|
||||
#badge {
|
||||
width: 225px;
|
||||
height: 80px;
|
||||
background-image: url("logo.png");
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
#brand {
|
||||
font-size: 18pt;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
#brand:before {
|
||||
content: "${NAME}";
|
||||
}
|
||||
|
||||
#index-brand {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#index-brand:before {
|
||||
content: "${NAME}";
|
||||
}
|
||||
30
branding/registry/branding.css
Normal file
@@ -0,0 +1,30 @@
|
||||
body.login-pf {
|
||||
background: url("bg-plain.jpg") no-repeat 50% 0;
|
||||
background-size: cover;
|
||||
background-color: #101010;
|
||||
}
|
||||
|
||||
#badge {
|
||||
width: 225px;
|
||||
height: 80px;
|
||||
background-image: url("logo.png");
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
#brand {
|
||||
font-size: 18pt;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
#brand:before {
|
||||
content: "${NAME}";
|
||||
}
|
||||
|
||||
#index-brand {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#index-brand:before {
|
||||
content: "${NAME}";
|
||||
}
|
||||
30
branding/ubuntu/branding.css
Normal file
@@ -0,0 +1,30 @@
|
||||
body.login-pf {
|
||||
background: url("bg-plain.jpg") no-repeat 50% 0;
|
||||
background-size: cover;
|
||||
background-color: #101010;
|
||||
}
|
||||
|
||||
#badge {
|
||||
width: 220px;
|
||||
height: 80px;
|
||||
background-image: url("logo.png");
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
#brand {
|
||||
font-size: 18pt;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
#brand:before {
|
||||
content: "${PRETTY_NAME}";
|
||||
}
|
||||
|
||||
#index-brand {
|
||||
content: "${NAME}";
|
||||
}
|
||||
|
||||
#index-brand:before {
|
||||
content: "${NAME}";
|
||||
}
|
||||
BIN
branding/ubuntu/favicon.ico
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
branding/ubuntu/logo.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
dashboard/dashboard.css.gz
Normal file
BIN
dashboard/dashboard.min.js.gz
Normal file
BIN
dashboard/index.html.gz
Normal file
21
dashboard/manifest.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"version": "215",
|
||||
"requires": {
|
||||
"cockpit": "138"
|
||||
},
|
||||
|
||||
"dashboard": {
|
||||
"index": {
|
||||
"label": "Dashboard",
|
||||
"docs": [
|
||||
{
|
||||
"label": "Managing remote systems",
|
||||
"url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/managing-remote-systems-in-the-web-console_system-management-using-the-rhel-8-web-console"
|
||||
}
|
||||
],
|
||||
"order": 10,
|
||||
"wants": "multiple-machines",
|
||||
"icon": "fa-tachometer"
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
dashboard/po.ca.js.gz
Normal file
BIN
dashboard/po.cs.js.gz
Normal file
BIN
dashboard/po.de.js.gz
Normal file
BIN
dashboard/po.es.js.gz
Normal file
BIN
dashboard/po.fr.js.gz
Normal file
BIN
dashboard/po.it.js.gz
Normal file
BIN
dashboard/po.ja.js.gz
Normal file
BIN
dashboard/po.js.gz
Normal file
BIN
dashboard/po.ko.js.gz
Normal file
BIN
dashboard/po.nl.js.gz
Normal file
BIN
dashboard/po.pl.js.gz
Normal file
BIN
dashboard/po.pt_BR.js.gz
Normal file
BIN
dashboard/po.ru.js.gz
Normal file
BIN
dashboard/po.sv.js.gz
Normal file
BIN
dashboard/po.uk.js.gz
Normal file
BIN
dashboard/po.zh_CN.js.gz
Normal file
BIN
dashboard/po.zh_TW.js.gz
Normal file
257
lj360/lj360.css
Normal file
@@ -0,0 +1,257 @@
|
||||
/* Cockpit 风格样式 - 无内联样式 */
|
||||
|
||||
body.pf-m-redhat-font {
|
||||
background-color: #f5f5f5;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'RedHatText', 'Overpass', 'Segoe UI', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.cockpit-service-page {
|
||||
padding: 24px 30px;
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
/* 卡片样式 */
|
||||
.pf-c-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e6e9ed;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.05);
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.pf-c-card__header {
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #e6e9ed;
|
||||
background-color: #fafbfc;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pf-c-card__header i {
|
||||
margin-right: 10px;
|
||||
color: #0066cc;
|
||||
}
|
||||
|
||||
.pf-c-card__body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 状态面板 */
|
||||
.status-panel {
|
||||
background: #fafbfc;
|
||||
border-left: 4px solid #73bcf7;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-weight: 500;
|
||||
color: #151515;
|
||||
}
|
||||
|
||||
/* Cockpit 风格开关 */
|
||||
.cockpit-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 48px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.cockpit-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.cockpit-switch .slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #b8bbbf;
|
||||
transition: 0.2s;
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
.cockpit-switch .slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
transition: 0.2s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.cockpit-switch input:checked + .slider {
|
||||
background-color: #0066cc;
|
||||
}
|
||||
|
||||
.cockpit-switch input:focus + .slider {
|
||||
box-shadow: 0 0 1px #0066cc;
|
||||
}
|
||||
|
||||
.cockpit-switch input:checked + .slider:before {
|
||||
transform: translateX(22px);
|
||||
}
|
||||
|
||||
/* 设置行 */
|
||||
.setting-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid #eaeef2;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.setting-label label {
|
||||
font-weight: 600;
|
||||
color: #151515;
|
||||
font-size: 14px;
|
||||
margin-bottom: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.setting-description {
|
||||
font-size: 12px;
|
||||
color: #72767b;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.setting-control {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 操作按钮组 */
|
||||
.action-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.pf-c-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.15s;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.pf-c-button i {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.pf-c-button.pf-m-primary {
|
||||
background-color: #0066cc;
|
||||
color: white;
|
||||
border-color: #0066cc;
|
||||
}
|
||||
|
||||
.pf-c-button.pf-m-primary:hover {
|
||||
background-color: #005cbb;
|
||||
border-color: #005cbb;
|
||||
}
|
||||
|
||||
.pf-c-button.pf-m-secondary {
|
||||
background-color: #ffffff;
|
||||
color: #0066cc;
|
||||
border-color: #b8bbbf;
|
||||
}
|
||||
|
||||
.pf-c-button.pf-m-secondary:hover {
|
||||
background-color: #f5f5f5;
|
||||
border-color: #0066cc;
|
||||
}
|
||||
|
||||
/* 分隔线 */
|
||||
.separator {
|
||||
height: 1px;
|
||||
background: #eaeef2;
|
||||
margin: 20px 0 16px 0;
|
||||
}
|
||||
|
||||
/* 单独测试区域 */
|
||||
.testing-section {
|
||||
margin: 8px 0 16px 0;
|
||||
}
|
||||
|
||||
.testing-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #72767b;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.testing-title i {
|
||||
color: #6ca100;
|
||||
}
|
||||
|
||||
/* 输出区域 */
|
||||
.output-section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.output-label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #4d5258;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.log-box {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 14px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Menlo', 'Monaco', 'Consolas', monospace;
|
||||
font-size: 12px;
|
||||
height: 280px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
border: 1px solid #3c3f41;
|
||||
}
|
||||
|
||||
/* 状态颜色变化 */
|
||||
.status-panel.status-success {
|
||||
border-left-color: #3f9c35;
|
||||
}
|
||||
|
||||
.status-panel.status-danger {
|
||||
border-left-color: #c00;
|
||||
}
|
||||
|
||||
.status-panel.status-warning {
|
||||
border-left-color: #ec7a08;
|
||||
}
|
||||
84
lj360/lj360.html
Normal file
@@ -0,0 +1,84 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh" class="index-page">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="../base1/patternfly.css" rel="stylesheet">
|
||||
<link href="../base1/patternfly-additions.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="lj360.css">
|
||||
<script src="../base1/cockpit.js"></script>
|
||||
</head>
|
||||
<body class="pf-m-redhat-font">
|
||||
<div class="cockpit-service-page">
|
||||
<!-- 状态卡片 -->
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__body">
|
||||
<div class="status-panel" id="status-panel">
|
||||
<div class="status-icon" id="status-icon">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
<div class="status-text" id="status-text">正在获取状态...</div>
|
||||
</div>
|
||||
|
||||
<!-- 开机自启动开关 - 使用 Cockpit 风格 -->
|
||||
<div class="setting-row">
|
||||
<div class="setting-label">
|
||||
<label></label>
|
||||
<div class="setting-description">系统启动时自动运行人员接近报警系统</div>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<label class="cockpit-switch">
|
||||
<input type="checkbox" id="autostart-switch">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮组 -->
|
||||
<div class="action-bar">
|
||||
<button class="pf-c-button pf-m-primary" id="btn-start">
|
||||
<i class="fa fa-play"></i> 启动服务
|
||||
</button>
|
||||
<button class="pf-c-button pf-m-secondary" id="btn-stop">
|
||||
<i class="fa fa-stop"></i> 停止服务
|
||||
</button>
|
||||
<button class="pf-c-button pf-m-secondary" id="btn-restart">
|
||||
<i class="fa fa-refresh"></i> 重启服务
|
||||
</button>
|
||||
<button class="pf-c-button pf-m-secondary" id="btn-status">
|
||||
<i class="fa fa-search"></i> 刷新状态
|
||||
</button>
|
||||
<button class="pf-c-button pf-m-secondary" id="btn-log">
|
||||
<i class="fa fa-file-text-o"></i> 查看日志
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 单独测试区域 -->
|
||||
<div class="separator"></div>
|
||||
<div class="testing-section">
|
||||
<div class="testing-title">
|
||||
<i class="fa fa-flask"></i> 单独测试
|
||||
</div>
|
||||
<div class="action-bar">
|
||||
<button class="pf-c-button pf-m-secondary" id="btn-pyon">
|
||||
<i class="fa fa-play-circle-o"></i> 单次测试启动
|
||||
</button>
|
||||
<button class="pf-c-button pf-m-secondary" id="btn-pyoff">
|
||||
<i class="fa fa-stop-circle-o"></i> 单次测试停止
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输出信息区域 -->
|
||||
<div class="output-section">
|
||||
<div class="output-label">
|
||||
<i class="fa fa-terminal"></i> 输出信息
|
||||
</div>
|
||||
<div class="log-box" id="output">等待操作...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="lj360.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
233
lj360/lj360.js
Normal file
@@ -0,0 +1,233 @@
|
||||
const SCRIPT = "/home/ztl/LJ360/bash/video_tool.sh";
|
||||
const SERVICE = "lj360_camera";
|
||||
const LOG_FILE = "/home/ztl/LJ360/lj360_camera_keepalive.log";
|
||||
const WEBPY = "/home/ztl/LJ360/web.py";
|
||||
|
||||
// DOM 元素
|
||||
let statusIcon, statusText, outputBox, autostartSwitch;
|
||||
|
||||
function initElements() {
|
||||
statusIcon = document.getElementById("status-icon");
|
||||
statusText = document.getElementById("status-text");
|
||||
outputBox = document.getElementById("output");
|
||||
autostartSwitch = document.getElementById("autostart-switch");
|
||||
}
|
||||
|
||||
function setOutput(text) {
|
||||
if (outputBox) {
|
||||
outputBox.textContent = text;
|
||||
outputBox.scrollTop = outputBox.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
function setStatus(text, isActive) {
|
||||
if (statusText) {
|
||||
statusText.textContent = text;
|
||||
}
|
||||
const panel = document.getElementById("status-panel");
|
||||
if (panel) {
|
||||
// 移除现有状态类
|
||||
panel.classList.remove("status-success", "status-danger", "status-warning");
|
||||
if (isActive === true) {
|
||||
panel.classList.add("status-success");
|
||||
} else if (isActive === false) {
|
||||
panel.classList.add("status-danger");
|
||||
} else if (isActive === undefined) {
|
||||
panel.classList.add("status-warning");
|
||||
}
|
||||
}
|
||||
if (statusIcon) {
|
||||
if (isActive === true) {
|
||||
statusIcon.innerHTML = '<i class="fa fa-check-circle" style="color:#3f9c35"></i>';
|
||||
} else if (isActive === false) {
|
||||
statusIcon.innerHTML = '<i class="fa fa-exclamation-circle" style="color:#c00"></i>';
|
||||
} else {
|
||||
statusIcon.innerHTML = '<i class="fa fa-refresh fa-spin"></i>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function runCommand(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 refreshStatus() {
|
||||
setStatus("正在获取服务状态...", undefined);
|
||||
setOutput("⏳ 正在刷新状态...");
|
||||
|
||||
// 获取服务运行状态
|
||||
runCommand(["systemctl", "is-active", SERVICE + ".service"], (outActive, errActive) => {
|
||||
const isRunning = (outActive.trim() === "active");
|
||||
|
||||
// 获取开机自启状态
|
||||
runCommand(["systemctl", "is-enabled", SERVICE + ".service"], (outEnabled, errEnabled) => {
|
||||
let isEnabled = false;
|
||||
if (!errEnabled) {
|
||||
const enabledOut = outEnabled.trim();
|
||||
isEnabled = (enabledOut === "enabled" || enabledOut === "static");
|
||||
}
|
||||
|
||||
// 更新开关状态(不触发change事件)
|
||||
if (autostartSwitch) {
|
||||
autostartSwitch.checked = isEnabled;
|
||||
}
|
||||
|
||||
// 更新状态文本
|
||||
if (isRunning) {
|
||||
setStatus("服务正在运行" + (isEnabled ? " (开机自启已启用)" : " (开机自启未启用)"), true);
|
||||
} else {
|
||||
setStatus("服务未运行" + (isEnabled ? " (开机自启已启用但未运行)" : " (开机自启未启用)"), false);
|
||||
}
|
||||
|
||||
// 获取详细状态输出
|
||||
runCommand(["systemctl", "status", SERVICE + ".service", "--no-pager", "-l"], (outDetail) => {
|
||||
setOutput(outDetail || "无状态信息");
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 设置开机自启(通过开关)
|
||||
function setAutostart(enabled) {
|
||||
setOutput("⏳ " + (enabled ? "正在启用开机自启..." : "正在禁用开机自启..."));
|
||||
const action = enabled ? "enable_autostart" : "disable_autostart";
|
||||
runCommand(["bash", SCRIPT, action], (out, err) => {
|
||||
if (err) {
|
||||
setOutput("❌ 错误: " + (err.message || err) + "\n" + (out || ""));
|
||||
// 恢复开关状态
|
||||
setTimeout(() => refreshStatus(), 500);
|
||||
} else {
|
||||
setOutput((enabled ? "✅ 开机自启已启用" : "✅ 开机自启已禁用") + "\n" + (out || ""));
|
||||
setTimeout(() => refreshStatus(), 300);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 启动服务
|
||||
function startService() {
|
||||
setOutput("⏳ 正在启动服务...");
|
||||
runCommand(["systemctl", "start", SERVICE + ".service"], (out, err) => {
|
||||
if (err) {
|
||||
setOutput("❌ 启动失败: " + (err.message || err) + "\n" + out);
|
||||
} else {
|
||||
setOutput("✅ 服务已启动\n" + out);
|
||||
}
|
||||
setTimeout(() => refreshStatus(), 500);
|
||||
});
|
||||
}
|
||||
|
||||
// 停止服务
|
||||
function stopService() {
|
||||
setOutput("⏳ 正在停止服务...");
|
||||
runCommand(["systemctl", "stop", SERVICE + ".service"], (out, err) => {
|
||||
if (err) {
|
||||
setOutput("❌ 停止失败: " + (err.message || err) + "\n" + out);
|
||||
} else {
|
||||
setOutput("✅ 服务已停止\n" + out);
|
||||
}
|
||||
setTimeout(() => refreshStatus(), 500);
|
||||
});
|
||||
}
|
||||
|
||||
// 重启服务
|
||||
function restartService() {
|
||||
setOutput("⏳ 正在重启服务...");
|
||||
runCommand(["systemctl", "restart", SERVICE + ".service"], (out, err) => {
|
||||
if (err) {
|
||||
setOutput("❌ 重启失败: " + (err.message || err) + "\n" + out);
|
||||
} else {
|
||||
setOutput("✅ 服务已重启\n" + out);
|
||||
}
|
||||
setTimeout(() => refreshStatus(), 800);
|
||||
});
|
||||
}
|
||||
|
||||
// 查看日志
|
||||
function viewLog() {
|
||||
setOutput("⏳ 读取日志文件...");
|
||||
runCommand(["tail", "-n", "80", LOG_FILE], (out, err) => {
|
||||
if (err) {
|
||||
setOutput("❌ 无法读取日志: " + (err.message || err) + "\n尝试查看服务日志...");
|
||||
// 备用:查看 journalctl
|
||||
runCommand(["journalctl", "-u", SERVICE + ".service", "-n", "50", "--no-pager"], (out2, err2) => {
|
||||
if (err2) {
|
||||
setOutput("📭 日志为空或无法读取\n" + (out2 || ""));
|
||||
} else {
|
||||
setOutput("📋 系统日志 (journalctl):\n" + (out2 || "无日志"));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setOutput("📋 服务日志 (最近80行):\n" + (out || "日志为空"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 单独启动 web.py
|
||||
function startWebPy() {
|
||||
setOutput("⏳ 正在启动 web.py...");
|
||||
runCommand([
|
||||
"bash", "-c",
|
||||
"export DISPLAY=:0 && cd /home/ztl/LJ360 && sudo -u ztl python3 /home/ztl/LJ360/web.py >> /home/ztl/LJ360/web.log 2>&1 &"
|
||||
], (out, err) => {
|
||||
if (err) {
|
||||
setOutput("❌ 启动 web.py 失败: " + (err.message || err));
|
||||
} else {
|
||||
setOutput("✅ web.py 已在后台启动\n可通过 'ps aux | grep web.py' 查看进程");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 停止 web.py
|
||||
function stopWebPy() {
|
||||
setOutput("⏳ 正在关闭 web.py...");
|
||||
runCommand(["bash", "-c", "pkill -f /home/ztl/LJ360/web.py"], (out, err) => {
|
||||
if (err) {
|
||||
// pkill 没有找到进程也会返回错误,这是正常的
|
||||
if (err.message && err.message.includes("exit code 1")) {
|
||||
setOutput("⚠️ 未找到运行中的 web.py 进程");
|
||||
} else {
|
||||
setOutput("❌ 关闭失败: " + (err.message || err));
|
||||
}
|
||||
} else {
|
||||
setOutput("✅ web.py 已关闭");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 事件绑定
|
||||
function bindEvents() {
|
||||
const btnStart = document.getElementById("btn-start");
|
||||
const btnStop = document.getElementById("btn-stop");
|
||||
const btnRestart = document.getElementById("btn-restart");
|
||||
const btnStatus = document.getElementById("btn-status");
|
||||
const btnLog = document.getElementById("btn-log");
|
||||
const btnPyon = document.getElementById("btn-pyon");
|
||||
const btnPyoff = document.getElementById("btn-pyoff");
|
||||
|
||||
if (btnStart) btnStart.addEventListener("click", startService);
|
||||
if (btnStop) btnStop.addEventListener("click", stopService);
|
||||
if (btnRestart) btnRestart.addEventListener("click", restartService);
|
||||
if (btnStatus) btnStatus.addEventListener("click", refreshStatus);
|
||||
if (btnLog) btnLog.addEventListener("click", viewLog);
|
||||
if (btnPyon) btnPyon.addEventListener("click", startWebPy);
|
||||
if (btnPyoff) btnPyoff.addEventListener("click", stopWebPy);
|
||||
|
||||
// 开关事件:开机自启变更
|
||||
if (autostartSwitch) {
|
||||
autostartSwitch.addEventListener("change", function(e) {
|
||||
setAutostart(e.target.checked);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 页面初始化
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
initElements();
|
||||
bindEvents();
|
||||
refreshStatus();
|
||||
});
|
||||
420
lj360/lj360_config.css
Normal file
@@ -0,0 +1,420 @@
|
||||
/* ===== 标签页 ===== */
|
||||
.pf-c-tabs {
|
||||
margin-bottom: 0;
|
||||
border-bottom: 1px solid #d2d2d2;
|
||||
background: #fff;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.pf-c-tabs__list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.pf-c-tabs__item {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pf-c-tabs__link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 14px 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #6a6e73;
|
||||
border: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.pf-c-tabs__link:hover {
|
||||
color: #0066cc;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.pf-c-tabs__item.pf-m-current .pf-c-tabs__link {
|
||||
color: #0066cc;
|
||||
border-bottom-color: #0066cc;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ===== 内容面板 ===== */
|
||||
.tab-panel {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.tab-panel.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ===== 区域标题 ===== */
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #151515;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.section-title i {
|
||||
color: #0066cc;
|
||||
}
|
||||
|
||||
.section-hint {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #72767b;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.warning-hint {
|
||||
color: #ec7a08 !important;
|
||||
}
|
||||
|
||||
.warning-hint i {
|
||||
color: #ec7a08 !important;
|
||||
}
|
||||
|
||||
/* ===== Logo 预览 ===== */
|
||||
.logo-preview-container {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.logo-preview-box {
|
||||
width: 320px;
|
||||
height: 160px;
|
||||
border: 2px dashed #d2d2d2;
|
||||
border-radius: 6px;
|
||||
background: #1e1e1e;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.logo-preview-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #5a5a5a;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.logo-preview-placeholder i {
|
||||
font-size: 32px;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.logo-preview-img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.logo-preview-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
/* ===== 上传区域 ===== */
|
||||
.upload-area {
|
||||
border: 2px dashed #b8bbbf;
|
||||
border-radius: 6px;
|
||||
padding: 28px 20px;
|
||||
text-align: center;
|
||||
background: #fafbfc;
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.upload-area:hover,
|
||||
.upload-area.drag-over {
|
||||
border-color: #0066cc;
|
||||
background: #f0f7ff;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 36px;
|
||||
color: #b8bbbf;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.upload-area:hover .upload-icon,
|
||||
.upload-area.drag-over .upload-icon {
|
||||
color: #0066cc;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
font-size: 14px;
|
||||
color: #4d5258;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.upload-link {
|
||||
color: #0066cc;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
font-size: 12px;
|
||||
color: #72767b;
|
||||
}
|
||||
|
||||
.upload-status {
|
||||
padding: 10px 14px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
margin-bottom: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.upload-status.success {
|
||||
background: #ecf7ec;
|
||||
border: 1px solid #3f9c35;
|
||||
color: #1e4620;
|
||||
}
|
||||
|
||||
.upload-status.error {
|
||||
background: #fff0ee;
|
||||
border: 1px solid #c00;
|
||||
color: #7d1d1d;
|
||||
}
|
||||
|
||||
.upload-status.uploading {
|
||||
background: #e7f1fa;
|
||||
border: 1px solid #73bcf7;
|
||||
color: #004080;
|
||||
}
|
||||
|
||||
/* ===== Logo 文件列表 ===== */
|
||||
.logo-list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #4d5258;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 5px 10px !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.logo-list {
|
||||
border: 1px solid #e6e9ed;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.logo-list-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 28px;
|
||||
color: #72767b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.logo-list-empty i {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.logo-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: background 0.1s;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.logo-list-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.logo-list-item:hover {
|
||||
background: #f5f9ff;
|
||||
}
|
||||
|
||||
.logo-item-thumb {
|
||||
width: 48px;
|
||||
height: 28px;
|
||||
background: #1e1e1e;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logo-item-thumb img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.logo-item-thumb-placeholder {
|
||||
color: #555;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.logo-item-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.logo-item-name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #151515;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.logo-item-meta {
|
||||
font-size: 11px;
|
||||
color: #72767b;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.logo-item-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ===== 烧写区域 ===== */
|
||||
.flash-area {
|
||||
background: #fffbf0;
|
||||
border: 1px solid #f5e6c8;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.flash-select-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.flash-label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #4d5258;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pf-c-form-control {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
padding: 7px 10px;
|
||||
border: 1px solid #b8bbbf;
|
||||
border-radius: 3px;
|
||||
font-size: 13px;
|
||||
background: #fff;
|
||||
color: #151515;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.pf-c-form-control:focus {
|
||||
border-color: #0066cc;
|
||||
box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.2);
|
||||
}
|
||||
|
||||
.flash-note {
|
||||
font-size: 12px;
|
||||
color: #72767b;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.flash-note code {
|
||||
background: #f0f0f0;
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Menlo', 'Monaco', 'Consolas', monospace;
|
||||
color: #c00;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* 危险按钮 */
|
||||
.pf-c-button.pf-m-danger {
|
||||
background-color: #c00;
|
||||
color: white;
|
||||
border-color: #c00;
|
||||
}
|
||||
|
||||
.pf-c-button.pf-m-danger:hover:not(:disabled) {
|
||||
background-color: #a30000;
|
||||
border-color: #a30000;
|
||||
}
|
||||
|
||||
.pf-c-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ===== 空状态占位 ===== */
|
||||
.empty-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
color: #72767b;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-placeholder > i {
|
||||
font-size: 48px;
|
||||
color: #d2d2d2;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #4d5258;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 13px;
|
||||
color: #72767b;
|
||||
}
|
||||
|
||||
/* ===== 通用 ===== */
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
152
lj360/lj360_config.html
Normal file
@@ -0,0 +1,152 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh" class="index-page">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="../base1/patternfly.css" rel="stylesheet">
|
||||
<link href="../base1/patternfly-additions.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="lj360.css">
|
||||
<link rel="stylesheet" href="lj360_config.css">
|
||||
<script src="../base1/cockpit.js"></script>
|
||||
</head>
|
||||
<body class="pf-m-redhat-font">
|
||||
<div class="cockpit-service-page">
|
||||
|
||||
<!-- 标签页导航 -->
|
||||
<div class="pf-c-tabs" id="main-tabs">
|
||||
<ul class="pf-c-tabs__list">
|
||||
<li class="pf-c-tabs__item pf-m-current" data-tab="tab-logo">
|
||||
<button class="pf-c-tabs__link">
|
||||
<i class="fa fa-image"></i> 开机 Logo
|
||||
</button>
|
||||
</li>
|
||||
<li class="pf-c-tabs__item" data-tab="tab-comm">
|
||||
<button class="pf-c-tabs__link">
|
||||
<i class="fa fa-plug"></i> 通讯参数
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- ===== Tab: 开机 Logo ===== -->
|
||||
<div class="tab-panel" id="tab-logo">
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__header">
|
||||
<i class="fa fa-image"></i> 开机 Logo 管理
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
|
||||
<!-- 当前 Logo 预览 -->
|
||||
<div class="section-title">
|
||||
<i class="fa fa-eye"></i> 当前 Logo 预览
|
||||
</div>
|
||||
<div class="logo-preview-container">
|
||||
<div class="logo-preview-box" id="logo-preview-box">
|
||||
<div class="logo-preview-placeholder" id="logo-placeholder">
|
||||
<i class="fa fa-picture-o"></i>
|
||||
<span>点击"读取当前Logo"加载预览</span>
|
||||
</div>
|
||||
<img id="logo-preview-img" class="logo-preview-img hidden" alt="当前Logo预览">
|
||||
</div>
|
||||
<div class="logo-preview-actions">
|
||||
<button class="pf-c-button pf-m-secondary" id="btn-read-logo">
|
||||
<i class="fa fa-download"></i> 读取当前 Logo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="separator"></div>
|
||||
|
||||
<!-- Logo 库 -->
|
||||
<div class="section-title">
|
||||
<i class="fa fa-folder-open-o"></i> Logo 库
|
||||
<span class="section-hint">存储目录:/home/ztl/LJ360/config/logos/</span>
|
||||
</div>
|
||||
|
||||
<!-- 上传新 Logo -->
|
||||
<div class="upload-area" id="upload-area">
|
||||
<div class="upload-icon">
|
||||
<i class="fa fa-cloud-upload"></i>
|
||||
</div>
|
||||
<div class="upload-text">
|
||||
拖拽 BMP 文件到此处,或
|
||||
<label class="upload-link" for="logo-file-input">点击选择文件</label>
|
||||
</div>
|
||||
<div class="upload-hint">仅支持 .bmp 格式,建议分辨率与屏幕一致</div>
|
||||
<input type="file" id="logo-file-input" accept=".bmp" class="hidden">
|
||||
</div>
|
||||
|
||||
<!-- 上传状态 -->
|
||||
<div id="upload-status" class="upload-status hidden"></div>
|
||||
|
||||
<!-- Logo 文件列表 -->
|
||||
<div class="logo-list-header">
|
||||
<span>已保存的 Logo 文件</span>
|
||||
<button class="pf-c-button pf-m-secondary btn-sm" id="btn-refresh-list">
|
||||
<i class="fa fa-refresh"></i> 刷新列表
|
||||
</button>
|
||||
</div>
|
||||
<div class="logo-list" id="logo-list">
|
||||
<div class="logo-list-empty">
|
||||
<i class="fa fa-inbox"></i> 暂无 Logo 文件,请上传
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="separator"></div>
|
||||
|
||||
<!-- 烧写操作 -->
|
||||
<div class="section-title">
|
||||
<i class="fa fa-fire"></i> 烧写到设备
|
||||
<span class="section-hint warning-hint">
|
||||
<i class="fa fa-warning"></i> 此操作将直接写入 logo 分区,请谨慎操作
|
||||
</span>
|
||||
</div>
|
||||
<div class="flash-area">
|
||||
<div class="flash-select-row">
|
||||
<div class="flash-label">选择要烧写的 Logo:</div>
|
||||
<select class="pf-c-form-control" id="flash-logo-select">
|
||||
<option value="">-- 请先选择 Logo 文件 --</option>
|
||||
</select>
|
||||
<button class="pf-c-button pf-m-danger" id="btn-flash-logo" disabled>
|
||||
<i class="fa fa-bolt"></i> 烧写到设备
|
||||
</button>
|
||||
</div>
|
||||
<div class="flash-note">
|
||||
<i class="fa fa-info-circle"></i>
|
||||
烧写命令说明:将选定的 logo.bmp 与 logo_kernel.bmp 合并写入
|
||||
<code>/dev/disk/by-partlabel/logo</code> 分区
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输出信息 -->
|
||||
<div class="output-section">
|
||||
<div class="output-label">
|
||||
<i class="fa fa-terminal"></i> 操作输出
|
||||
</div>
|
||||
<div class="log-box" id="logo-output">等待操作...</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== Tab: 通讯参数 ===== -->
|
||||
<div class="tab-panel hidden" id="tab-comm">
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__header">
|
||||
<i class="fa fa-plug"></i> 通讯参数配置
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<div class="empty-placeholder">
|
||||
<i class="fa fa-cogs"></i>
|
||||
<div class="empty-title">暂未配置</div>
|
||||
<div class="empty-desc">通讯参数配置功能正在开发中,敬请期待</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script src="lj360_config.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
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);
|
||||
});
|
||||
16
lj360/manifest.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"main": "lj360.html",
|
||||
"menu": {
|
||||
"lj360-service": {
|
||||
"label": "启动管理",
|
||||
"path": "lj360.html",
|
||||
"order": 10
|
||||
},
|
||||
"lj360-config": {
|
||||
"label": "参数管理",
|
||||
"path": "lj360_config.html",
|
||||
"order": 20
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
2
motd/inactive.motd
Normal file
@@ -0,0 +1,2 @@
|
||||
Activate the web console with: systemctl enable --now cockpit.socket
|
||||
|
||||
24
motd/update-motd
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/bin/sh -e
|
||||
|
||||
# syntax: update-motd [port [hostname [ipaddr [protocol]]]]
|
||||
# each argument can be given as the empty string to use the default
|
||||
|
||||
# port number from cmdline, then systemctl file, then 9090
|
||||
# take the last Listen line; this will be the user-specified one
|
||||
port=${1:-$(systemctl show --property Listen cockpit.socket |
|
||||
sed -E '$!d;$s/.*[^0-9]([0-9]+).*/\1/;')}
|
||||
port=${port:-9090}
|
||||
|
||||
# hostname from cmdline, then `hostname -f`
|
||||
hostname=${2:-$(hostname -f || hostname)}
|
||||
|
||||
# ip addr from cmdline, then default route source addr
|
||||
ip=${3:-$(ip -o route get 255.0 2>/dev/null | sed -e 's/.*src \([^ ]*\) .*/\1/')}
|
||||
|
||||
# protocol from cmdline, then https
|
||||
protocol=${4:-https}
|
||||
|
||||
hostname_url="${protocol}://${hostname}:${port}/"
|
||||
ip_url="${ip:+ or ${protocol}://${ip}:${port}/}"
|
||||
|
||||
printf 'Web console: %s%s\n\n' "${hostname_url}" "${ip_url}" > /run/cockpit/active.motd
|
||||
BIN
networkmanager/firewall.css.gz
Normal file
BIN
networkmanager/firewall.html.gz
Normal file
BIN
networkmanager/firewall.min.js.gz
Normal file
BIN
networkmanager/index.html.gz
Normal file
37
networkmanager/manifest.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"version": "215",
|
||||
"name": "network",
|
||||
"requires": {
|
||||
"cockpit": "186"
|
||||
},
|
||||
|
||||
"menu": {
|
||||
"index": {
|
||||
"label": "Networking",
|
||||
"order": 40,
|
||||
"docs": [
|
||||
{
|
||||
"label": "Managing networking",
|
||||
"url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/managing-networking-in-the-web-console_system-management-using-the-rhel-8-web-console"
|
||||
},
|
||||
{
|
||||
"label": "Managing firewall",
|
||||
"url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/using-the-web-console-for-managing-firewall_system-management-using-the-rhel-8-web-console"
|
||||
}
|
||||
],
|
||||
"keywords": [
|
||||
{
|
||||
"matches": ["network", "interface", "bridge", "vlan", "bond", "team", "port", "mac", "ipv4", "ipv6"]
|
||||
},
|
||||
{
|
||||
"matches": ["firewall", "zone", "tcp", "udp"],
|
||||
"goto": "/network/firewall"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
"hacks": {
|
||||
"with_networkmanager_needs_root": "yes"
|
||||
}
|
||||
}
|
||||