feat(web): unify web entry, preset config, SSE streaming and dual-pane live logs
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
# 服务器配置
|
||||
server.port=18088
|
||||
server.servlet.context-path=/
|
||||
|
||||
# 文件上传配置
|
||||
spring.servlet.multipart.max-file-size=100MB
|
||||
spring.servlet.multipart.max-request-size=100MB
|
||||
|
||||
# 日志配置
|
||||
logging.level.com.svnlog=INFO
|
||||
logging.level.org.springframework=INFO
|
||||
|
||||
# SVN 预设配置
|
||||
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_2024
|
||||
+419
-133
@@ -14,8 +14,7 @@ const CUSTOM_PRESET_ID = "custom";
|
||||
|
||||
const viewMeta = {
|
||||
dashboard: { title: "工作台", desc: "查看系统状态与最近产物" },
|
||||
svn: { title: "SVN 日志抓取", desc: "配置 SVN 参数并生成 Markdown" },
|
||||
ai: { title: "AI 工作量分析", desc: "选择 Markdown 后生成工作量 Excel" },
|
||||
svn: { title: "SVN 日志抓取", desc: "一键抓取SVN日志并导出工作量Excel" },
|
||||
history: { title: "任务历史", desc: "查看任务执行状态、日志与产物" },
|
||||
settings: { title: "系统设置", desc: "配置 API Key 与输出目录" },
|
||||
};
|
||||
@@ -26,6 +25,20 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
await loadPresets();
|
||||
await refreshAll();
|
||||
await loadSettings();
|
||||
|
||||
// 自动填充当月默认值
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
// 月份选择器:YYYY-MM
|
||||
document.querySelector("#version-month").value = `${year}-${month}`;
|
||||
// 工作周期:YYYY年MM月
|
||||
document.querySelector("#svn-form [name='period']").value = `${year}年${month}月`;
|
||||
// 输出文件名:YYYYMM工作量统计.xlsx
|
||||
document.querySelector("#svn-form [name='outputFileName']").value = `${year}${month}工作量统计.xlsx`;
|
||||
|
||||
// 绑定自动填充版本按钮
|
||||
document.querySelector("#btn-auto-fill").addEventListener("click", onAutoFillVersions);
|
||||
|
||||
state.polling = setInterval(refreshAll, 5000);
|
||||
});
|
||||
@@ -46,15 +59,11 @@ function bindForms() {
|
||||
const svnForm = document.querySelector("#svn-form");
|
||||
svnForm.addEventListener("submit", onRunSvn);
|
||||
|
||||
const aiForm = document.querySelector("#ai-form");
|
||||
aiForm.addEventListener("submit", onRunAi);
|
||||
|
||||
|
||||
const settingsForm = document.querySelector("#settings-form");
|
||||
settingsForm.addEventListener("submit", onSaveSettings);
|
||||
|
||||
const svnPresetSelect = document.querySelector("#svn-preset-select");
|
||||
svnPresetSelect.addEventListener("change", onSvnPresetChange);
|
||||
|
||||
const taskFilterBtn = document.querySelector("#btn-task-filter");
|
||||
if (taskFilterBtn) {
|
||||
taskFilterBtn.addEventListener("click", onTaskFilterSubmit);
|
||||
@@ -76,9 +85,7 @@ function switchView(view) {
|
||||
loadTaskPage();
|
||||
renderFileTable();
|
||||
}
|
||||
if (view === "ai") {
|
||||
renderMdFilePicker();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function apiFetch(url, options = {}) {
|
||||
@@ -109,9 +116,7 @@ async function refreshAll() {
|
||||
loadTaskPage();
|
||||
renderFileTable();
|
||||
}
|
||||
if (state.activeView === "ai") {
|
||||
renderMdFilePicker();
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
toast(err.message, true);
|
||||
}
|
||||
@@ -130,60 +135,31 @@ async function loadPresets() {
|
||||
}
|
||||
|
||||
function renderPresetSelects() {
|
||||
const svnSelect = document.querySelector("#svn-preset-select");
|
||||
const settingsSelect = document.querySelector("#settings-default-preset");
|
||||
svnSelect.innerHTML = "";
|
||||
if (!settingsSelect) return;
|
||||
|
||||
settingsSelect.innerHTML = "";
|
||||
|
||||
state.presets.forEach((preset) => {
|
||||
const option1 = document.createElement("option");
|
||||
option1.value = preset.id;
|
||||
option1.textContent = `${preset.name}`;
|
||||
svnSelect.appendChild(option1);
|
||||
|
||||
const option2 = document.createElement("option");
|
||||
option2.value = preset.id;
|
||||
option2.textContent = `${preset.name}`;
|
||||
settingsSelect.appendChild(option2);
|
||||
const option = document.createElement("option");
|
||||
option.value = preset.id;
|
||||
option.textContent = `${preset.name}`;
|
||||
settingsSelect.appendChild(option);
|
||||
});
|
||||
|
||||
const customOption = document.createElement("option");
|
||||
customOption.value = CUSTOM_PRESET_ID;
|
||||
customOption.textContent = "自定义 SVN 地址";
|
||||
svnSelect.appendChild(customOption);
|
||||
|
||||
const selected = state.defaultPresetId || (state.presets[0] ? state.presets[0].id : CUSTOM_PRESET_ID);
|
||||
svnSelect.value = selected;
|
||||
settingsSelect.value = selected;
|
||||
const selected = state.defaultPresetId || (state.presets[0] ? state.presets[0].id : "");
|
||||
if (selected) {
|
||||
settingsSelect.value = selected;
|
||||
}
|
||||
}
|
||||
|
||||
// 已移除单个项目预设功能,使用固定三个项目配置
|
||||
function onSvnPresetChange(event) {
|
||||
applyPresetToSvnForm(event.target.value);
|
||||
// 保留空实现兼容
|
||||
}
|
||||
|
||||
function applyPresetToSvnForm(presetId) {
|
||||
const form = document.querySelector("#svn-form");
|
||||
const select = document.querySelector("#svn-preset-select");
|
||||
const projectInput = form.querySelector("[name='projectName']");
|
||||
const urlInput = form.querySelector("[name='url']");
|
||||
|
||||
if (presetId === CUSTOM_PRESET_ID) {
|
||||
select.value = CUSTOM_PRESET_ID;
|
||||
projectInput.readOnly = false;
|
||||
urlInput.readOnly = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const preset = state.presets.find((item) => item.id === presetId);
|
||||
if (!preset) {
|
||||
return;
|
||||
}
|
||||
|
||||
select.value = preset.id;
|
||||
projectInput.value = preset.name;
|
||||
urlInput.value = preset.url;
|
||||
projectInput.readOnly = true;
|
||||
urlInput.readOnly = true;
|
||||
// 保留空实现兼容
|
||||
}
|
||||
|
||||
function renderDashboard() {
|
||||
@@ -229,22 +205,20 @@ function renderDashboard() {
|
||||
}
|
||||
|
||||
async function onTestConnection() {
|
||||
const form = document.querySelector("#svn-form");
|
||||
const payload = readForm(form);
|
||||
if (!payload.url || !payload.username || !payload.password) {
|
||||
toast("请先填写 SVN 地址、账号和密码", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.querySelector("#btn-test-connection");
|
||||
setLoading(btn, true);
|
||||
try {
|
||||
const firstPreset = state.presets && state.presets.length > 0 ? state.presets[0] : null;
|
||||
if (!firstPreset || !firstPreset.id) {
|
||||
throw new Error("未加载到 SVN 预设,请刷新页面后重试");
|
||||
}
|
||||
|
||||
await apiFetch("/api/svn/test-connection", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
url: payload.url,
|
||||
username: payload.username,
|
||||
password: payload.password,
|
||||
presetId: firstPreset.id,
|
||||
username: "liujing2",
|
||||
password: "sunri@20230620*#&",
|
||||
}),
|
||||
});
|
||||
toast("SVN 连接成功");
|
||||
@@ -255,93 +229,344 @@ async function onTestConnection() {
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForTaskCompletion(taskId) {
|
||||
while (true) {
|
||||
try {
|
||||
const task = await apiFetch(`/api/tasks/${encodeURIComponent(taskId)}`);
|
||||
if (task.status === "SUCCESS") {
|
||||
return task;
|
||||
}
|
||||
if (task.status === "FAILED" || task.status === "CANCELLED") {
|
||||
throw new Error(`任务 ${taskId} 执行失败: ${task.error || task.message}`);
|
||||
}
|
||||
// 等待2秒再查询
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function onRunSvn(event) {
|
||||
event.preventDefault();
|
||||
const form = event.target;
|
||||
const payload = readForm(form);
|
||||
const btn = document.querySelector("#btn-svn-run");
|
||||
const logPanel = document.querySelector("#log-panel");
|
||||
let aiStream = null;
|
||||
|
||||
// 显示日志面板,清空日志
|
||||
logPanel.style.display = "block";
|
||||
clearLog();
|
||||
appendLog("任务开始...");
|
||||
setLoading(btn, true);
|
||||
form.disabled = true;
|
||||
|
||||
try {
|
||||
const data = await apiFetch("/api/svn/fetch", {
|
||||
if (!state.presets || state.presets.length < 3) {
|
||||
throw new Error("SVN 预设加载异常,请刷新页面后重试");
|
||||
}
|
||||
|
||||
const revisionRanges = [
|
||||
{ start: payload.startRevision_1, end: payload.endRevision_1 },
|
||||
{ start: payload.startRevision_2, end: payload.endRevision_2 },
|
||||
{ start: payload.startRevision_3, end: payload.endRevision_3 },
|
||||
];
|
||||
|
||||
const projects = revisionRanges
|
||||
.map((range, idx) => ({
|
||||
presetId: state.presets[idx].id,
|
||||
name: state.presets[idx].name,
|
||||
start: range.start,
|
||||
end: range.end,
|
||||
}))
|
||||
.filter((project) => project.start && project.end);
|
||||
|
||||
if (projects.length === 0) {
|
||||
appendLog("错误:请至少填写一个项目的开始和结束版本号", true);
|
||||
toast("请至少填写一个项目的开始和结束版本号", true);
|
||||
return;
|
||||
}
|
||||
|
||||
appendLog(`检测到 ${projects.length} 个待处理项目`);
|
||||
|
||||
const mdFiles = [];
|
||||
for (let i = 0; i < projects.length; i++) {
|
||||
const project = projects[i];
|
||||
appendLog(`正在提交 ${project.name} 的抓取任务...`);
|
||||
const data = await apiFetch("/api/svn/fetch", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
presetId: project.presetId,
|
||||
username: "liujing2",
|
||||
password: "sunri@20230620*#&",
|
||||
startRevision: toNumberOrNull(project.start),
|
||||
endRevision: toNumberOrNull(project.end),
|
||||
filterUser: payload.filterUser || "",
|
||||
}),
|
||||
});
|
||||
const taskId = data.taskId;
|
||||
appendLog(`已创建抓取任务:${project.name} (任务ID: ${taskId.slice(0,8)})`);
|
||||
appendLog(`正在抓取 ${project.name} 日志...`);
|
||||
|
||||
// 严格串行:当前项目完成后才开始下一个项目
|
||||
while (true) {
|
||||
const task = await apiFetch(`/api/tasks/${encodeURIComponent(taskId)}`);
|
||||
if (task.status === "SUCCESS") {
|
||||
appendLog(`${project.name} 抓取完成`);
|
||||
if (task.message) appendLog(task.message);
|
||||
if (task.files && task.files.length > 0) {
|
||||
mdFiles.push(...task.files.filter(f => f.endsWith(".md")));
|
||||
appendLog(`生成文件: ${task.files.join(", ")}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (task.status === "FAILED" || task.status === "CANCELLED") {
|
||||
throw new Error(
|
||||
`${project.name} 抓取失败 (任务ID: ${taskId.slice(0,8)}): ${task.error || task.message}`
|
||||
);
|
||||
}
|
||||
// 显示任务进度
|
||||
if (task.message) appendLog(`[${project.name}] ${task.message} (进度: ${task.progress}%)`);
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
}
|
||||
}
|
||||
|
||||
appendLog(`所有SVN抓取任务完成,共生成 ${mdFiles.length} 个Markdown文件`);
|
||||
|
||||
// 调用AI分析接口
|
||||
appendLog("正在提交AI分析任务...");
|
||||
const aiData = await apiFetch("/api/ai/analyze", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
projectName: payload.projectName || "",
|
||||
url: payload.url,
|
||||
username: payload.username,
|
||||
password: payload.password,
|
||||
startRevision: toNumberOrNull(payload.startRevision),
|
||||
endRevision: toNumberOrNull(payload.endRevision),
|
||||
filterUser: payload.filterUser || "",
|
||||
}),
|
||||
});
|
||||
toast(`SVN 抓取任务已创建:${data.taskId}`);
|
||||
switchView("history");
|
||||
refreshAll();
|
||||
} catch (err) {
|
||||
toast(err.message, true);
|
||||
} finally {
|
||||
setLoading(btn, false);
|
||||
}
|
||||
}
|
||||
|
||||
function renderMdFilePicker() {
|
||||
const box = document.querySelector("#md-file-picker");
|
||||
const mdFiles = state.files.filter((f) => f.path.toLowerCase().endsWith(".md"));
|
||||
box.innerHTML = "";
|
||||
|
||||
if (mdFiles.length === 0) {
|
||||
box.innerHTML = "<p class='muted'>暂无 Markdown 文件,请先执行 SVN 抓取。</p>";
|
||||
return;
|
||||
}
|
||||
|
||||
mdFiles.forEach((file, idx) => {
|
||||
const path = file.path;
|
||||
const id = `md-file-${idx}`;
|
||||
const label = document.createElement("label");
|
||||
const input = document.createElement("input");
|
||||
input.type = "checkbox";
|
||||
input.id = id;
|
||||
input.value = path;
|
||||
label.setAttribute("for", id);
|
||||
const span = document.createElement("span");
|
||||
span.textContent = `${path} (${formatBytes(file.size)})`;
|
||||
label.appendChild(input);
|
||||
label.appendChild(span);
|
||||
box.appendChild(label);
|
||||
});
|
||||
}
|
||||
|
||||
async function onRunAi(event) {
|
||||
event.preventDefault();
|
||||
const form = event.target;
|
||||
const payload = readForm(form);
|
||||
const checked = [...document.querySelectorAll("#md-file-picker input[type='checkbox']:checked")]
|
||||
.map((input) => input.value);
|
||||
if (!checked.length) {
|
||||
toast("请至少选择一个 Markdown 文件", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.querySelector("#btn-ai-run");
|
||||
setLoading(btn, true);
|
||||
try {
|
||||
const data = await apiFetch("/api/ai/analyze", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
filePaths: checked,
|
||||
filePaths: mdFiles,
|
||||
period: payload.period || "",
|
||||
apiKey: payload.apiKey || "",
|
||||
apiKey: "", // 使用内置API Key
|
||||
outputFileName: payload.outputFileName || "",
|
||||
}),
|
||||
});
|
||||
toast(`AI 分析任务已创建:${data.taskId}`);
|
||||
switchView("history");
|
||||
|
||||
appendSystemLog(`AI分析任务已创建 (任务ID: ${aiData.taskId.slice(0,8)})`);
|
||||
appendSystemLog("正在进行AI分析,请耐心等待...");
|
||||
|
||||
const streamState = {
|
||||
reasoningBuffer: "",
|
||||
answerBuffer: "",
|
||||
streamAvailable: true,
|
||||
};
|
||||
aiStream = openTaskEventStream(aiData.taskId, {
|
||||
onPhase: (payload) => {
|
||||
if (payload && payload.message) {
|
||||
appendSystemLog(payload.message);
|
||||
}
|
||||
},
|
||||
onReasoning: (text) => {
|
||||
if (!text) return;
|
||||
streamState.reasoningBuffer += text;
|
||||
flushStreamBuffer(streamState, "reasoningBuffer", "reasoning", false);
|
||||
},
|
||||
onAnswer: (text) => {
|
||||
if (!text) return;
|
||||
streamState.answerBuffer += text;
|
||||
flushStreamBuffer(streamState, "answerBuffer", "answer", false);
|
||||
},
|
||||
onUsage: (payload) => {
|
||||
if (!payload) return;
|
||||
appendSystemLog(`Token统计: prompt=${payload.promptTokens || 0}, completion=${payload.completionTokens || 0}, total=${payload.totalTokens || 0}`);
|
||||
},
|
||||
onError: (payload) => {
|
||||
if (payload && payload.detail) {
|
||||
appendSystemLog(`流式错误: ${payload.detail}`, true);
|
||||
}
|
||||
},
|
||||
onTransportError: () => {
|
||||
if (streamState.streamAvailable) {
|
||||
streamState.streamAvailable = false;
|
||||
appendSystemLog("实时流中断,已回退到轮询模式");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// 等待AI任务完成,实时显示日志
|
||||
while (true) {
|
||||
const task = await apiFetch(`/api/tasks/${encodeURIComponent(aiData.taskId)}`);
|
||||
if (task.status === "SUCCESS") {
|
||||
flushStreamBuffer(streamState, "reasoningBuffer", "reasoning", true);
|
||||
flushStreamBuffer(streamState, "answerBuffer", "answer", true);
|
||||
appendSystemLog("AI分析完成");
|
||||
if (task.message) appendSystemLog(task.message);
|
||||
aiStream.close();
|
||||
break;
|
||||
}
|
||||
if (task.status === "FAILED" || task.status === "CANCELLED") {
|
||||
flushStreamBuffer(streamState, "reasoningBuffer", "reasoning", true);
|
||||
flushStreamBuffer(streamState, "answerBuffer", "answer", true);
|
||||
aiStream.close();
|
||||
throw new Error(`AI分析失败: ${task.error || task.message}`);
|
||||
}
|
||||
// 显示AI思考过程
|
||||
if (!streamState.streamAvailable && task.message) {
|
||||
// 避免重复输出相同消息
|
||||
const lastLog = document.querySelector("#system-log-output p:last-child");
|
||||
if (!lastLog || !lastLog.textContent.includes(task.message)) {
|
||||
appendSystemLog(task.message);
|
||||
}
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
// 获取最终任务结果
|
||||
const aiTask = await apiFetch(`/api/tasks/${encodeURIComponent(aiData.taskId)}`);
|
||||
|
||||
// 找到生成的Excel文件并自动下载
|
||||
if (aiTask.files && aiTask.files.length > 0) {
|
||||
const excelFile = aiTask.files.find(f => f.endsWith(".xlsx"));
|
||||
if (excelFile) {
|
||||
appendSystemLog("Excel生成成功,开始下载...");
|
||||
// 触发下载
|
||||
window.open(`/api/files/download?path=${encodeURIComponent(excelFile)}`, "_blank");
|
||||
appendSystemLog("✅ 任务全部完成!");
|
||||
}
|
||||
}
|
||||
|
||||
refreshAll();
|
||||
} catch (err) {
|
||||
appendLog(`错误: ${err.message}`, true);
|
||||
toast(err.message, true);
|
||||
} finally {
|
||||
if (aiStream) {
|
||||
aiStream.close();
|
||||
}
|
||||
setLoading(btn, false);
|
||||
form.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openTaskEventStream(taskId, handlers = {}) {
|
||||
if (!window.EventSource) {
|
||||
handlers.onTransportError && handlers.onTransportError();
|
||||
return { close: () => {} };
|
||||
}
|
||||
|
||||
const streamUrl = `/api/tasks/${encodeURIComponent(taskId)}/stream`;
|
||||
const source = new EventSource(streamUrl);
|
||||
const parse = (event) => {
|
||||
try {
|
||||
return JSON.parse(event.data || "{}");
|
||||
} catch (err) {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
source.addEventListener("phase", (event) => {
|
||||
handlers.onPhase && handlers.onPhase(parse(event));
|
||||
});
|
||||
source.addEventListener("reasoning_delta", (event) => {
|
||||
const payload = parse(event);
|
||||
handlers.onReasoning && handlers.onReasoning(payload.text || "");
|
||||
});
|
||||
source.addEventListener("answer_delta", (event) => {
|
||||
const payload = parse(event);
|
||||
handlers.onAnswer && handlers.onAnswer(payload.text || "");
|
||||
});
|
||||
source.addEventListener("usage", (event) => {
|
||||
handlers.onUsage && handlers.onUsage(parse(event));
|
||||
});
|
||||
source.addEventListener("error", (event) => {
|
||||
handlers.onError && handlers.onError(parse(event));
|
||||
});
|
||||
source.onerror = () => {
|
||||
handlers.onTransportError && handlers.onTransportError();
|
||||
source.close();
|
||||
};
|
||||
|
||||
return {
|
||||
close: () => {
|
||||
source.close();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function flushStreamBuffer(streamState, key, target, force) {
|
||||
const text = streamState[key] || "";
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldFlush = force || text.length >= 64 || /[。!?\n]$/.test(text);
|
||||
if (!shouldFlush) {
|
||||
return;
|
||||
}
|
||||
const cleaned = text.replace(/\s+/g, " ").trim();
|
||||
if (target === "reasoning") {
|
||||
appendReasoning(cleaned);
|
||||
} else if (target === "answer") {
|
||||
appendAnswer(cleaned);
|
||||
} else {
|
||||
appendSystemLog(cleaned);
|
||||
}
|
||||
streamState[key] = "";
|
||||
}
|
||||
|
||||
function appendSystemLog(message, isError = false) {
|
||||
const logOutput = document.querySelector("#system-log-output");
|
||||
markPanelReady("#system-log-output");
|
||||
const p = document.createElement("p");
|
||||
const time = new Date().toLocaleTimeString("zh-CN", { hour12: false });
|
||||
p.style.margin = "2px 0";
|
||||
if (isError) {
|
||||
p.style.color = "#dc2626";
|
||||
p.textContent = `[${time}] ❌ ${message}`;
|
||||
} else {
|
||||
p.style.color = "#1e293b";
|
||||
p.textContent = `[${time}] ℹ️ ${message}`;
|
||||
}
|
||||
logOutput.appendChild(p);
|
||||
logOutput.scrollTop = logOutput.scrollHeight;
|
||||
}
|
||||
|
||||
function appendReasoning(message) {
|
||||
appendPane("#reasoning-output", message, "#334155");
|
||||
}
|
||||
|
||||
function appendAnswer(message) {
|
||||
appendPane("#answer-output", message, "#166534");
|
||||
}
|
||||
|
||||
function appendPane(selector, message, color) {
|
||||
const logOutput = document.querySelector(selector);
|
||||
markPanelReady(selector);
|
||||
const p = document.createElement("p");
|
||||
p.style.margin = "2px 0";
|
||||
p.style.color = color;
|
||||
p.textContent = message;
|
||||
logOutput.appendChild(p);
|
||||
logOutput.scrollTop = logOutput.scrollHeight;
|
||||
}
|
||||
|
||||
// 兼容旧调用
|
||||
function appendLog(message, isError = false) {
|
||||
appendSystemLog(message, isError);
|
||||
}
|
||||
|
||||
function clearLog() {
|
||||
const system = document.querySelector("#system-log-output");
|
||||
const reasoning = document.querySelector("#reasoning-output");
|
||||
const answer = document.querySelector("#answer-output");
|
||||
|
||||
system.innerHTML = "<p class='muted'>等待任务开始...</p>";
|
||||
reasoning.innerHTML = "<p class='muted'>等待思考输出...</p>";
|
||||
answer.innerHTML = "<p class='muted'>等待答案输出...</p>";
|
||||
}
|
||||
|
||||
function markPanelReady(selector) {
|
||||
const panel = document.querySelector(selector);
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
const muted = panel.querySelector(".muted");
|
||||
if (muted) {
|
||||
muted.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -600,3 +825,64 @@ function sortByTimeDesc(left, right) {
|
||||
const r = right ? new Date(right).getTime() : 0;
|
||||
return r - l;
|
||||
}
|
||||
|
||||
// 自动填充版本号
|
||||
async function onAutoFillVersions() {
|
||||
const btn = document.querySelector("#btn-auto-fill");
|
||||
const monthInput = document.querySelector("#version-month");
|
||||
const [year, month] = monthInput.value.split("-");
|
||||
|
||||
setLoading(btn, true);
|
||||
appendLog(`开始查询 ${year}年${month}月 的版本范围...`);
|
||||
|
||||
if (!state.presets || state.presets.length < 3) {
|
||||
appendLog("错误:未加载到完整 SVN 预设,请刷新页面后重试", true);
|
||||
setLoading(btn, false);
|
||||
return;
|
||||
}
|
||||
|
||||
const projects = [1, 2, 3].map((index) => {
|
||||
const preset = state.presets[index - 1];
|
||||
return {
|
||||
presetId: preset.id,
|
||||
name: preset.name,
|
||||
startInput: document.querySelector(`#svn-form [name='startRevision_${index}']`),
|
||||
endInput: document.querySelector(`#svn-form [name='endRevision_${index}']`),
|
||||
};
|
||||
});
|
||||
|
||||
try {
|
||||
for (const project of projects) {
|
||||
appendLog(`正在查询 ${project.name} 的版本范围...`);
|
||||
|
||||
// 调用后端接口获取月份版本范围
|
||||
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" // 只查询该用户的提交(完整用户名)
|
||||
}),
|
||||
});
|
||||
|
||||
if (data.startRevision && data.endRevision) {
|
||||
project.startInput.value = data.startRevision;
|
||||
project.endInput.value = data.endRevision;
|
||||
appendLog(`${project.name} 版本范围: ${data.startRevision} - ${data.endRevision}`);
|
||||
} else {
|
||||
appendLog(`⚠️ ${project.name} 该月份无提交记录`, true);
|
||||
}
|
||||
}
|
||||
|
||||
appendLog("✅ 所有项目版本号填充完成");
|
||||
toast("版本号填充完成");
|
||||
} catch (err) {
|
||||
appendLog(`填充失败: ${err.message}`, true);
|
||||
toast(err.message, true);
|
||||
} finally {
|
||||
setLoading(btn, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,8 @@
|
||||
<aside class="sidebar" aria-label="主导航">
|
||||
<h1>SVN 工作台</h1>
|
||||
<nav>
|
||||
<button class="nav-item active" data-view="dashboard">工作台</button>
|
||||
<button class="nav-item" data-view="svn">SVN 日志抓取</button>
|
||||
<button class="nav-item" data-view="ai">AI 工作量分析</button>
|
||||
<button class="nav-item" data-view="dashboard">工作台</button>
|
||||
<button class="nav-item active" data-view="svn">SVN 日志抓取</button>
|
||||
<button class="nav-item" data-view="history">任务历史</button>
|
||||
<button class="nav-item" data-view="settings">系统设置</button>
|
||||
</nav>
|
||||
@@ -64,41 +63,87 @@
|
||||
|
||||
<section class="view" id="view-svn">
|
||||
<article class="card form-card">
|
||||
<h3>SVN 抓取参数</h3>
|
||||
<h3>SVN 批量抓取参数</h3>
|
||||
<div class="alert info span-2" style="margin-bottom:16px;padding:12px;border-radius:10px;background:#d1f0eb;color:#0f766e">
|
||||
默认已填充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;">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<form id="svn-form" class="form-grid">
|
||||
<label>预置项目
|
||||
<select name="presetId" id="svn-preset-select" aria-label="预置 SVN 项目"></select>
|
||||
</label>
|
||||
<label>项目名<input name="projectName" placeholder="如:PRS-7050"></label>
|
||||
<label>SVN 地址<input required name="url" placeholder="https://..." aria-label="SVN 地址"></label>
|
||||
<label>账号<input required name="username" placeholder="请输入账号"></label>
|
||||
<label>密码<input required type="password" name="password" placeholder="请输入密码"></label>
|
||||
<label>开始版本号<input name="startRevision" inputmode="numeric" placeholder="默认最新"></label>
|
||||
<label>结束版本号<input name="endRevision" inputmode="numeric" placeholder="默认最新"></label>
|
||||
<label class="span-2">过滤用户名<input name="filterUser" placeholder="包含匹配,留空不过滤"></label>
|
||||
<!-- 项目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">
|
||||
<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">
|
||||
<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">
|
||||
<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">开始抓取并导出</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;">
|
||||
<h3>执行进度</h3>
|
||||
<div class="live-grid">
|
||||
<section class="live-column reasoning">
|
||||
<header>思考过程</header>
|
||||
<div id="reasoning-output" class="live-output">
|
||||
<p class="muted">等待思考输出...</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="live-column answer">
|
||||
<header>最终输出</header>
|
||||
<div id="answer-output" class="live-output">
|
||||
<p class="muted">等待答案输出...</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="system-log-wrap">
|
||||
<header>系统日志</header>
|
||||
<div id="system-log-output" class="system-output">
|
||||
<p class="muted">等待任务开始...</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="view" id="view-ai">
|
||||
<article class="card form-card">
|
||||
<h3>AI 分析参数</h3>
|
||||
<form id="ai-form" class="form-grid">
|
||||
<label class="span-2">选择 Markdown 输入文件</label>
|
||||
<div class="span-2 file-picker" id="md-file-picker" role="group" aria-label="Markdown 文件选择"></div>
|
||||
<label>工作周期<input name="period" placeholder="例如 2026年03月"></label>
|
||||
<label>输出文件名<input name="outputFileName" placeholder="例如 202603工作量统计.xlsx"></label>
|
||||
<label class="span-2">临时 API Key(可选)<input type="password" name="apiKey" placeholder="优先使用设置页或环境变量"></label>
|
||||
<div class="actions span-2">
|
||||
<button type="submit" id="btn-ai-run" class="primary">开始 AI 分析并导出 Excel</button>
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="view" id="view-history">
|
||||
<article class="card">
|
||||
|
||||
@@ -368,6 +368,45 @@ td {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.live-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.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) {
|
||||
.app-shell {
|
||||
grid-template-columns: 1fr;
|
||||
@@ -399,6 +438,12 @@ td {
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.live-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation: none !important;
|
||||
|
||||
Reference in New Issue
Block a user