整合去雾网页工具
This commit is contained in:
265
web_dehaze/static/app.js
Normal file
265
web_dehaze/static/app.js
Normal file
@@ -0,0 +1,265 @@
|
||||
const state = {
|
||||
images: [],
|
||||
capabilities: null,
|
||||
selectedImage: null,
|
||||
currentJob: null,
|
||||
pollTimer: null,
|
||||
};
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
function assetUrl(path) {
|
||||
return `/asset?path=${encodeURIComponent(path)}&t=${Date.now()}`;
|
||||
}
|
||||
|
||||
async function api(path, options = {}) {
|
||||
const response = await fetch(path, options);
|
||||
const payload = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(payload.error || response.statusText);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (!Number.isFinite(bytes)) return "";
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
function renderImages() {
|
||||
const box = $("imageList");
|
||||
box.innerHTML = "";
|
||||
state.images.forEach((image) => {
|
||||
const item = document.createElement("button");
|
||||
item.className = `image-item ${state.selectedImage === image.name ? "active" : ""}`;
|
||||
item.type = "button";
|
||||
item.innerHTML = `
|
||||
<span>${image.name}</span>
|
||||
<span class="image-meta">${image.width || "?"}x${image.height || "?"} · ${formatBytes(image.size)}</span>
|
||||
`;
|
||||
item.addEventListener("click", () => selectImage(image.name));
|
||||
box.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function renderMethods() {
|
||||
const box = $("methodList");
|
||||
box.innerHTML = "";
|
||||
const methods = state.capabilities?.methods || {};
|
||||
Object.entries(methods).forEach(([key, info]) => {
|
||||
const label = document.createElement("label");
|
||||
label.className = `check-item ${info.available ? "" : "disabled"}`;
|
||||
label.innerHTML = `
|
||||
<span>
|
||||
<strong>${info.label}</strong>
|
||||
<span class="method-meta">${info.available ? "ready" : `缺 ${info.module_ok ? "模型" : info.module}`}</span>
|
||||
</span>
|
||||
<input type="checkbox" value="${key}" ${info.available || key === "Baidu_API" || key === "DCP" ? "" : "disabled"} ${key === "DCP" ? "checked" : ""} />
|
||||
`;
|
||||
box.appendChild(label);
|
||||
});
|
||||
}
|
||||
|
||||
function renderPostprocessors() {
|
||||
const box = $("postList");
|
||||
box.innerHTML = "";
|
||||
const processors = state.capabilities?.postprocessors || {};
|
||||
Object.entries(processors).forEach(([key, info]) => {
|
||||
const label = document.createElement("label");
|
||||
label.className = "check-item";
|
||||
label.innerHTML = `
|
||||
<span><strong>${info.label}</strong></span>
|
||||
<input type="checkbox" value="${key}" ${key === "manual_sv" ? "checked" : ""} />
|
||||
`;
|
||||
box.appendChild(label);
|
||||
});
|
||||
}
|
||||
|
||||
function renderReferenceOptions() {
|
||||
const select = $("referenceImage");
|
||||
select.innerHTML = "";
|
||||
state.images.forEach((image) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = image.name;
|
||||
option.textContent = image.name;
|
||||
option.selected = image.name === state.selectedImage;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
function setJobState(text, status = "") {
|
||||
const box = $("jobState");
|
||||
box.textContent = text;
|
||||
box.className = `job-state ${status}`;
|
||||
}
|
||||
|
||||
async function selectImage(name) {
|
||||
state.selectedImage = name;
|
||||
$("currentTitle").textContent = name;
|
||||
renderImages();
|
||||
renderReferenceOptions();
|
||||
await refreshResults();
|
||||
}
|
||||
|
||||
function resultCard(title, path, exists = true) {
|
||||
const card = document.createElement("article");
|
||||
card.className = "result-card";
|
||||
const badge = exists ? '<span class="badge ok">ready</span>' : '<span class="badge pending">未生成</span>';
|
||||
const body = exists
|
||||
? `<div class="image-frame"><img src="${assetUrl(path)}" alt="${title}" loading="lazy" /></div>`
|
||||
: '<div class="image-frame"><span class="missing">暂无结果</span></div>';
|
||||
card.innerHTML = `<header><h3>${title}</h3>${badge}</header>${body}`;
|
||||
return card;
|
||||
}
|
||||
|
||||
function updatePostSourceOptions(results) {
|
||||
const select = $("postSource");
|
||||
const previous = select.value;
|
||||
select.innerHTML = "";
|
||||
const original = document.createElement("option");
|
||||
original.value = "original";
|
||||
original.textContent = "原图";
|
||||
select.appendChild(original);
|
||||
results.dehaze.filter((item) => item.exists).forEach((item) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = item.method;
|
||||
option.textContent = item.label;
|
||||
select.appendChild(option);
|
||||
});
|
||||
if ([...select.options].some((option) => option.value === previous)) {
|
||||
select.value = previous;
|
||||
} else if (results.dehaze.some((item) => item.method === "DCP" && item.exists)) {
|
||||
select.value = "DCP";
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshResults() {
|
||||
if (!state.selectedImage) return;
|
||||
const results = await api(`/api/results?image=${encodeURIComponent(state.selectedImage)}`);
|
||||
const grid = $("resultGrid");
|
||||
grid.innerHTML = "";
|
||||
grid.appendChild(resultCard(results.original.label, results.original.path, true));
|
||||
results.dehaze.forEach((item) => {
|
||||
grid.appendChild(resultCard(item.label, item.path, item.exists));
|
||||
});
|
||||
results.postprocess.forEach((item) => {
|
||||
grid.appendChild(resultCard(item.name, item.path, true));
|
||||
});
|
||||
updatePostSourceOptions(results);
|
||||
}
|
||||
|
||||
function selectedValues(containerId) {
|
||||
return [...$(containerId).querySelectorAll("input[type=checkbox]:checked")].map((input) => input.value);
|
||||
}
|
||||
|
||||
function postParams() {
|
||||
return {
|
||||
manual_sv: {
|
||||
s_gain: Number($("sGain").value),
|
||||
v_gain: Number($("vGain").value),
|
||||
},
|
||||
hsv_hist: {
|
||||
match_hue: $("matchHue").checked,
|
||||
},
|
||||
hist_auto_sv: {
|
||||
match_hue: $("matchHue").checked,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function startJob(endpoint, payload) {
|
||||
const response = await api(endpoint, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
state.currentJob = response.job.id;
|
||||
$("logBox").textContent = "";
|
||||
setJobState("Running", "running");
|
||||
pollJob();
|
||||
}
|
||||
|
||||
async function pollJob() {
|
||||
if (!state.currentJob) return;
|
||||
clearTimeout(state.pollTimer);
|
||||
try {
|
||||
const job = await api(`/api/job?id=${encodeURIComponent(state.currentJob)}`);
|
||||
$("logBox").textContent = (job.logs || []).join("\n");
|
||||
$("logBox").scrollTop = $("logBox").scrollHeight;
|
||||
if (job.status === "running") {
|
||||
setJobState("Running", "running");
|
||||
state.pollTimer = setTimeout(pollJob, 1000);
|
||||
} else {
|
||||
setJobState(job.status === "done" ? "Done" : "Error", job.status);
|
||||
await refreshResults();
|
||||
}
|
||||
} catch (error) {
|
||||
setJobState("Error", "error");
|
||||
$("logBox").textContent = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function runSelectedModels() {
|
||||
if (!state.selectedImage) return;
|
||||
const methods = selectedValues("methodList");
|
||||
const payload = {
|
||||
image: state.selectedImage,
|
||||
methods,
|
||||
options: {
|
||||
DCP: {
|
||||
sz: Number($("dcpSz").value),
|
||||
tx: Number($("dcpTx").value),
|
||||
},
|
||||
},
|
||||
};
|
||||
await startJob("/api/run", payload);
|
||||
}
|
||||
|
||||
async function runPostprocess() {
|
||||
if (!state.selectedImage) return;
|
||||
const processors = selectedValues("postList");
|
||||
const payload = {
|
||||
image: state.selectedImage,
|
||||
source: $("postSource").value,
|
||||
reference: $("referenceImage").value || state.selectedImage,
|
||||
processors,
|
||||
params: postParams(),
|
||||
};
|
||||
await startJob("/api/postprocess", payload);
|
||||
}
|
||||
|
||||
function bindControls() {
|
||||
$("runBtn").addEventListener("click", runSelectedModels);
|
||||
$("postBtn").addEventListener("click", runPostprocess);
|
||||
$("refreshBtn").addEventListener("click", refreshResults);
|
||||
$("sGain").addEventListener("input", () => {
|
||||
$("sGainValue").textContent = `${Math.round(Number($("sGain").value) * 100)}%`;
|
||||
});
|
||||
$("vGain").addEventListener("input", () => {
|
||||
$("vGainValue").textContent = `${Math.round(Number($("vGain").value) * 100)}%`;
|
||||
});
|
||||
}
|
||||
|
||||
async function init() {
|
||||
bindControls();
|
||||
const [capabilities, images] = await Promise.all([api("/api/capabilities"), api("/api/images")]);
|
||||
state.capabilities = capabilities;
|
||||
state.images = images.images || [];
|
||||
$("envLine").textContent = capabilities.results_dir;
|
||||
renderMethods();
|
||||
renderPostprocessors();
|
||||
renderImages();
|
||||
if (state.images.length) {
|
||||
await selectImage(state.images[0].name);
|
||||
} else {
|
||||
$("currentTitle").textContent = "待去雾图片为空";
|
||||
}
|
||||
setJobState("Idle");
|
||||
}
|
||||
|
||||
init().catch((error) => {
|
||||
setJobState("Error", "error");
|
||||
$("logBox").textContent = error.stack || error.message;
|
||||
});
|
||||
93
web_dehaze/static/index.html
Normal file
93
web_dehaze/static/index.html
Normal file
@@ -0,0 +1,93 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Dehaze Console</title>
|
||||
<link rel="stylesheet" href="/static/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell">
|
||||
<aside class="control-panel">
|
||||
<div class="brand">
|
||||
<span class="brand-mark"></span>
|
||||
<div>
|
||||
<h1>Dehaze Console</h1>
|
||||
<p id="envLine">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="panel-section">
|
||||
<h2>待去雾图片</h2>
|
||||
<div id="imageList" class="image-list"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel-section">
|
||||
<h2>模型</h2>
|
||||
<div id="methodList" class="check-grid"></div>
|
||||
<div class="param-grid">
|
||||
<label>
|
||||
<span>DCP 窗口</span>
|
||||
<input id="dcpSz" type="number" min="3" max="99" step="1" value="10" />
|
||||
</label>
|
||||
<label>
|
||||
<span>DCP tx</span>
|
||||
<input id="dcpTx" type="number" min="0.01" max="1" step="0.01" value="0.20" />
|
||||
</label>
|
||||
</div>
|
||||
<button id="runBtn" class="primary-btn">运行选中模型</button>
|
||||
</section>
|
||||
|
||||
<section class="panel-section">
|
||||
<h2>后处理</h2>
|
||||
<label class="field">
|
||||
<span>源图</span>
|
||||
<select id="postSource"></select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>参考图</span>
|
||||
<select id="referenceImage"></select>
|
||||
</label>
|
||||
<div id="postList" class="check-grid compact"></div>
|
||||
<div class="slider-row">
|
||||
<span>S</span>
|
||||
<input id="sGain" type="range" min="0" max="2.5" step="0.01" value="1" />
|
||||
<strong id="sGainValue">100%</strong>
|
||||
</div>
|
||||
<div class="slider-row">
|
||||
<span>V</span>
|
||||
<input id="vGain" type="range" min="0" max="2.5" step="0.01" value="1" />
|
||||
<strong id="vGainValue">100%</strong>
|
||||
</div>
|
||||
<label class="toggle-line">
|
||||
<input id="matchHue" type="checkbox" />
|
||||
<span>匹配 H 通道</span>
|
||||
</label>
|
||||
<button id="postBtn" class="secondary-btn">生成后处理</button>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<section class="workspace">
|
||||
<header class="topbar">
|
||||
<div>
|
||||
<p class="eyebrow">当前图片</p>
|
||||
<h2 id="currentTitle">未选择</h2>
|
||||
</div>
|
||||
<div id="jobState" class="job-state">Idle</div>
|
||||
</header>
|
||||
|
||||
<section id="resultGrid" class="result-grid"></section>
|
||||
|
||||
<section class="log-panel">
|
||||
<div class="log-head">
|
||||
<h2>日志</h2>
|
||||
<button id="refreshBtn" class="text-btn">刷新</button>
|
||||
</div>
|
||||
<pre id="logBox"></pre>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/static/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
428
web_dehaze/static/style.css
Normal file
428
web_dehaze/static/style.css
Normal file
@@ -0,0 +1,428 @@
|
||||
:root {
|
||||
--paper: #f4f0e8;
|
||||
--panel: #fffaf1;
|
||||
--ink: #1e2520;
|
||||
--muted: #6d756f;
|
||||
--line: #d8d0c3;
|
||||
--green: #1f6b57;
|
||||
--green-dark: #124839;
|
||||
--cobalt: #295f9f;
|
||||
--amber: #c1842d;
|
||||
--red: #b3473f;
|
||||
--shadow: 0 18px 60px rgba(33, 37, 31, 0.12);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
color: var(--ink);
|
||||
background:
|
||||
linear-gradient(90deg, rgba(31, 107, 87, 0.06) 1px, transparent 1px),
|
||||
linear-gradient(0deg, rgba(31, 107, 87, 0.05) 1px, transparent 1px),
|
||||
var(--paper);
|
||||
background-size: 28px 28px;
|
||||
font-family: "Aptos", "Noto Sans SC", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.shell {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(300px, 360px) 1fr;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
padding: 24px 20px;
|
||||
border-right: 1px solid var(--line);
|
||||
background: rgba(255, 250, 241, 0.94);
|
||||
box-shadow: var(--shadow);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding-bottom: 22px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border: 2px solid var(--ink);
|
||||
background:
|
||||
linear-gradient(135deg, transparent 46%, var(--ink) 47%, var(--ink) 53%, transparent 54%),
|
||||
linear-gradient(45deg, var(--green) 0 48%, var(--amber) 48% 100%);
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.brand p,
|
||||
.eyebrow {
|
||||
margin-top: 6px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.panel-section {
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.panel-section h2,
|
||||
.log-head h2 {
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.image-list,
|
||||
.check-grid {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.image-item,
|
||||
.check-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
min-height: 42px;
|
||||
padding: 9px 10px;
|
||||
border: 1px solid var(--line);
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
|
||||
.image-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.image-item.active {
|
||||
border-color: var(--green);
|
||||
background: rgba(31, 107, 87, 0.12);
|
||||
}
|
||||
|
||||
.image-meta,
|
||||
.method-meta {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.check-item.disabled {
|
||||
color: var(--muted);
|
||||
background: rgba(216, 208, 195, 0.34);
|
||||
}
|
||||
|
||||
.check-item input {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
accent-color: var(--green);
|
||||
}
|
||||
|
||||
.compact {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.param-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.param-grid label,
|
||||
.field {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.param-grid input,
|
||||
.field select {
|
||||
width: 100%;
|
||||
min-height: 38px;
|
||||
border: 1px solid var(--line);
|
||||
background: #fffdf8;
|
||||
color: var(--ink);
|
||||
padding: 7px 8px;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.primary-btn,
|
||||
.secondary-btn,
|
||||
.text-btn {
|
||||
min-height: 42px;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.primary-btn,
|
||||
.secondary-btn {
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.primary-btn {
|
||||
background: var(--green);
|
||||
}
|
||||
|
||||
.primary-btn:hover {
|
||||
background: var(--green-dark);
|
||||
}
|
||||
|
||||
.secondary-btn {
|
||||
background: var(--cobalt);
|
||||
}
|
||||
|
||||
.secondary-btn:hover {
|
||||
background: #1e4d83;
|
||||
}
|
||||
|
||||
.text-btn {
|
||||
padding: 0 12px;
|
||||
border-color: var(--line);
|
||||
background: #fffdf8;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.slider-row {
|
||||
display: grid;
|
||||
grid-template-columns: 20px 1fr 52px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.slider-row input {
|
||||
accent-color: var(--green);
|
||||
}
|
||||
|
||||
.slider-row strong {
|
||||
color: var(--ink);
|
||||
font-size: 12px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.toggle-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.toggle-line input {
|
||||
accent-color: var(--green);
|
||||
}
|
||||
|
||||
.workspace {
|
||||
min-width: 0;
|
||||
padding: 26px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.topbar h2 {
|
||||
margin-top: 4px;
|
||||
font-size: clamp(22px, 3vw, 38px);
|
||||
line-height: 1.05;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.job-state {
|
||||
min-width: 92px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--line);
|
||||
background: rgba(255, 250, 241, 0.82);
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.job-state.running {
|
||||
color: var(--cobalt);
|
||||
border-color: rgba(41, 95, 159, 0.35);
|
||||
}
|
||||
|
||||
.job-state.error {
|
||||
color: var(--red);
|
||||
border-color: rgba(179, 71, 63, 0.4);
|
||||
}
|
||||
|
||||
.job-state.done {
|
||||
color: var(--green);
|
||||
border-color: rgba(31, 107, 87, 0.4);
|
||||
}
|
||||
|
||||
.result-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 14px;
|
||||
max-height: calc(100vh - 300px);
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
min-height: 230px;
|
||||
border: 1px solid var(--line);
|
||||
background: rgba(255, 250, 241, 0.82);
|
||||
}
|
||||
|
||||
.result-card header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
min-height: 42px;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.result-card h3 {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.badge {
|
||||
flex: 0 0 auto;
|
||||
padding: 3px 7px;
|
||||
border: 1px solid var(--line);
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.badge.ok {
|
||||
color: var(--green);
|
||||
border-color: rgba(31, 107, 87, 0.4);
|
||||
}
|
||||
|
||||
.badge.pending {
|
||||
color: var(--amber);
|
||||
border-color: rgba(193, 132, 45, 0.42);
|
||||
}
|
||||
|
||||
.image-frame {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 188px;
|
||||
padding: 8px;
|
||||
background:
|
||||
linear-gradient(45deg, rgba(30, 37, 32, 0.05) 25%, transparent 25% 75%, rgba(30, 37, 32, 0.05) 75%),
|
||||
linear-gradient(45deg, rgba(30, 37, 32, 0.05) 25%, transparent 25% 75%, rgba(30, 37, 32, 0.05) 75%);
|
||||
background-position: 0 0, 8px 8px;
|
||||
background-size: 16px 16px;
|
||||
}
|
||||
|
||||
.image-frame img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 38vh;
|
||||
object-fit: contain;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.missing {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.log-panel {
|
||||
margin-top: 18px;
|
||||
border: 1px solid var(--line);
|
||||
background: rgba(30, 37, 32, 0.92);
|
||||
color: #e9eadf;
|
||||
}
|
||||
|
||||
.log-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 44px;
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.log-head h2 {
|
||||
margin: 0;
|
||||
color: #f7f2e8;
|
||||
}
|
||||
|
||||
.log-panel .text-btn {
|
||||
min-height: 30px;
|
||||
background: transparent;
|
||||
color: #f7f2e8;
|
||||
border-color: rgba(255, 255, 255, 0.22);
|
||||
}
|
||||
|
||||
#logBox {
|
||||
height: 172px;
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
font-family: "Cascadia Mono", "Noto Sans Mono", monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.workspace {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
align-items: start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.result-grid {
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user