Add PACS image mount cache refresh settings
This commit is contained in:
@@ -44,6 +44,9 @@ const state = {
|
||||
token: localStorage.getItem("dicom_upp_registration_token") || "",
|
||||
user: null,
|
||||
links: { pacs_viewer_url: "http://127.0.0.1:8107", relation_viewer_url: "http://127.0.0.1:8108" },
|
||||
settings: null,
|
||||
cacheStatus: null,
|
||||
cachePollTimer: 0,
|
||||
statusFilter: "",
|
||||
partFilter: "",
|
||||
modelFilter: "",
|
||||
@@ -224,6 +227,7 @@ async function bootstrap() {
|
||||
}
|
||||
buildPoseControls();
|
||||
await loadStatus();
|
||||
await loadSettings();
|
||||
await loadCases();
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
@@ -251,6 +255,69 @@ async function loadStatus() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
try {
|
||||
const payload = await api("/api/settings");
|
||||
state.settings = payload.refresh || {};
|
||||
state.cacheStatus = payload.cache || {};
|
||||
renderSettings();
|
||||
pollCacheWhileRunning();
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
}
|
||||
|
||||
function renderSettings() {
|
||||
const settings = state.settings || {};
|
||||
const cache = state.cacheStatus || {};
|
||||
$("autoRefreshEnabled").checked = settings.auto_refresh_enabled !== false;
|
||||
$("refreshTime").value = settings.refresh_time || "03:00";
|
||||
$("cacheRootText").textContent = cache.cache_root || settings.cache_root || "";
|
||||
$("cacheStage").textContent = cache.running ? "刷新中" : cache.stage === "finished" ? "已完成" : cache.stage === "error" ? "异常" : "待刷新";
|
||||
$("cacheStage").className = `status-pill ${cache.stage === "error" ? "offline" : cache.running || cache.stage === "finished" ? "online" : ""}`;
|
||||
const progress = Math.max(0, Math.min(100, Number(cache.progress || 0)));
|
||||
$("cacheProgressFill").style.width = `${progress}%`;
|
||||
$("cacheMessage").textContent = cache.message || "等待刷新";
|
||||
$("cacheCases").textContent = `${Number(cache.processed_cases || 0)} / ${Number(cache.total_cases || 0)} CT`;
|
||||
$("cacheFiles").textContent = `STL 复制 ${Number(cache.copied_files || 0)} · 复用 ${Number(cache.skipped_files || 0)} · 缺失 ${Number(cache.missing_files || 0)}`;
|
||||
$("cacheNextRun").textContent = cache.next_scheduled_at ? `下次 ${cache.next_scheduled_at}` : "下次 -";
|
||||
$("manualCacheRefreshBtn").disabled = Boolean(cache.running);
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
const payload = {
|
||||
auto_refresh_enabled: $("autoRefreshEnabled").checked,
|
||||
refresh_time: $("refreshTime").value || "03:00",
|
||||
};
|
||||
const result = await api("/api/settings", { method: "POST", body: JSON.stringify(payload) });
|
||||
state.settings = result.refresh || payload;
|
||||
state.cacheStatus = result.cache || state.cacheStatus;
|
||||
renderSettings();
|
||||
}
|
||||
|
||||
async function refreshCacheNow() {
|
||||
$("manualCacheRefreshBtn").disabled = true;
|
||||
const result = await api("/api/cache/refresh", { method: "POST", body: JSON.stringify({}) });
|
||||
state.cacheStatus = result.cache || state.cacheStatus;
|
||||
renderSettings();
|
||||
pollCacheWhileRunning(true);
|
||||
}
|
||||
|
||||
function pollCacheWhileRunning(force = false) {
|
||||
window.clearTimeout(state.cachePollTimer);
|
||||
const running = Boolean(state.cacheStatus?.running);
|
||||
if (!running && !force) return;
|
||||
state.cachePollTimer = window.setTimeout(async () => {
|
||||
try {
|
||||
state.cacheStatus = await api("/api/cache/status");
|
||||
renderSettings();
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
pollCacheWhileRunning();
|
||||
}, running ? 1600 : 800);
|
||||
}
|
||||
|
||||
async function loadCases(selectFirst = true) {
|
||||
setTopLoading(true);
|
||||
try {
|
||||
@@ -1924,8 +1991,16 @@ function wireEvents() {
|
||||
});
|
||||
$("refreshBtn").addEventListener("click", async () => {
|
||||
await loadStatus();
|
||||
await loadSettings();
|
||||
await loadCases(false);
|
||||
});
|
||||
$("settingsBtn").addEventListener("click", async () => {
|
||||
$("settingsPanel").classList.remove("hidden");
|
||||
await loadSettings();
|
||||
});
|
||||
$("settingsCloseBtn").addEventListener("click", () => $("settingsPanel").classList.add("hidden"));
|
||||
$("saveSettingsBtn").addEventListener("click", saveSettings);
|
||||
$("manualCacheRefreshBtn").addEventListener("click", refreshCacheNow);
|
||||
$("relationBtn").addEventListener("click", () => openLinkedApp("relation"));
|
||||
$("viewerBtn").addEventListener("click", () => openLinkedApp("viewer"));
|
||||
$("fitBtn").addEventListener("click", fitCamera);
|
||||
|
||||
@@ -40,12 +40,60 @@
|
||||
<button id="viewerBtn" class="ghost-btn" type="button">DICOM 阅片系统</button>
|
||||
<span id="dbStatus" class="status-pill">数据库</span>
|
||||
<button id="refreshBtn" class="ghost-btn" type="button">刷新</button>
|
||||
<button id="settingsBtn" class="ghost-btn" type="button">设置</button>
|
||||
<span id="userBadge" class="user-badge">未登录</span>
|
||||
<button id="logoutBtn" class="ghost-btn" type="button">退出</button>
|
||||
</div>
|
||||
</header>
|
||||
<div id="topLoading" class="top-loading hidden"><span></span></div>
|
||||
|
||||
<section id="settingsPanel" class="settings-panel hidden" aria-label="系统设置">
|
||||
<div class="settings-shell">
|
||||
<div class="settings-head">
|
||||
<div>
|
||||
<h2>系统设置</h2>
|
||||
<p>DICOM / UPP 数据刷新与配准缓存</p>
|
||||
</div>
|
||||
<button id="settingsCloseBtn" class="ghost-btn" type="button">关闭</button>
|
||||
</div>
|
||||
<div class="settings-grid">
|
||||
<section class="settings-card">
|
||||
<div class="settings-card-head">
|
||||
<h3>自动刷新</h3>
|
||||
<label class="switch-line">
|
||||
<input id="autoRefreshEnabled" type="checkbox" />
|
||||
<span>启用</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<label for="refreshTime">每日刷新时间</label>
|
||||
<input id="refreshTime" type="time" value="03:00" />
|
||||
</div>
|
||||
<div class="settings-actions">
|
||||
<button id="saveSettingsBtn" class="primary-btn" type="button">保存设置</button>
|
||||
<button id="manualCacheRefreshBtn" class="ghost-btn" type="button">立刻刷新缓存</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="settings-card">
|
||||
<div class="settings-card-head">
|
||||
<h3>缓存状态</h3>
|
||||
<span id="cacheStage" class="status-pill">未刷新</span>
|
||||
</div>
|
||||
<div class="cache-progress">
|
||||
<span id="cacheProgressFill"></span>
|
||||
</div>
|
||||
<div id="cacheMessage" class="cache-message">等待刷新</div>
|
||||
<div class="settings-metrics">
|
||||
<span id="cacheCases">0 / 0 CT</span>
|
||||
<span id="cacheFiles">STL 0</span>
|
||||
<span id="cacheNextRun">下次 -</span>
|
||||
</div>
|
||||
<code id="cacheRootText" class="cache-root"></code>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<main class="workspace">
|
||||
<aside class="case-panel panel">
|
||||
<div class="panel-head">
|
||||
|
||||
@@ -98,6 +98,146 @@ button {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.settings-panel {
|
||||
position: fixed;
|
||||
inset: 66px 0 0 0;
|
||||
z-index: 18;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
background: rgba(7, 10, 15, 0.62);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.settings-shell {
|
||||
width: min(760px, calc(100vw - 24px));
|
||||
height: 100%;
|
||||
padding: 18px;
|
||||
border-left: 1px solid var(--line);
|
||||
background: linear-gradient(180deg, rgba(15, 22, 32, 0.98), rgba(8, 13, 20, 0.98));
|
||||
box-shadow: -24px 0 54px rgba(0, 0, 0, 0.38);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.settings-head,
|
||||
.settings-card-head,
|
||||
.settings-actions,
|
||||
.settings-metrics {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.settings-head {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.settings-head h2,
|
||||
.settings-card h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.settings-head h2 {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.settings-head p {
|
||||
margin: 5px 0 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
min-width: 0;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
padding: 16px;
|
||||
background: rgba(10, 16, 24, 0.9);
|
||||
}
|
||||
|
||||
.settings-card h3 {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.switch-line {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.settings-row {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin: 18px 0;
|
||||
}
|
||||
|
||||
.settings-row label {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.settings-row input {
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
padding: 0 12px;
|
||||
color: var(--text);
|
||||
background: rgba(6, 10, 15, 0.92);
|
||||
}
|
||||
|
||||
.settings-actions {
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.cache-progress {
|
||||
height: 10px;
|
||||
margin: 18px 0 12px;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: rgba(148, 163, 184, 0.18);
|
||||
}
|
||||
|
||||
.cache-progress span {
|
||||
display: block;
|
||||
width: 0%;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, var(--cyan), var(--blue));
|
||||
transition: width 0.25s ease;
|
||||
}
|
||||
|
||||
.cache-message {
|
||||
min-height: 42px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.settings-metrics {
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cache-root {
|
||||
display: block;
|
||||
margin-top: 14px;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
color: #bce7ff;
|
||||
background: rgba(6, 10, 15, 0.88);
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.top-actions .ghost-btn,
|
||||
.viewer-tools .ghost-btn,
|
||||
.viewer-tools .primary-btn,
|
||||
@@ -1817,4 +1957,8 @@ input[type="range"] {
|
||||
overflow-x: auto;
|
||||
grid-template-columns: 320px 560px 420px;
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user