Files

330 lines
13 KiB
JavaScript

import { t, getLanguage, setLanguage } from "../i18n.js";
import { API } from "../api.js";
import { captureCanvasThumbnail, debounce, getActiveTabName, setActiveTabName, generateDefaultWorkflowName } from "../utils.js";
import { GroupTree } from "./GroupTree.js";
import { WorkflowList } from "./WorkflowList.js";
import { VersionHistory } from "./VersionHistory.js";
import { showDialog, showSaveAsDialog } from "./Dialog.js";
export class WorkflowPanel {
constructor(container, { onSaved } = {}) {
this.container = container;
this.onSaved = onSaved;
this.workflows = {};
this.groups = {};
this.currentGroup = null;
this.groupTree = null;
this.wfList = null;
this.versionPanel = null;
this.statusEl = null;
this.countEl = null;
this.searchInput = null;
this.logToggleBtn = null;
this.backendLoggingEnabled = true;
this._build();
this.loadData();
}
_build() {
this.container.innerHTML = "";
this.container.style.cssText = `
display:flex;flex-direction:column;height:100%;
background:#1a1a1a;color:#fff;font-family:sans-serif;
font-size:13px;overflow:hidden;
`;
const topbar = document.createElement("div");
topbar.style.cssText = `
padding:8px 10px;border-bottom:1px solid #2a2a2a;
display:flex;align-items:center;gap:7px;
flex-shrink:0;background:#141414;flex-wrap:wrap;
`;
const title = document.createElement("div");
title.textContent = t("workflowManager");
title.style.cssText = "font-size:13px;font-weight:600;color:#fff;flex-shrink:0;";
this.searchInput = document.createElement("input");
this.searchInput.type = "text";
this.searchInput.placeholder = t("searchPlaceholder");
this.searchInput.style.cssText = `
flex:1;min-width:80px;padding:5px 9px;background:#242424;
border:1px solid #333;color:#fff;border-radius:5px;
font-size:12px;outline:none;transition:border-color .15s;
`;
this.searchInput.addEventListener("focus", () => this.searchInput.style.borderColor = "#0066cc");
this.searchInput.addEventListener("blur", () => this.searchInput.style.borderColor = "#333");
this.searchInput.addEventListener("input", debounce(() => this.wfList?.setQuery(this.searchInput.value), 200));
const languageSelect = document.createElement("select");
languageSelect.title = t("language");
languageSelect.value = getLanguage();
languageSelect.style.cssText = `
padding:5px 7px;background:#242424;color:#ddd;
border:1px solid #333;border-radius:5px;font-size:11px;outline:none;
`;
[
["zh", "中文"],
["en", "English"],
].forEach(([value, label]) => {
const option = document.createElement("option");
option.value = value;
option.textContent = label;
option.selected = value === getLanguage();
languageSelect.appendChild(option);
});
languageSelect.addEventListener("change", () => {
setLanguage(languageSelect.value);
this._build();
this.loadData();
});
const saveBtnQuick = this._btn(`💾 ${t("saveCurrent")}`, "#0066cc");
const saveBtnAs = this._btn(`📋 ${t("saveAs")}`, "#2a2a2a");
const groupBtn = this._btn(`📁 ${t("createGroup")}`, "#2a2a2a");
const refreshBtn = this._btn("↻", "#2a2a2a");
refreshBtn.title = t("refresh");
this.logToggleBtn = this._btn("", "#2a2a2a");
this.logToggleBtn.title = this._langText("后台日志", "Backend logs");
this._updateLogToggle();
saveBtnQuick.addEventListener("click", () => this._saveCurrentQuick());
saveBtnAs.addEventListener("click", () => this._saveCurrentAs());
groupBtn.addEventListener("click", () => this._createGroup());
refreshBtn.addEventListener("click", () => this.loadData());
this.logToggleBtn.addEventListener("click", () => this._toggleBackendLogging());
[title, this.searchInput, languageSelect, this.logToggleBtn, saveBtnQuick, saveBtnAs, groupBtn, refreshBtn]
.forEach(el => topbar.appendChild(el));
const body = document.createElement("div");
body.style.cssText = "display:flex;flex:1;overflow:hidden;position:relative;";
const treeWrap = document.createElement("div");
treeWrap.style.cssText = `
width:110px;flex-shrink:0;border-right:1px solid #2a2a2a;
overflow-y:auto;background:#161616;padding:6px 0;
`;
const listWrap = document.createElement("div");
listWrap.style.cssText = "flex:1;overflow-y:auto;padding:8px;";
this.groupTree = new GroupTree(treeWrap, {
onSelect: id => { this.currentGroup = id; this.wfList.setGroup(id); },
onDrop: (wfId, groupId) => this._moveWorkflow(wfId, groupId),
onRefresh: () => this.loadData(),
});
this.wfList = new WorkflowList(listWrap, {
onLoad: id => this._loadWorkflow(id),
onRefresh: () => this.loadData(),
onShowVersions: (id, name) => this.versionPanel.show(id, name),
});
this.versionPanel = new VersionHistory(body, {
onRestore: () => this.loadData(),
});
body.appendChild(treeWrap);
body.appendChild(listWrap);
const statusBar = document.createElement("div");
statusBar.style.cssText = `
padding:4px 10px;border-top:1px solid #2a2a2a;
display:flex;align-items:center;justify-content:space-between;
flex-shrink:0;background:#141414;
`;
this.statusEl = document.createElement("span");
this.statusEl.style.cssText = "font-size:11px;color:#FFD700;";
this.countEl = document.createElement("span");
this.countEl.style.cssText = "font-size:11px;color:#444;";
statusBar.appendChild(this.statusEl);
statusBar.appendChild(this.countEl);
this.container.appendChild(topbar);
this.container.appendChild(body);
this.container.appendChild(statusBar);
}
_btn(text, bg) {
const btn = document.createElement("button");
btn.textContent = text;
btn.style.cssText = `
padding:5px 10px;background:${bg};color:#fff;
border:1px solid #3a3a3a;border-radius:5px;
cursor:pointer;font-size:11px;flex-shrink:0;
transition:filter .12s;white-space:nowrap;
`;
btn.addEventListener("mouseenter", () => btn.style.filter = "brightness(1.2)");
btn.addEventListener("mouseleave", () => btn.style.filter = "");
return btn;
}
async loadData() {
try {
const [res, settingsRes] = await Promise.all([
API.list(),
API.settings(),
]);
this.workflows = res.workflows || {};
this.groups = res.groups || {};
if (settingsRes.status === "success") {
this.backendLoggingEnabled = settingsRes.settings?.backend_logging_enabled !== false;
this._updateLogToggle();
}
this.groupTree?.setData(this.groups);
this.groupTree?.setActive(this.currentGroup);
this.wfList?.setData(this.workflows, this.groups);
this.wfList?.setGroup(this.currentGroup);
if (this.countEl) {
this.countEl.textContent = t("count", { count: Object.keys(this.workflows).length });
}
} catch (error) {
console.error("[yeban-WM] 加载数据失败:", error);
}
}
setStatus(text) {
if (this.statusEl) this.statusEl.textContent = text;
}
_updateLogToggle() {
if (!this.logToggleBtn) return;
this.logToggleBtn.textContent = this.backendLoggingEnabled
? this._langText("日志: 开", "Logs: On")
: this._langText("日志: 关", "Logs: Off");
this.logToggleBtn.title = this._langText("后台日志", "Backend logs");
this.logToggleBtn.style.color = this.backendLoggingEnabled ? "#fff" : "#888";
}
_langText(zh, en) {
return getLanguage() === "zh" ? zh : en;
}
async _toggleBackendLogging() {
try {
const res = await API.updateSettings({
backend_logging_enabled: !this.backendLoggingEnabled,
});
if (res.status !== "success") {
this.setStatus(`${this._langText("设置失败", "Settings failed")}: ${res.message || "unknown error"}`);
return;
}
this.backendLoggingEnabled = res.settings?.backend_logging_enabled !== false;
this._updateLogToggle();
this.setStatus(this.backendLoggingEnabled
? this._langText("后台日志已开启", "Backend logs enabled")
: this._langText("后台日志已关闭", "Backend logs disabled"));
} catch (error) {
console.error("[yeban-WM] 更新设置失败:", error);
this.setStatus(this._langText("设置异常", "Settings error"));
}
}
async _saveCurrentQuick() {
try {
let tabName = getActiveTabName();
if (!tabName || tabName.trim() === "") {
tabName = generateDefaultWorkflowName();
setActiveTabName(tabName);
}
tabName = tabName.trim();
const groupId = (this.currentGroup && this.currentGroup !== "__starred__") ? this.currentGroup : null;
const workflowData = window.app.graph.serialize();
const dataUrl = captureCanvasThumbnail();
let isOverwriting = false;
for (const workflowId of Object.keys(this.workflows)) {
const workflow = this.workflows[workflowId];
if (workflow.name === tabName && workflow.group === groupId) {
isOverwriting = true;
break;
}
}
const res = await API.save(tabName, JSON.stringify(workflowData), groupId, { auto_save: false });
if (res.status === "success") {
if (dataUrl) await API.saveThumbnail(res.workflow_id, dataUrl);
const tag = isOverwriting ? `⚡ ${t("overwritten")}` : `✓ ${t("saved")}`;
this.setStatus(`${tag}: ${tabName}`);
this.onSaved?.({ workflow_id: res.workflow_id, name: tabName, group_id: groupId });
await this.loadData();
} else {
this.setStatus(`✗ ${t("saveFailed")}: ${res.message}`);
}
} catch (error) {
console.error("[yeban-WM] 快速保存失败:", error);
this.setStatus(`✗ ${t("saveError")}`);
}
}
async _saveCurrentAs() {
try {
let defaultName = getActiveTabName();
if (!defaultName || defaultName.trim() === "") {
defaultName = generateDefaultWorkflowName();
}
const result = await showSaveAsDialog({
defaultName,
groups: this.groups,
currentGroupId: this.currentGroup && this.currentGroup !== "__starred__" ? this.currentGroup : null,
confirmText: t("saveAs"),
cancelText: t("cancel"),
});
if (!result) return;
const { name, groupId } = result;
const workflowData = window.app.graph.serialize();
const dataUrl = captureCanvasThumbnail();
const res = await API.save(name, JSON.stringify(workflowData), groupId, { auto_save: false });
if (res.status === "success") {
if (dataUrl) await API.saveThumbnail(res.workflow_id, dataUrl);
setActiveTabName(name);
this.setStatus(`✓ ${t("savedAs")}: ${name}`);
this.onSaved?.({ workflow_id: res.workflow_id, name, group_id: groupId });
await this.loadData();
} else {
this.setStatus(`✗ ${t("saveFailed")}: ${res.message}`);
}
} catch (error) {
console.error("[yeban-WM] 另存为失败:", error);
this.setStatus(`✗ ${t("saveError")}`);
}
}
async _loadWorkflow(id) {
const res = await API.load(id);
if (res.status !== "success") return;
await window.app.loadGraphData(res.data);
const wf = this.workflows[id];
setActiveTabName(wf?.name || id);
this.setStatus(`✓ ${t("workflowLoaded")}: ${wf?.name || id}`);
this.onSaved?.({
workflow_id: id,
name: wf?.name || id,
group_id: wf?.group || null,
});
}
async _createGroup() {
const result = await showDialog({
title: t("createGroup"),
fields: [{ key: "name", label: t("groupName"), placeholder: t("enterGroupName") }],
});
if (!result?.name) return;
const parentId = (this.currentGroup && this.currentGroup !== "__starred__") ? this.currentGroup : null;
await API.createGroup(result.name, parentId);
await this.loadData();
}
async _moveWorkflow(wfId, targetGroupId) {
await API.move(wfId, targetGroupId);
await this.loadData();
}
}