feat: update web ui, docker make commands, and related docs/config
This commit is contained in:
@@ -9,16 +9,3 @@ spring.servlet.multipart.max-request-size=100MB
|
||||
# Logging settings
|
||||
logging.level.com.svnlog=INFO
|
||||
logging.level.org.springframework=INFO
|
||||
|
||||
# SVN preset settings
|
||||
svn.default-preset-id=preset-1
|
||||
svn.presets[0].id=preset-1
|
||||
svn.presets[0].name=PRS-7050??????
|
||||
svn.presets[0].url=https://10.6.221.149:48080/svn/houtai/001_????/PRS-7050??????/01_???/V1.00
|
||||
svn.presets[1].id=preset-2
|
||||
svn.presets[1].name=PRS-7950????
|
||||
svn.presets[1].url=https://10.6.221.149:48080/svn/houtai/001_????/PRS-7950????/01_???/V2.00
|
||||
svn.presets[2].id=preset-3
|
||||
svn.presets[2].name=PRS-7950??????????
|
||||
svn.presets[2].url=https://10.6.221.149:48080/svn/houtai/001_????/PRS-7950????/01_???/V1.00_20\
|
||||
24
|
||||
@@ -102,23 +102,41 @@ async function apiFetch(url, options = {}) {
|
||||
}
|
||||
|
||||
async function refreshAll() {
|
||||
try {
|
||||
const [tasksResp, filesResp, healthResp] = await Promise.all([
|
||||
apiFetch("/api/tasks"),
|
||||
apiFetch("/api/files"),
|
||||
apiFetch("/api/health/details"),
|
||||
]);
|
||||
state.tasks = (tasksResp || []).slice().sort((a, b) => sortByTimeDesc(a.createdAt, b.createdAt));
|
||||
state.files = (filesResp.files || []).slice().sort((a, b) => sortByTimeDesc(a.modifiedAt, b.modifiedAt));
|
||||
state.health = healthResp || null;
|
||||
renderDashboard();
|
||||
if (state.activeView === "history") {
|
||||
loadTaskPage();
|
||||
renderFileTable();
|
||||
}
|
||||
const [tasksResult, filesResult, healthResult] = await Promise.allSettled([
|
||||
apiFetch("/api/tasks"),
|
||||
apiFetch("/api/files"),
|
||||
apiFetch("/api/health/details"),
|
||||
]);
|
||||
|
||||
} catch (err) {
|
||||
toast(err.message, true);
|
||||
if (tasksResult.status === "fulfilled") {
|
||||
const tasksResp = tasksResult.value;
|
||||
state.tasks = (tasksResp || []).slice().sort((a, b) => sortByTimeDesc(a.createdAt, b.createdAt));
|
||||
} else {
|
||||
console.warn("refresh tasks failed:", tasksResult.reason);
|
||||
}
|
||||
|
||||
if (filesResult.status === "fulfilled") {
|
||||
const filesResp = filesResult.value || {};
|
||||
state.files = (filesResp.files || []).slice().sort((a, b) => sortByTimeDesc(a.modifiedAt, b.modifiedAt));
|
||||
if (filesResp.error) {
|
||||
console.warn("files api degraded:", filesResp.error);
|
||||
}
|
||||
} else {
|
||||
console.warn("refresh files failed:", filesResult.reason);
|
||||
state.files = [];
|
||||
}
|
||||
|
||||
if (healthResult.status === "fulfilled") {
|
||||
state.health = healthResult.value || null;
|
||||
} else {
|
||||
console.warn("refresh health failed:", healthResult.reason);
|
||||
state.health = null;
|
||||
}
|
||||
|
||||
renderDashboard();
|
||||
if (state.activeView === "history") {
|
||||
loadTaskPage();
|
||||
renderFileTable();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,6 +265,40 @@ async function waitForTaskCompletion(taskId) {
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForStreamCompletion(streamState, timeoutMs) {
|
||||
const start = Date.now();
|
||||
while (!streamState.streamCompleted && (Date.now() - start) < timeoutMs) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
return streamState.streamCompleted;
|
||||
}
|
||||
|
||||
function syncTaskAiOutput(task, streamState) {
|
||||
if (!task || !streamState) {
|
||||
return;
|
||||
}
|
||||
const reasoning = task.aiReasoningText || "";
|
||||
const answer = task.aiAnswerText || "";
|
||||
|
||||
if (reasoning.length > streamState.reasoningRenderedLength) {
|
||||
const delta = reasoning.slice(streamState.reasoningRenderedLength);
|
||||
if (delta) {
|
||||
appendReasoning(delta);
|
||||
streamState.reasoningRenderedLength = reasoning.length;
|
||||
streamState.firstDeltaReceived = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (answer.length > streamState.answerRenderedLength) {
|
||||
const delta = answer.slice(streamState.answerRenderedLength);
|
||||
if (delta) {
|
||||
appendAnswer(delta);
|
||||
streamState.answerRenderedLength = answer.length;
|
||||
streamState.firstDeltaReceived = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function onRunSvn(event) {
|
||||
event.preventDefault();
|
||||
const form = event.target;
|
||||
@@ -353,8 +405,23 @@ async function onRunSvn(event) {
|
||||
reasoningBuffer: "",
|
||||
answerBuffer: "",
|
||||
streamAvailable: true,
|
||||
streamConnected: false,
|
||||
firstEventReceived: false,
|
||||
firstDeltaReceived: false,
|
||||
streamCompleted: false,
|
||||
taskTerminal: false,
|
||||
reasoningRenderedLength: 0,
|
||||
answerRenderedLength: 0,
|
||||
lastAiStatusLogged: "",
|
||||
};
|
||||
aiStream = openTaskEventStream(aiData.taskId, {
|
||||
onOpen: () => {
|
||||
streamState.streamConnected = true;
|
||||
appendSystemLog("实时流连接已建立");
|
||||
},
|
||||
onFirstEvent: () => {
|
||||
streamState.firstEventReceived = true;
|
||||
},
|
||||
onPhase: (payload) => {
|
||||
if (payload && payload.message) {
|
||||
appendSystemLog(payload.message);
|
||||
@@ -362,11 +429,21 @@ async function onRunSvn(event) {
|
||||
},
|
||||
onReasoning: (text) => {
|
||||
if (!text) return;
|
||||
if (!streamState.firstDeltaReceived) {
|
||||
streamState.firstDeltaReceived = true;
|
||||
appendSystemLog("已接收 AI 流式输出");
|
||||
}
|
||||
streamState.reasoningRenderedLength += text.length;
|
||||
streamState.reasoningBuffer += text;
|
||||
flushStreamBuffer(streamState, "reasoningBuffer", "reasoning", false);
|
||||
},
|
||||
onAnswer: (text) => {
|
||||
if (!text) return;
|
||||
if (!streamState.firstDeltaReceived) {
|
||||
streamState.firstDeltaReceived = true;
|
||||
appendSystemLog("已接收 AI 流式输出");
|
||||
}
|
||||
streamState.answerRenderedLength += text.length;
|
||||
streamState.answerBuffer += text;
|
||||
flushStreamBuffer(streamState, "answerBuffer", "answer", false);
|
||||
},
|
||||
@@ -375,14 +452,28 @@ async function onRunSvn(event) {
|
||||
appendSystemLog(`Token统计: prompt=${payload.promptTokens || 0}, completion=${payload.completionTokens || 0}, total=${payload.totalTokens || 0}`);
|
||||
},
|
||||
onError: (payload) => {
|
||||
if (payload && payload.detail) {
|
||||
appendSystemLog(`流式错误: ${payload.detail}`, true);
|
||||
const detail = payload && (payload.detail || payload.error || payload.message);
|
||||
if (detail) {
|
||||
appendSystemLog(`流式错误: ${detail}`, true);
|
||||
}
|
||||
},
|
||||
onTransportError: () => {
|
||||
onDone: () => {
|
||||
streamState.streamCompleted = true;
|
||||
appendSystemLog("流式输出已结束");
|
||||
},
|
||||
onTransportError: (meta) => {
|
||||
if (streamState.taskTerminal || streamState.streamCompleted) {
|
||||
return;
|
||||
}
|
||||
if (streamState.streamAvailable) {
|
||||
streamState.streamAvailable = false;
|
||||
appendSystemLog("实时流中断,已回退到轮询模式");
|
||||
if (meta && !meta.opened) {
|
||||
appendSystemLog("实时流连接失败,已回退到轮询模式", true);
|
||||
} else if (meta && !meta.firstEventReceived) {
|
||||
appendSystemLog("实时流未收到事件即中断,已回退到轮询模式", true);
|
||||
} else {
|
||||
appendSystemLog("实时流中断,已回退到轮询模式", true);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -390,17 +481,32 @@ async function onRunSvn(event) {
|
||||
// 等待AI任务完成,实时显示日志
|
||||
while (true) {
|
||||
const task = await apiFetch(`/api/tasks/${encodeURIComponent(aiData.taskId)}`);
|
||||
syncTaskAiOutput(task, streamState);
|
||||
if (!streamState.streamAvailable && task.aiStreamStatus && task.aiStreamStatus !== streamState.lastAiStatusLogged) {
|
||||
streamState.lastAiStatusLogged = task.aiStreamStatus;
|
||||
appendSystemLog(`轮询回显状态: ${task.aiStreamStatus}`);
|
||||
}
|
||||
if (task.status === "SUCCESS") {
|
||||
streamState.taskTerminal = true;
|
||||
flushStreamBuffer(streamState, "reasoningBuffer", "reasoning", true);
|
||||
flushStreamBuffer(streamState, "answerBuffer", "answer", true);
|
||||
syncTaskAiOutput(task, streamState);
|
||||
appendSystemLog("AI分析完成");
|
||||
if (task.message) appendSystemLog(task.message);
|
||||
if (!streamState.streamCompleted) {
|
||||
const completed = await waitForStreamCompletion(streamState, 2500);
|
||||
if (!completed) {
|
||||
appendSystemLog("流式收尾超时,已使用轮询结果完成任务");
|
||||
}
|
||||
}
|
||||
aiStream.close();
|
||||
break;
|
||||
}
|
||||
if (task.status === "FAILED" || task.status === "CANCELLED") {
|
||||
streamState.taskTerminal = true;
|
||||
flushStreamBuffer(streamState, "reasoningBuffer", "reasoning", true);
|
||||
flushStreamBuffer(streamState, "answerBuffer", "answer", true);
|
||||
syncTaskAiOutput(task, streamState);
|
||||
aiStream.close();
|
||||
throw new Error(`AI分析失败: ${task.error || task.message}`);
|
||||
}
|
||||
@@ -450,6 +556,11 @@ function openTaskEventStream(taskId, handlers = {}) {
|
||||
|
||||
const streamUrl = `/api/tasks/${encodeURIComponent(taskId)}/stream`;
|
||||
const source = new EventSource(streamUrl);
|
||||
const meta = {
|
||||
opened: false,
|
||||
done: false,
|
||||
firstEventReceived: false,
|
||||
};
|
||||
const parse = (event) => {
|
||||
try {
|
||||
return JSON.parse(event.data || "{}");
|
||||
@@ -457,26 +568,56 @@ function openTaskEventStream(taskId, handlers = {}) {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
const markFirstEvent = () => {
|
||||
if (meta.firstEventReceived) {
|
||||
return;
|
||||
}
|
||||
meta.firstEventReceived = true;
|
||||
handlers.onFirstEvent && handlers.onFirstEvent();
|
||||
};
|
||||
|
||||
source.onopen = () => {
|
||||
meta.opened = true;
|
||||
handlers.onOpen && handlers.onOpen();
|
||||
};
|
||||
|
||||
source.addEventListener("phase", (event) => {
|
||||
markFirstEvent();
|
||||
handlers.onPhase && handlers.onPhase(parse(event));
|
||||
});
|
||||
source.addEventListener("reasoning_delta", (event) => {
|
||||
markFirstEvent();
|
||||
const payload = parse(event);
|
||||
handlers.onReasoning && handlers.onReasoning(payload.text || "");
|
||||
});
|
||||
source.addEventListener("answer_delta", (event) => {
|
||||
markFirstEvent();
|
||||
const payload = parse(event);
|
||||
handlers.onAnswer && handlers.onAnswer(payload.text || "");
|
||||
});
|
||||
source.addEventListener("usage", (event) => {
|
||||
markFirstEvent();
|
||||
handlers.onUsage && handlers.onUsage(parse(event));
|
||||
});
|
||||
source.addEventListener("error", (event) => {
|
||||
markFirstEvent();
|
||||
handlers.onError && handlers.onError(parse(event));
|
||||
});
|
||||
source.addEventListener("done", (event) => {
|
||||
markFirstEvent();
|
||||
meta.done = true;
|
||||
handlers.onDone && handlers.onDone(parse(event));
|
||||
source.close();
|
||||
});
|
||||
source.onerror = () => {
|
||||
handlers.onTransportError && handlers.onTransportError();
|
||||
if (meta.done) {
|
||||
return;
|
||||
}
|
||||
handlers.onTransportError && handlers.onTransportError({
|
||||
opened: meta.opened,
|
||||
closed: source.readyState === EventSource.CLOSED,
|
||||
firstEventReceived: meta.firstEventReceived,
|
||||
});
|
||||
source.close();
|
||||
};
|
||||
|
||||
@@ -497,7 +638,15 @@ function flushStreamBuffer(streamState, key, target, force) {
|
||||
if (!shouldFlush) {
|
||||
return;
|
||||
}
|
||||
const cleaned = text.replace(/\s+/g, " ").trim();
|
||||
const cleaned = text
|
||||
.replace(/\r/g, "")
|
||||
.replace(/[ \t]+\n/g, "\n")
|
||||
.replace(/\n{3,}/g, "\n\n")
|
||||
.trim();
|
||||
if (!cleaned) {
|
||||
streamState[key] = "";
|
||||
return;
|
||||
}
|
||||
if (target === "reasoning") {
|
||||
appendReasoning(cleaned);
|
||||
} else if (target === "answer") {
|
||||
@@ -510,15 +659,17 @@ function flushStreamBuffer(streamState, key, target, force) {
|
||||
|
||||
function appendSystemLog(message, isError = false) {
|
||||
const logOutput = document.querySelector("#system-log-output");
|
||||
if (!logOutput) {
|
||||
console.warn("system-log-output not found:", message);
|
||||
return;
|
||||
}
|
||||
markPanelReady("#system-log-output");
|
||||
const p = document.createElement("p");
|
||||
const time = new Date().toLocaleTimeString("zh-CN", { hour12: false });
|
||||
p.style.margin = "2px 0";
|
||||
p.className = isError ? "log-line is-error" : "log-line is-info";
|
||||
if (isError) {
|
||||
p.style.color = "#dc2626";
|
||||
p.textContent = `[${time}] ❌ ${message}`;
|
||||
} else {
|
||||
p.style.color = "#1e293b";
|
||||
p.textContent = `[${time}] ℹ️ ${message}`;
|
||||
}
|
||||
logOutput.appendChild(p);
|
||||
@@ -526,19 +677,22 @@ function appendSystemLog(message, isError = false) {
|
||||
}
|
||||
|
||||
function appendReasoning(message) {
|
||||
appendPane("#reasoning-output", message, "#334155");
|
||||
appendPane("#reasoning-output", message, "is-reasoning");
|
||||
}
|
||||
|
||||
function appendAnswer(message) {
|
||||
appendPane("#answer-output", message, "#166534");
|
||||
appendPane("#answer-output", message, "is-answer");
|
||||
}
|
||||
|
||||
function appendPane(selector, message, color) {
|
||||
function appendPane(selector, message, toneClass) {
|
||||
const logOutput = document.querySelector(selector);
|
||||
if (!logOutput) {
|
||||
console.warn(`${selector} not found:`, message);
|
||||
return;
|
||||
}
|
||||
markPanelReady(selector);
|
||||
const p = document.createElement("p");
|
||||
p.style.margin = "2px 0";
|
||||
p.style.color = color;
|
||||
p.className = `log-line ${toneClass}`.trim();
|
||||
p.textContent = message;
|
||||
logOutput.appendChild(p);
|
||||
logOutput.scrollTop = logOutput.scrollHeight;
|
||||
@@ -854,19 +1008,34 @@ async function onAutoFillVersions() {
|
||||
try {
|
||||
for (const project of projects) {
|
||||
appendLog(`正在查询 ${project.name} 的版本范围...`);
|
||||
const traceId = `autofill-${project.presetId}-${Date.now()}`;
|
||||
const requestPayload = {
|
||||
presetId: project.presetId,
|
||||
username: "liujing2",
|
||||
password: "sunri@20230620*#&",
|
||||
year: parseInt(year),
|
||||
month: parseInt(month),
|
||||
clientTraceId: traceId,
|
||||
};
|
||||
const requestPayloadForLog = {
|
||||
presetId: requestPayload.presetId,
|
||||
username: requestPayload.username,
|
||||
password: maskSecret(requestPayload.password),
|
||||
year: requestPayload.year,
|
||||
month: requestPayload.month,
|
||||
clientTraceId: requestPayload.clientTraceId,
|
||||
};
|
||||
appendLog(`[AutoFill][Request] project=${project.name} payload=${JSON.stringify(requestPayloadForLog)}`);
|
||||
|
||||
// 调用后端接口获取月份版本范围
|
||||
const data = await apiFetch("/api/svn/version-range", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
presetId: project.presetId,
|
||||
username: "liujing2",
|
||||
password: "sunri@20230620*#&",
|
||||
year: parseInt(year),
|
||||
month: parseInt(month),
|
||||
filterUser: "liujing2@SZNARI" // 只查询该用户的提交(完整用户名)
|
||||
}),
|
||||
body: JSON.stringify(requestPayload),
|
||||
});
|
||||
appendLog(`[AutoFill][Response] traceId=${traceId} payload=${safeJsonStringify(data)}`);
|
||||
if (data && data.resolvedSvnUrl) {
|
||||
appendLog(`[AutoFill][SVN] traceId=${traceId} actualUrl=${data.resolvedSvnUrl}`);
|
||||
}
|
||||
|
||||
if (data.startRevision && data.endRevision) {
|
||||
project.startInput.value = data.startRevision;
|
||||
@@ -881,8 +1050,28 @@ async function onAutoFillVersions() {
|
||||
toast("版本号填充完成");
|
||||
} catch (err) {
|
||||
appendLog(`填充失败: ${err.message}`, true);
|
||||
appendLog(`[AutoFill][Error] detail=${err.message}`, true);
|
||||
toast(err.message, true);
|
||||
} finally {
|
||||
setLoading(btn, false);
|
||||
}
|
||||
}
|
||||
|
||||
function maskSecret(secret) {
|
||||
if (!secret) {
|
||||
return "(empty)";
|
||||
}
|
||||
return `***len=${String(secret).length}`;
|
||||
}
|
||||
|
||||
function safeJsonStringify(value) {
|
||||
try {
|
||||
const raw = JSON.stringify(value);
|
||||
if (!raw) {
|
||||
return "{}";
|
||||
}
|
||||
return raw.length > 600 ? `${raw.slice(0, 600)}...(truncated)` : raw;
|
||||
} catch (err) {
|
||||
return `\"<invalid-json:${err.message}>\"`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,391 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SVN 日志工作台</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/styles-redesign.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- 主题切换按钮 -->
|
||||
<button class="theme-toggle" id="theme-toggle" aria-label="切换主题">
|
||||
<svg class="icon-sun" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="5"/>
|
||||
<line x1="12" y1="1" x2="12" y2="3"/>
|
||||
<line x1="12" y1="21" x2="12" y2="23"/>
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
|
||||
<line x1="1" y1="12" x2="3" y2="12"/>
|
||||
<line x1="21" y1="12" x2="23" y2="12"/>
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
|
||||
</svg>
|
||||
<svg class="icon-moon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="app-container">
|
||||
<!-- 侧边栏 -->
|
||||
<aside class="sidebar" role="navigation" aria-label="主导航">
|
||||
<div class="sidebar-header">
|
||||
<svg class="logo-icon" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
|
||||
</svg>
|
||||
<h1 class="logo-text">SVN 工作台</h1>
|
||||
</div>
|
||||
|
||||
<nav class="nav-menu">
|
||||
<button class="nav-item active" data-view="dashboard">
|
||||
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="7" height="7"/>
|
||||
<rect x="14" y="3" width="7" height="7"/>
|
||||
<rect x="14" y="14" width="7" height="7"/>
|
||||
<rect x="3" y="14" width="7" height="7"/>
|
||||
</svg>
|
||||
<span>工作台</span>
|
||||
</button>
|
||||
<button class="nav-item" data-view="svn">
|
||||
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
||||
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/>
|
||||
<line x1="12" y1="22.08" x2="12" y2="12"/>
|
||||
</svg>
|
||||
<span>SVN 日志</span>
|
||||
</button>
|
||||
<button class="nav-item" data-view="ai">
|
||||
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
|
||||
<path d="M2 17l10 5 10-5"/>
|
||||
<path d="M2 12l10 5 10-5"/>
|
||||
</svg>
|
||||
<span>AI 分析</span>
|
||||
</button>
|
||||
<button class="nav-item" data-view="history">
|
||||
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<polyline points="12 6 12 12 16 14"/>
|
||||
</svg>
|
||||
<span>任务历史</span>
|
||||
</button>
|
||||
<button class="nav-item" data-view="settings">
|
||||
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M12 1v6m0 6v6m5.2-13.2l-4.2 4.2m-2 2l-4.2 4.2M23 12h-6m-6 0H5m13.2 5.2l-4.2-4.2m-2-2l-4.2-4.2"/>
|
||||
</svg>
|
||||
<span>系统设置</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<div class="status-indicator">
|
||||
<span class="status-dot"></span>
|
||||
<span class="status-text">系统正常</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<main class="main-content" id="main-content">
|
||||
<!-- 页面头部 -->
|
||||
<header class="page-header">
|
||||
<div class="header-content">
|
||||
<h2 class="page-title" id="page-title">工作台</h2>
|
||||
<p class="page-description" id="page-description">查看系统状态与最近产物</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 工作台视图 -->
|
||||
<section class="view active" id="view-dashboard" aria-live="polite">
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-grid">
|
||||
<article class="stat-card">
|
||||
<div class="stat-icon stat-icon-primary">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<p class="stat-label">任务总数</p>
|
||||
<p class="stat-value" id="stat-total">0</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="stat-card">
|
||||
<div class="stat-icon stat-icon-warning">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<polyline points="12 6 12 12 16 14"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<p class="stat-label">执行中</p>
|
||||
<p class="stat-value" id="stat-running">0</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="stat-card">
|
||||
<div class="stat-icon stat-icon-danger">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="15" y1="9" x2="9" y2="15"/>
|
||||
<line x1="9" y1="9" x2="15" y2="15"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<p class="stat-label">失败任务</p>
|
||||
<p class="stat-value" id="stat-failed">0</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="stat-card">
|
||||
<div class="stat-icon stat-icon-success">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
|
||||
<polyline points="22 4 12 14.01 9 11.01"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<p class="stat-label">系统状态</p>
|
||||
<p class="stat-value stat-value-small" id="stat-health">-</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<!-- 健康检查卡片 -->
|
||||
<article class="glass-card" id="health-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">健康检查</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted" id="health-details">加载中...</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- 最近任务和文件 -->
|
||||
<div class="grid-2">
|
||||
<article class="glass-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">最近任务</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul id="recent-tasks" class="item-list"></ul>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="glass-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">最近文件</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul id="recent-files" class="item-list"></ul>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- SVN 日志抓取视图 -->
|
||||
<section class="view" id="view-svn">
|
||||
<article class="glass-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">SVN 抓取参数</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="svn-form" class="form-layout">
|
||||
<div class="form-group">
|
||||
<label for="svn-preset-select" class="form-label">预置项目</label>
|
||||
<select name="presetId" id="svn-preset-select" class="form-select" aria-label="预置 SVN 项目"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="project-name" class="form-label">项目名</label>
|
||||
<input type="text" name="projectName" id="project-name" class="form-input" placeholder="如:PRS-7050">
|
||||
</div>
|
||||
|
||||
<div class="form-group form-group-full">
|
||||
<label for="svn-url" class="form-label">SVN 地址 <span class="required">*</span></label>
|
||||
<input type="url" name="url" id="svn-url" class="form-input" placeholder="https://..." required aria-label="SVN 地址">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="svn-username" class="form-label">账号 <span class="required">*</span></label>
|
||||
<input type="text" name="username" id="svn-username" class="form-input" placeholder="请输入账号" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="svn-password" class="form-label">密码 <span class="required">*</span></label>
|
||||
<input type="password" name="password" id="svn-password" class="form-input" placeholder="请输入密码" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="start-revision" class="form-label">开始版本号</label>
|
||||
<input type="text" name="startRevision" id="start-revision" class="form-input" inputmode="numeric" placeholder="默认最新">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="end-revision" class="form-label">结束版本号</label>
|
||||
<input type="text" name="endRevision" id="end-revision" class="form-input" inputmode="numeric" placeholder="默认最新">
|
||||
</div>
|
||||
|
||||
<div class="form-group form-group-full">
|
||||
<label for="filter-user" class="form-label">过滤用户名</label>
|
||||
<input type="text" name="filterUser" id="filter-user" class="form-input" placeholder="包含匹配,留空不过滤">
|
||||
</div>
|
||||
|
||||
<div class="form-actions form-group-full">
|
||||
<button type="button" id="btn-test-connection" class="btn btn-secondary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
|
||||
<polyline points="22 4 12 14.01 9 11.01"/>
|
||||
</svg>
|
||||
测试连接
|
||||
</button>
|
||||
<button type="submit" id="btn-svn-run" class="btn btn-primary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
||||
</svg>
|
||||
开始抓取并导出
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<!-- AI 工作量分析视图 -->
|
||||
<section class="view" id="view-ai">
|
||||
<article class="glass-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">AI 分析参数</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="ai-form" class="form-layout">
|
||||
<div class="form-group form-group-full">
|
||||
<label class="form-label">选择 Markdown 输入文件</label>
|
||||
<div class="file-picker" id="md-file-picker" role="group" aria-label="Markdown 文件选择"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="work-period" class="form-label">工作周期</label>
|
||||
<input type="text" name="period" id="work-period" class="form-input" placeholder="例如 2026年03月">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="output-filename" class="form-label">输出文件名</label>
|
||||
<input type="text" name="outputFileName" id="output-filename" class="form-input" placeholder="例如 202603工作量统计.xlsx">
|
||||
</div>
|
||||
|
||||
<div class="form-group form-group-full">
|
||||
<label for="temp-api-key" class="form-label">临时 API Key(可选)</label>
|
||||
<input type="password" name="apiKey" id="temp-api-key" class="form-input" placeholder="优先使用设置页或环境变量">
|
||||
</div>
|
||||
|
||||
<div class="form-actions form-group-full">
|
||||
<button type="submit" id="btn-ai-run" class="btn btn-primary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
|
||||
<path d="M2 17l10 5 10-5"/>
|
||||
<path d="M2 12l10 5 10-5"/>
|
||||
</svg>
|
||||
开始 AI 分析并导出 Excel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<!-- 任务历史视图 -->
|
||||
<section class="view" id="view-history">
|
||||
<article class="glass-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">任务列表</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="filter-toolbar" id="history-toolbar">
|
||||
<select id="task-filter-status" class="form-select" aria-label="状态筛选">
|
||||
<option value="">全部状态</option>
|
||||
<option value="PENDING">PENDING</option>
|
||||
<option value="RUNNING">RUNNING</option>
|
||||
<option value="SUCCESS">SUCCESS</option>
|
||||
<option value="FAILED">FAILED</option>
|
||||
<option value="CANCELLED">CANCELLED</option>
|
||||
</select>
|
||||
<select id="task-filter-type" class="form-select" aria-label="类型筛选">
|
||||
<option value="">全部类型</option>
|
||||
<option value="SVN_FETCH">SVN_FETCH</option>
|
||||
<option value="AI_ANALYZE">AI_ANALYZE</option>
|
||||
</select>
|
||||
<input id="task-filter-keyword" class="form-input" placeholder="搜索任务ID/信息" aria-label="关键词搜索">
|
||||
<button id="btn-task-filter" type="button" class="btn btn-secondary">查询</button>
|
||||
</div>
|
||||
<div id="task-table" class="table-container"></div>
|
||||
<div class="pagination" id="task-pager"></div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="glass-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">输出文件</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="file-table" class="table-container"></div>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<!-- 系统设置视图 -->
|
||||
<section class="view" id="view-settings">
|
||||
<article class="glass-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">系统设置</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="settings-form" class="form-layout">
|
||||
<div class="form-group form-group-full">
|
||||
<label for="api-key-input" class="form-label">DeepSeek API Key</label>
|
||||
<input type="password" name="apiKey" id="api-key-input" class="form-input" placeholder="设置后将保存在当前进程内存">
|
||||
</div>
|
||||
|
||||
<div class="form-group form-group-full">
|
||||
<label for="default-preset" class="form-label">默认 SVN 项目</label>
|
||||
<select name="defaultSvnPresetId" id="default-preset" class="form-select"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group form-group-full">
|
||||
<label for="output-dir" class="form-label">输出目录</label>
|
||||
<input type="text" name="outputDir" id="output-dir" class="form-input" placeholder="默认 outputs">
|
||||
</div>
|
||||
|
||||
<div class="form-actions form-group-full">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
|
||||
<polyline points="17 21 17 13 7 13 7 21"/>
|
||||
<polyline points="7 3 7 8 15 8"/>
|
||||
</svg>
|
||||
保存设置
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<p id="settings-state" class="text-muted"></p>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Toast 通知 -->
|
||||
<div class="toast-container" id="toast-container" aria-live="assertive" aria-atomic="true"></div>
|
||||
|
||||
<!-- 加载指示器 -->
|
||||
<div class="loading-overlay" id="loading-overlay">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
|
||||
<script src="/app-redesign.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -7,12 +7,22 @@
|
||||
<link rel="stylesheet" href="/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg-grid" aria-hidden="true"></div>
|
||||
<div class="bg-glow bg-glow-1" aria-hidden="true"></div>
|
||||
<div class="bg-glow bg-glow-2" aria-hidden="true"></div>
|
||||
|
||||
<div class="app-shell">
|
||||
<aside class="sidebar" aria-label="主导航">
|
||||
<h1>SVN 工作台</h1>
|
||||
<nav>
|
||||
<button class="nav-item" data-view="dashboard">工作台</button>
|
||||
<button class="nav-item active" data-view="svn">SVN 日志抓取</button>
|
||||
<div class="brand">
|
||||
<span class="brand-dot" aria-hidden="true"></span>
|
||||
<div>
|
||||
<h1>SVN 工作台</h1>
|
||||
<p>日志抓取与统计分析</p>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="nav-list">
|
||||
<button class="nav-item active" data-view="dashboard">工作台</button>
|
||||
<button class="nav-item" data-view="svn">SVN 日志抓取</button>
|
||||
<button class="nav-item" data-view="history">任务历史</button>
|
||||
<button class="nav-item" data-view="settings">系统设置</button>
|
||||
</nav>
|
||||
@@ -64,61 +74,59 @@
|
||||
<section class="view" id="view-svn">
|
||||
<article class="card form-card">
|
||||
<h3>SVN 批量抓取参数</h3>
|
||||
<div class="alert info span-2" style="margin-bottom:16px;padding:12px;border-radius:10px;background:#d1f0eb;color:#0f766e">
|
||||
默认已填充3个常用项目路径,可选择月份自动填充版本号,或手动填写
|
||||
|
||||
<div class="alert info span-2">
|
||||
默认已填充 3 个常用项目路径,可选择月份自动填充版本号,或手动填写。
|
||||
</div>
|
||||
<div class="span-2" style="margin-bottom:16px;padding:12px;border:1px solid var(--border);border-radius:10px;">
|
||||
<div class="grid cols-3" style="gap:10px;align-items:end;">
|
||||
|
||||
<div class="month-panel span-2">
|
||||
<div class="grid cols-3 month-grid">
|
||||
<label>统计月份
|
||||
<input type="month" id="version-month">
|
||||
</label>
|
||||
<div style="grid-column: span 2;">
|
||||
<button type="button" id="btn-auto-fill" class="primary" style="width:100%">一键填充所有项目版本号</button>
|
||||
<div class="span-2 month-action">
|
||||
<button type="button" id="btn-auto-fill" class="primary">一键填充所有项目版本号</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="svn-form" class="form-grid">
|
||||
<!-- 项目1 -->
|
||||
<div class="span-2 project-item" style="border:1px solid var(--border);border-radius:10px;padding:12px;margin-bottom:8px">
|
||||
<h4 style="margin:0 0 10px 0">项目 1:PRS-7050 场站智慧管控</h4>
|
||||
<div class="grid cols-2" style="gap:10px">
|
||||
<div class="span-2 project-item">
|
||||
<h4>项目 1:PRS-7050 场站智慧管控</h4>
|
||||
<div class="grid cols-2">
|
||||
<label>开始版本号<input name="startRevision_1" inputmode="numeric" placeholder="请输入开始版本"></label>
|
||||
<label>结束版本号<input name="endRevision_1" inputmode="numeric" placeholder="请输入结束版本"></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 项目2 -->
|
||||
<div class="span-2 project-item" style="border:1px solid var(--border);border-radius:10px;padding:12px;margin-bottom:8px">
|
||||
<h4 style="margin:0 0 10px 0">项目 2:PRS-7950 在线巡视</h4>
|
||||
<div class="grid cols-2" style="gap:10px">
|
||||
|
||||
<div class="span-2 project-item">
|
||||
<h4>项目 2:PRS-7950 在线巡视</h4>
|
||||
<div class="grid cols-2">
|
||||
<label>开始版本号<input name="startRevision_2" inputmode="numeric" placeholder="请输入开始版本"></label>
|
||||
<label>结束版本号<input name="endRevision_2" inputmode="numeric" placeholder="请输入结束版本"></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 项目3 -->
|
||||
<div class="span-2 project-item" style="border:1px solid var(--border);border-radius:10px;padding:12px;margin-bottom:12px">
|
||||
<h4 style="margin:0 0 10px 0">项目 3:PRS-7950 在线巡视电科院测试版</h4>
|
||||
<div class="grid cols-2" style="gap:10px">
|
||||
|
||||
<div class="span-2 project-item">
|
||||
<h4>项目 3:PRS-7950 在线巡视电科院测试版</h4>
|
||||
<div class="grid cols-2">
|
||||
<label>开始版本号<input name="startRevision_3" inputmode="numeric" placeholder="请输入开始版本"></label>
|
||||
<label>结束版本号<input name="endRevision_3" inputmode="numeric" placeholder="请输入结束版本"></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 通用配置 -->
|
||||
|
||||
<label class="span-2">过滤用户名<input name="filterUser" placeholder="包含匹配,留空不过滤" value="liujing"></label>
|
||||
<label>工作周期<input name="period" placeholder="例如 2026年03月"></label>
|
||||
<label>输出文件名<input name="outputFileName" placeholder="例如 202603工作量统计.xlsx"></label>
|
||||
|
||||
|
||||
<div class="actions span-2">
|
||||
<button type="button" id="btn-test-connection">测试连接</button>
|
||||
<button type="submit" id="btn-svn-run" class="primary">一键抓取并导出 Excel</button>
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<!-- 执行日志面板 -->
|
||||
<article class="card" id="log-panel" style="display:none;margin-top:16px;">
|
||||
|
||||
<article class="card" id="log-panel">
|
||||
<h3>执行进度</h3>
|
||||
<div class="live-grid">
|
||||
<section class="live-column reasoning">
|
||||
@@ -143,8 +151,6 @@
|
||||
</article>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
<section class="view" id="view-history">
|
||||
<article class="card">
|
||||
<h3>任务列表</h3>
|
||||
@@ -199,6 +205,6 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/app.js" defer></script>
|
||||
<script src="/app.js?v=20260407_1811" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,100 +1,216 @@
|
||||
:root {
|
||||
--bg: #eef2f5;
|
||||
--panel: #ffffff;
|
||||
--text: #122126;
|
||||
--muted: #4b5f66;
|
||||
--primary: #0f766e;
|
||||
--primary-soft: #d1f0eb;
|
||||
--danger: #b42318;
|
||||
--warning: #b54708;
|
||||
--success: #067647;
|
||||
--border: #d6e0e4;
|
||||
--shadow: 0 10px 24px rgba(12, 41, 49, 0.08);
|
||||
--bg-0: #0b1020;
|
||||
--bg-1: #121a2f;
|
||||
--bg-2: #1a2744;
|
||||
|
||||
--surface-0: rgba(19, 30, 53, 0.62);
|
||||
--surface-1: rgba(24, 37, 64, 0.84);
|
||||
--surface-2: #1f3158;
|
||||
|
||||
--text-0: #e8efff;
|
||||
--text-1: #c4d2f0;
|
||||
--text-2: #91a3cc;
|
||||
|
||||
--accent-0: #6ba6ff;
|
||||
--accent-1: #8fc0ff;
|
||||
--accent-soft: rgba(107, 166, 255, 0.16);
|
||||
|
||||
--success: #49c28a;
|
||||
--warning: #f0b85d;
|
||||
--danger: #ff7f87;
|
||||
|
||||
--border-0: rgba(150, 180, 230, 0.24);
|
||||
--border-1: rgba(150, 180, 230, 0.4);
|
||||
|
||||
--shadow-0: 0 22px 54px rgba(4, 8, 20, 0.45);
|
||||
--shadow-1: 0 10px 30px rgba(8, 14, 32, 0.36);
|
||||
|
||||
--radius-sm: 10px;
|
||||
--radius-md: 14px;
|
||||
--radius-lg: 20px;
|
||||
|
||||
--space-1: 8px;
|
||||
--space-2: 12px;
|
||||
--space-3: 16px;
|
||||
--space-4: 20px;
|
||||
--space-5: 24px;
|
||||
|
||||
--z-bg: 0;
|
||||
--z-layout: 5;
|
||||
--z-toast: 50;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
color: var(--text);
|
||||
background: radial-gradient(circle at top right, #dff4ef 0%, var(--bg) 42%, #edf1f7 100%);
|
||||
color: var(--text-0);
|
||||
background:
|
||||
radial-gradient(1200px 800px at -15% 130%, #1d2749 0%, transparent 60%),
|
||||
radial-gradient(900px 700px at 110% -10%, #1f3b67 0%, transparent 62%),
|
||||
linear-gradient(160deg, var(--bg-0) 0%, var(--bg-1) 52%, #131f38 100%);
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.bg-grid {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: var(--z-bg);
|
||||
background-image:
|
||||
linear-gradient(rgba(116, 153, 211, 0.08) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(116, 153, 211, 0.08) 1px, transparent 1px);
|
||||
background-size: 36px 36px;
|
||||
mask-image: radial-gradient(circle at 35% 15%, black 0%, transparent 78%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.bg-glow {
|
||||
position: fixed;
|
||||
border-radius: 999px;
|
||||
pointer-events: none;
|
||||
z-index: var(--z-bg);
|
||||
}
|
||||
|
||||
.bg-glow-1 {
|
||||
width: 420px;
|
||||
height: 420px;
|
||||
left: -120px;
|
||||
top: 30%;
|
||||
background: rgba(86, 135, 214, 0.18);
|
||||
filter: blur(60px);
|
||||
}
|
||||
|
||||
.bg-glow-2 {
|
||||
width: 360px;
|
||||
height: 360px;
|
||||
right: -90px;
|
||||
top: 80px;
|
||||
background: rgba(115, 168, 255, 0.12);
|
||||
filter: blur(58px);
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
position: relative;
|
||||
z-index: var(--z-layout);
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-columns: 260px 1fr;
|
||||
grid-template-columns: 280px 1fr;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
border-right: 1px solid var(--border);
|
||||
background: linear-gradient(180deg, #0d645d 0%, #13454f 100%);
|
||||
color: #f8fffd;
|
||||
padding: 24px 18px;
|
||||
background: linear-gradient(165deg, rgba(30, 47, 80, 0.86) 0%, rgba(17, 27, 48, 0.95) 100%);
|
||||
border: 1px solid var(--border-0);
|
||||
box-shadow: var(--shadow-0);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-4);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
top: var(--space-4);
|
||||
height: calc(100vh - 40px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.sidebar h1 {
|
||||
font-size: 22px;
|
||||
margin: 0 0 20px;
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.brand-dot {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(135deg, var(--accent-0), #80dbff);
|
||||
box-shadow: 0 0 0 6px rgba(107, 166, 255, 0.22);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.brand h1 {
|
||||
margin: 0;
|
||||
font-size: 21px;
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
|
||||
.sidebar nav {
|
||||
.brand p {
|
||||
margin: 6px 0 0;
|
||||
color: var(--text-2);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.nav-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
color: #ecf8f5;
|
||||
color: var(--text-1);
|
||||
text-align: left;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
font-size: 15px;
|
||||
line-height: 1.45;
|
||||
padding: 12px 14px;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||
transition: border-color 0.2s ease, background-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-item:hover,
|
||||
.nav-item:focus-visible {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
outline: 2px solid rgba(255, 255, 255, 0.4);
|
||||
outline-offset: 2px;
|
||||
color: var(--text-0);
|
||||
border-color: var(--border-1);
|
||||
background: rgba(139, 177, 240, 0.14);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: #effaf7;
|
||||
border-color: #bbebe2;
|
||||
color: #114549;
|
||||
color: #031126;
|
||||
background: linear-gradient(135deg, #8ab6ff 0%, #9ed4ff 100%);
|
||||
border-color: #acd2ff;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 10px 24px rgba(99, 159, 243, 0.34);
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 24px;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
padding-right: var(--space-2);
|
||||
}
|
||||
|
||||
.main-header {
|
||||
margin-bottom: 18px;
|
||||
background: var(--surface-0);
|
||||
border: 1px solid var(--border-0);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-4);
|
||||
box-shadow: var(--shadow-1);
|
||||
backdrop-filter: blur(7px);
|
||||
}
|
||||
|
||||
.main-header h2 {
|
||||
margin: 0;
|
||||
font-size: 28px;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.main-header p {
|
||||
margin: 6px 0 0;
|
||||
color: var(--muted);
|
||||
margin: 8px 0 0;
|
||||
color: var(--text-1);
|
||||
}
|
||||
|
||||
.view {
|
||||
@@ -107,17 +223,16 @@ body {
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.grid.cols-3 {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
margin-bottom: 16px;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.grid.cols-4 {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.grid.cols-3 {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid.cols-2 {
|
||||
@@ -125,23 +240,46 @@ body {
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
box-shadow: var(--shadow);
|
||||
padding: 16px;
|
||||
background: var(--surface-1);
|
||||
border: 1px solid var(--border-0);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-1);
|
||||
padding: var(--space-4);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.card + .card {
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 12px;
|
||||
margin: 0 0 var(--space-3);
|
||||
font-size: 18px;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.stat {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stat::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: -32px;
|
||||
bottom: -38px;
|
||||
width: 112px;
|
||||
height: 112px;
|
||||
border-radius: 999px;
|
||||
background: radial-gradient(circle at center, rgba(118, 173, 255, 0.36) 0%, rgba(118, 173, 255, 0) 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.stat p {
|
||||
font-size: 40px;
|
||||
margin: 0;
|
||||
font-size: 38px;
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
color: var(--accent-1);
|
||||
}
|
||||
|
||||
.list {
|
||||
@@ -151,20 +289,31 @@ body {
|
||||
}
|
||||
|
||||
.list li {
|
||||
border-bottom: 1px solid #edf2f4;
|
||||
padding: 10px 0;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
border-bottom: 1px solid rgba(157, 185, 229, 0.2);
|
||||
padding: 12px 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.list a {
|
||||
color: #afd0ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.list a:hover,
|
||||
.list a:focus-visible {
|
||||
color: #cde4ff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
label {
|
||||
@@ -172,6 +321,7 @@ label {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.6;
|
||||
color: var(--text-1);
|
||||
}
|
||||
|
||||
input,
|
||||
@@ -180,42 +330,87 @@ button {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
margin-top: 6px;
|
||||
border: 1px solid #b6c5ca;
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
min-height: 44px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
width: 100%;
|
||||
margin-top: 6px;
|
||||
border: 1px solid #b6c5ca;
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
min-height: 44px;
|
||||
background: #fff;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-0);
|
||||
color: var(--text-0);
|
||||
background: rgba(13, 21, 39, 0.64);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
input:focus-visible {
|
||||
outline: 2px solid #76b8ad;
|
||||
outline-offset: 1px;
|
||||
border-color: #4fa494;
|
||||
input::placeholder {
|
||||
color: rgba(173, 192, 228, 0.66);
|
||||
}
|
||||
|
||||
select:focus-visible {
|
||||
outline: 2px solid #76b8ad;
|
||||
outline-offset: 1px;
|
||||
border-color: #4fa494;
|
||||
input:focus-visible,
|
||||
select:focus-visible,
|
||||
button:focus-visible {
|
||||
outline: 2px solid rgba(136, 189, 255, 0.92);
|
||||
outline-offset: 2px;
|
||||
border-color: rgba(136, 189, 255, 0.92);
|
||||
}
|
||||
|
||||
select {
|
||||
appearance: none;
|
||||
background-image:
|
||||
linear-gradient(45deg, transparent 50%, #aacfff 50%),
|
||||
linear-gradient(135deg, #aacfff 50%, transparent 50%);
|
||||
background-position:
|
||||
calc(100% - 16px) 17px,
|
||||
calc(100% - 11px) 17px;
|
||||
background-size: 5px 5px, 5px 5px;
|
||||
background-repeat: no-repeat;
|
||||
padding-right: 34px;
|
||||
}
|
||||
|
||||
.span-2 {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.alert.info {
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid rgba(130, 174, 255, 0.38);
|
||||
background: rgba(96, 150, 235, 0.15);
|
||||
color: #c5dcff;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.month-panel {
|
||||
border: 1px solid var(--border-0);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(21, 34, 61, 0.6);
|
||||
padding: var(--space-3);
|
||||
}
|
||||
|
||||
.month-grid {
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.month-action {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.month-action button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.project-item {
|
||||
border: 1px solid var(--border-0);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(20, 31, 55, 0.62);
|
||||
padding: var(--space-3);
|
||||
}
|
||||
|
||||
.project-item h4 {
|
||||
margin: 0 0 10px;
|
||||
color: var(--text-0);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
@@ -224,30 +419,35 @@ select:focus-visible {
|
||||
|
||||
button {
|
||||
min-height: 44px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #a9bbc1;
|
||||
background: #f4f8fa;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-0);
|
||||
background: rgba(88, 120, 170, 0.22);
|
||||
color: var(--text-0);
|
||||
padding: 0 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
button:hover,
|
||||
button:focus-visible {
|
||||
background: #e4edf1;
|
||||
outline: 2px solid #b7cad2;
|
||||
outline-offset: 2px;
|
||||
background: rgba(122, 160, 221, 0.35);
|
||||
border-color: rgba(157, 194, 253, 0.72);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
button.primary {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #6fafff 0%, #8fcbff 100%);
|
||||
border-color: #9cd0ff;
|
||||
color: #04162d;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
button.primary:hover,
|
||||
button.primary:focus-visible {
|
||||
background: #0c5f59;
|
||||
background: linear-gradient(135deg, #85bcff 0%, #a1d7ff 100%);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
@@ -255,30 +455,84 @@ button:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.file-picker {
|
||||
border: 1px solid #c7d6db;
|
||||
border-radius: 10px;
|
||||
background: #f9fbfc;
|
||||
padding: 8px;
|
||||
max-height: 220px;
|
||||
overflow: auto;
|
||||
#log-panel {
|
||||
display: none;
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
|
||||
.file-picker label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
.live-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.file-picker label:hover {
|
||||
background: #ecf4f7;
|
||||
.live-column header,
|
||||
.system-log-wrap > header {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--text-1);
|
||||
margin-bottom: 7px;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
overflow-x: auto;
|
||||
.live-output,
|
||||
.system-output {
|
||||
height: 250px;
|
||||
overflow-y: auto;
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 12px;
|
||||
font-family: "JetBrains Mono", "Consolas", monospace;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.62;
|
||||
border: 1px solid var(--border-0);
|
||||
background: rgba(6, 12, 25, 0.72);
|
||||
}
|
||||
|
||||
.live-output {
|
||||
color: #d6e5ff;
|
||||
}
|
||||
|
||||
.live-column.reasoning .live-output {
|
||||
background: rgba(9, 18, 39, 0.82);
|
||||
}
|
||||
|
||||
.live-column.answer .live-output {
|
||||
background: rgba(12, 24, 37, 0.78);
|
||||
}
|
||||
|
||||
.system-output {
|
||||
color: #e8f1ff;
|
||||
background: rgba(6, 12, 24, 0.9);
|
||||
border-color: rgba(150, 180, 230, 0.34);
|
||||
}
|
||||
|
||||
.live-output .muted,
|
||||
.system-output .muted {
|
||||
color: #a9bde3;
|
||||
}
|
||||
|
||||
.log-line {
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.system-output .log-line.is-info {
|
||||
color: #dbe8ff;
|
||||
}
|
||||
|
||||
.system-output .log-line.is-error {
|
||||
color: #ffb6bc;
|
||||
}
|
||||
|
||||
.live-output .log-line.is-reasoning {
|
||||
color: #ccdeff;
|
||||
}
|
||||
|
||||
.live-output .log-line.is-answer {
|
||||
color: #baf2da;
|
||||
}
|
||||
|
||||
.system-log-wrap {
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
|
||||
.history-toolbar {
|
||||
@@ -288,12 +542,41 @@ button:disabled {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
overflow-x: auto;
|
||||
border: 1px solid rgba(157, 185, 229, 0.2);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
min-width: 720px;
|
||||
background: rgba(8, 15, 30, 0.35);
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 10px 8px;
|
||||
border-bottom: 1px solid rgba(157, 185, 229, 0.16);
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
th {
|
||||
color: #d5e4ff;
|
||||
background: rgba(129, 167, 229, 0.12);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.pager {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
color: var(--muted);
|
||||
color: var(--text-2);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@@ -302,19 +585,10 @@ button:disabled {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
min-width: 720px;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 10px 8px;
|
||||
border-bottom: 1px solid #e8eef0;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
.btn-cancel-task {
|
||||
min-height: 32px;
|
||||
padding: 0 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
@@ -323,112 +597,105 @@ td {
|
||||
padding: 2px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.tag.SUCCESS {
|
||||
background: #d1fadf;
|
||||
color: var(--success);
|
||||
background: rgba(73, 194, 138, 0.16);
|
||||
border-color: rgba(73, 194, 138, 0.5);
|
||||
color: #95edc2;
|
||||
}
|
||||
|
||||
.tag.RUNNING,
|
||||
.tag.PENDING {
|
||||
background: #fef0c7;
|
||||
color: var(--warning);
|
||||
background: rgba(240, 184, 93, 0.16);
|
||||
border-color: rgba(240, 184, 93, 0.45);
|
||||
color: #ffd99d;
|
||||
}
|
||||
|
||||
.tag.FAILED {
|
||||
background: #fee4e2;
|
||||
color: var(--danger);
|
||||
background: rgba(255, 127, 135, 0.16);
|
||||
border-color: rgba(255, 127, 135, 0.5);
|
||||
color: #ffb5bc;
|
||||
}
|
||||
|
||||
.tag.CANCELLED {
|
||||
background: #e4e7ec;
|
||||
color: #344054;
|
||||
background: rgba(148, 166, 196, 0.16);
|
||||
border-color: rgba(148, 166, 196, 0.45);
|
||||
color: #ced9f1;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--muted);
|
||||
color: var(--text-2);
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
border-radius: 10px;
|
||||
right: 24px;
|
||||
bottom: 24px;
|
||||
z-index: var(--z-toast);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 12px 14px;
|
||||
background: #11343b;
|
||||
color: #fff;
|
||||
background: rgba(8, 16, 33, 0.96);
|
||||
border: 1px solid rgba(132, 171, 235, 0.46);
|
||||
color: #eef4ff;
|
||||
min-width: 240px;
|
||||
max-width: 380px;
|
||||
max-width: 400px;
|
||||
display: none;
|
||||
box-shadow: var(--shadow);
|
||||
box-shadow: var(--shadow-0);
|
||||
}
|
||||
|
||||
.toast.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.live-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
@media (max-width: 1180px) {
|
||||
.grid.cols-4 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.live-column header,
|
||||
.system-log-wrap > header {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--muted);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.live-output,
|
||||
.system-output {
|
||||
height: 240px;
|
||||
overflow-y: auto;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
border: 1px solid var(--border);
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.live-column.reasoning .live-output {
|
||||
background: #f6fbff;
|
||||
}
|
||||
|
||||
.live-column.answer .live-output {
|
||||
background: #f5fdf8;
|
||||
}
|
||||
|
||||
.system-log-wrap {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
@media (max-width: 980px) {
|
||||
.app-shell {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: static;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: auto;
|
||||
padding: 14px;
|
||||
gap: 12px;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.sidebar nav {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
.brand p {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-list {
|
||||
flex-direction: row;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 2px;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
min-height: 44px;
|
||||
padding: 10px 13px;
|
||||
}
|
||||
|
||||
.main {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.grid.cols-3,
|
||||
.grid.cols-4,
|
||||
.grid.cols-2,
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.grid.cols-3,
|
||||
.form-grid,
|
||||
.history-toolbar {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -436,16 +703,42 @@ td {
|
||||
.span-2 {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.month-action {
|
||||
grid-column: span 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
@media (max-width: 760px) {
|
||||
.grid.cols-4 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.main-header h2 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: var(--space-3);
|
||||
}
|
||||
|
||||
.live-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.toast {
|
||||
right: 12px;
|
||||
left: 12px;
|
||||
bottom: 12px;
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
scroll-behavior: auto !important;
|
||||
|
||||
Reference in New Issue
Block a user