/** * 主应用逻辑 * 负责状态管理、事件绑定、界面渲染,以及维度筛选、地址栏导航与三种模式的差异化控制 */ // 全局应用状态 const state = { projects: [], // 项目列表 currentProject: null, // 当前选中项目 configs: [], // 当前项目全部配置(含 path) visibleConfigs: [], // 右侧面板当前展示的配置 mode: "edit", // 当前模式: plan/edit/readonly editingConfigId: null, // 正在编辑的配置项 ID(null 表示新增) confirmCallback: null, // 确认框回调 dimFilter: { idc: "", environment: "", group: "" }, // 维度栏筛选值(空=全部) }; /** * 显示 Toast 提示 * @param {string} message - 提示内容 * @param {string} type - 类型: success/error/info * @returns {void} */ function toast(message, type = "info") { const el = document.createElement("div"); el.className = `toast toast-${type}`; el.textContent = message; document.body.appendChild(el); setTimeout(() => el.remove(), 2500); } /** * 设置状态栏文字 * @param {string} text - 状态文字 * @returns {void} */ function setStatus(text) { document.getElementById("status-text").textContent = text; } /** * 打开模态框 * @param {string} id - 模态框元素 ID * @returns {void} */ function openModal(id) { document.getElementById(id).style.display = "flex"; } /** * 关闭模态框 * @param {string} id - 模态框元素 ID * @returns {void} */ function closeModal(id) { document.getElementById(id).style.display = "none"; } /** * 模式的中文名称映射 */ const MODE_NAMES = { plan: "规划", edit: "编辑", readonly: "只读" }; /** * 映射方式中文名称 */ const MAPPING_NAMES = { path: "路径", label: "标签" }; /** * 应用当前模式到界面(更新样式类、按钮状态、状态栏) * @returns {void} */ function applyMode() { document.body.classList.remove("mode-plan", "mode-edit", "mode-readonly"); document.body.classList.add(`mode-${state.mode}`); document.querySelectorAll(".mode-btn").forEach((btn) => { btn.classList.toggle("active", btn.dataset.mode === state.mode); }); document.getElementById("status-mode").textContent = `当前模式:${MODE_NAMES[state.mode]}`; } /** * HTML 转义,防止 XSS * @param {string} str - 原始字符串 * @returns {string} 转义后的字符串 */ function escapeHtml(str) { const div = document.createElement("div"); div.textContent = str; return div.innerHTML; } /** * 加载项目列表并填充选择器 * @returns {Promise} */ async function loadProjects() { state.projects = await API.getProjects(); const selector = document.getElementById("project-selector"); selector.innerHTML = ''; state.projects.forEach((p) => { const opt = document.createElement("option"); opt.value = p.id; opt.textContent = p.name; selector.appendChild(opt); }); } /** * 选中并加载指定项目的配置 * @param {string} projectId - 项目 ID * @returns {Promise} */ async function selectProject(projectId) { state.dimFilter = { idc: "", environment: "", group: "" }; if (!projectId) { state.currentProject = null; state.configs = []; document.getElementById("current-project-name").textContent = "未选择项目"; renderDimensionBar(); Tree.render(document.getElementById("tree-content"), null, [], onTreeSelect); renderConfigTable([], "/"); return; } state.currentProject = state.projects.find((p) => p.id === projectId); state.configs = await API.getConfigs(projectId); document.getElementById("current-project-name").textContent = state.currentProject.name; renderDimensionBar(); refreshTree(); renderConfigTable(state.configs, "/"); setStatus(`已加载项目:${state.currentProject.name},共 ${state.configs.length} 项配置`); } /** * 依据 label 维度筛选,得到用于展示的配置列表 * 仅映射为 label 的维度参与筛选;映射为 path 的维度已体现在树路径中 * @returns {Array} 筛选后的配置列表 */ function getFilteredConfigs() { const p = state.currentProject; if (!p) return []; return state.configs.filter((c) => { if (p.enableIdc && p.idcMapping === "label" && state.dimFilter.idc && c.idc !== state.dimFilter.idc) return false; if (p.enableEnvironment && p.environmentMapping === "label" && state.dimFilter.environment && c.environment !== state.dimFilter.environment) return false; if (p.enableGroup && p.groupMapping === "label" && state.dimFilter.group && c.group !== state.dimFilter.group) return false; return true; }); } /** * 重新构建并渲染配置树(应用当前维度筛选) * @returns {void} */ function refreshTree() { Tree.render(document.getElementById("tree-content"), state.currentProject, getFilteredConfigs(), onTreeSelect); } /** * 收集某维度在当前项目配置中出现过的全部取值 * @param {string} field - 维度字段名(idc/environment/group) * @returns {Array} 去重后的取值列表 */ function collectDimValues(field) { const set = new Set(); state.configs.forEach((c) => { if (c[field]) set.add(c[field]); }); return Array.from(set).sort(); } /** * 渲染维度栏:展示已启用维度的筛选下拉与映射方式 * @returns {void} */ function renderDimensionBar() { const p = state.currentProject; const mappingBtn = document.getElementById("btn-dim-mapping"); const items = [ { key: "idc", enable: "enableIdc", mapping: "idcMapping", field: "idc", box: "dim-idc", sel: "dim-idc-select", map: "dim-idc-mapping" }, { key: "environment", enable: "enableEnvironment", mapping: "environmentMapping", field: "environment", box: "dim-env", sel: "dim-env-select", map: "dim-env-mapping" }, { key: "group", enable: "enableGroup", mapping: "groupMapping", field: "group", box: "dim-group", sel: "dim-group-select", map: "dim-group-mapping" }, ]; if (!p) { mappingBtn.style.display = "none"; items.forEach((it) => (document.getElementById(it.box).style.display = "none")); return; } const anyEnabled = p.enableIdc || p.enableEnvironment || p.enableGroup; mappingBtn.style.display = anyEnabled ? "" : "none"; items.forEach((it) => { const box = document.getElementById(it.box); if (!p[it.enable]) { box.style.display = "none"; return; } box.style.display = ""; // 填充筛选下拉 const sel = document.getElementById(it.sel); const values = collectDimValues(it.field); sel.innerHTML = '' + values.map((v) => ``).join(""); sel.value = state.dimFilter[it.key] || ""; // 展示映射方式 document.getElementById(it.map).textContent = `[${MAPPING_NAMES[p[it.mapping]] || "路径"}]`; }); } /** * 树节点选中回调 * @param {Array} configs - 该节点下的配置列表 * @param {string} path - 节点路径 * @returns {void} */ function onTreeSelect(configs, path) { state.visibleConfigs = configs; renderConfigTable(configs, path); } /** * 渲染右侧配置表格并同步地址栏 * @param {Array} configs - 配置项列表 * @param {string} path - 当前路径 * @returns {void} */ function renderConfigTable(configs, path) { state.visibleConfigs = configs; document.getElementById("address-input").value = path === "/" ? "" : path; const tbody = document.getElementById("config-tbody"); tbody.innerHTML = ""; if (!configs || configs.length === 0) { tbody.innerHTML = '当前节点暂无配置项'; return; } configs.forEach((cfg) => { const tr = document.createElement("tr"); tr.innerHTML = ` ${escapeHtml(cfg.key)} ${escapeHtml(String(cfg.value))} ${escapeHtml(cfg.type)} ${escapeHtml(cfg.description)} `; tbody.appendChild(tr); }); } /** * 根据当前项目维度开关,控制配置模态框中维度字段的显示 * @returns {void} */ function updateDimensionFields() { const p = state.currentProject; document.getElementById("dim-idc-group").style.display = p && p.enableIdc ? "" : "none"; document.getElementById("dim-env-group").style.display = p && p.enableEnvironment ? "" : "none"; document.getElementById("dim-group-group").style.display = p && p.enableGroup ? "" : "none"; } /** * 打开配置编辑模态框 * @param {Object|null} config - 配置项,null 表示新增 * @param {boolean} readonly - 是否只读查看 * @returns {void} */ function openConfigModal(config, readonly) { state.editingConfigId = config ? config.id : null; document.getElementById("modal-config-title").textContent = readonly ? "查看配置项" : config ? "编辑配置项" : "新增配置项"; // 新增时用维度栏当前筛选值预填维度,方便就地新增 const preIdc = config ? config.idc || "" : state.dimFilter.idc || ""; const preEnv = config ? config.environment || "" : state.dimFilter.environment || ""; const preGroup = config ? config.group || "" : state.dimFilter.group || ""; document.getElementById("config-key").value = config ? config.key : ""; document.getElementById("config-idc").value = preIdc; document.getElementById("config-environment").value = preEnv; document.getElementById("config-group").value = preGroup; document.getElementById("config-value").value = config ? config.value : ""; document.getElementById("config-type").value = config ? config.type : "string"; document.getElementById("config-value-range").value = config ? config.valueRange || "" : ""; document.getElementById("config-description").value = config ? config.description : ""; document.getElementById("config-purpose").value = config ? config.purpose || "" : ""; updateDimensionFields(); applyFieldPermissions(readonly); openModal("modal-config"); } /** * 根据模式和是否只读查看,设置配置模态框各字段的可编辑性 * 规划模式:结构字段(类型/说明/用途/范围/键名/维度)可改,值不可改 * 编辑模式:值可改,已有项的结构字段锁定 * 只读查看:全部禁用 * @param {boolean} readonly - 是否只读查看 * @returns {void} */ function applyFieldPermissions(readonly) { const structFields = [ "config-key", "config-type", "config-value-range", "config-description", "config-purpose", "config-idc", "config-environment", "config-group", ]; const valueField = "config-value"; const saveBtn = document.getElementById("btn-save-config"); const disableAll = (disabled) => { structFields.forEach((id) => (document.getElementById(id).disabled = disabled)); document.getElementById(valueField).disabled = disabled; }; if (readonly || state.mode === "readonly") { disableAll(true); saveBtn.style.display = "none"; return; } saveBtn.style.display = ""; if (state.mode === "plan") { // 规划模式:可改结构,值不可改 structFields.forEach((id) => (document.getElementById(id).disabled = false)); document.getElementById(valueField).disabled = true; } else if (state.mode === "edit") { // 编辑模式:新增时结构可填;编辑已有项时仅值可改(键名/维度也允许调整路径) const isNew = !state.editingConfigId; document.getElementById("config-type").disabled = !isNew; document.getElementById("config-value-range").disabled = !isNew; document.getElementById("config-description").disabled = !isNew; document.getElementById("config-purpose").disabled = !isNew; document.getElementById("config-key").disabled = false; document.getElementById("config-idc").disabled = false; document.getElementById("config-environment").disabled = false; document.getElementById("config-group").disabled = false; document.getElementById(valueField).disabled = false; } } /** * 收集配置模态框表单数据 * @returns {Object} 表单数据 */ function collectConfigForm() { return { key: document.getElementById("config-key").value.trim(), idc: document.getElementById("config-idc").value.trim(), environment: document.getElementById("config-environment").value.trim(), group: document.getElementById("config-group").value.trim(), value: document.getElementById("config-value").value, type: document.getElementById("config-type").value, valueRange: document.getElementById("config-value-range").value.trim(), description: document.getElementById("config-description").value.trim(), purpose: document.getElementById("config-purpose").value.trim(), }; } /** * 保存配置项(新增或更新) * @returns {Promise} */ async function saveConfig() { if (!state.currentProject) { toast("请先选择项目", "error"); return; } const data = collectConfigForm(); try { if (state.editingConfigId) { await API.updateConfig(state.currentProject.id, state.editingConfigId, data); toast("配置已更新", "success"); } else { await API.createConfig(state.currentProject.id, data); toast("配置已新增", "success"); } closeModal("modal-config"); await selectProject(state.currentProject.id); } catch (err) { toast(err.message, "error"); } } /** * 保存新建项目 * @returns {Promise} */ async function saveProject() { const data = { name: document.getElementById("project-name").value.trim(), description: document.getElementById("project-description").value.trim(), enableIdc: document.getElementById("project-enable-idc").checked, enableEnvironment: document.getElementById("project-enable-env").checked, enableGroup: document.getElementById("project-enable-group").checked, idcMapping: document.getElementById("project-idc-mapping").value, environmentMapping: document.getElementById("project-env-mapping").value, groupMapping: document.getElementById("project-group-mapping").value, }; try { const project = await API.createProject(data); toast("项目已创建", "success"); closeModal("modal-project"); await loadProjects(); document.getElementById("project-selector").value = project.id; await selectProject(project.id); } catch (err) { toast(err.message, "error"); } } /** * 打开维度映射配置模态框,回填当前项目映射方式 * @returns {void} */ function openMappingModal() { const p = state.currentProject; if (!p) { toast("请先选择项目", "error"); return; } document.getElementById("map-idc-row").style.display = p.enableIdc ? "" : "none"; document.getElementById("map-env-row").style.display = p.enableEnvironment ? "" : "none"; document.getElementById("map-group-row").style.display = p.enableGroup ? "" : "none"; document.getElementById("map-idc-mapping").value = p.idcMapping || "path"; document.getElementById("map-env-mapping").value = p.environmentMapping || "path"; document.getElementById("map-group-mapping").value = p.groupMapping || "path"; openModal("modal-mapping"); } /** * 保存维度映射配置 * @returns {Promise} */ async function saveMapping() { if (!state.currentProject) return; const mappings = { idcMapping: document.getElementById("map-idc-mapping").value, environmentMapping: document.getElementById("map-env-mapping").value, groupMapping: document.getElementById("map-group-mapping").value, }; try { await API.updateMappings(state.currentProject.id, mappings); toast("映射配置已保存", "success"); closeModal("modal-mapping"); await selectProject(state.currentProject.id); } catch (err) { toast(err.message, "error"); } } /** * 显示确认框 * @param {string} message - 提示信息 * @param {Function} callback - 确认回调 * @returns {void} */ function confirmAction(message, callback) { document.getElementById("confirm-message").textContent = message; state.confirmCallback = callback; openModal("modal-confirm"); } /** * 导出当前项目配置为 JSON 文件 * @returns {void} */ function exportConfig() { if (!state.currentProject) { toast("请先选择项目", "error"); return; } const payload = { project: state.currentProject, configs: state.configs }; const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${state.currentProject.name}-configs.json`; a.click(); URL.revokeObjectURL(url); toast("配置已导出", "success"); } /** * 导入配置文件(逐条新增到当前项目) * @param {File} file - 用户选择的 JSON 文件 * @returns {Promise} */ async function importConfig(file) { if (!state.currentProject) { toast("请先选择项目", "error"); return; } if (state.mode === "readonly") { toast("只读模式下不允许导入", "error"); return; } try { const text = await file.text(); const payload = JSON.parse(text); const configs = payload.configs || []; let count = 0; for (const cfg of configs) { try { await API.createConfig(state.currentProject.id, cfg); count++; } catch { // 忽略单条失败,继续导入其余配置 } } toast(`成功导入 ${count} 项配置`, "success"); await selectProject(state.currentProject.id); } catch (err) { toast("导入失败:" + err.message, "error"); } } /** * 地址栏跳转:按输入路径定位并选中树节点 * @returns {void} */ function navigateToPath() { if (!state.currentProject) { toast("请先选择项目", "error"); return; } const target = document.getElementById("address-input").value.trim(); if (target === "" || target === "/") { Tree.selectByPath(document.getElementById("tree-content"), ""); renderConfigTable(getFilteredConfigs(), "/"); return; } const ok = Tree.selectByPath(document.getElementById("tree-content"), target); if (!ok) { toast("未找到该路径:" + target, "error"); } } /** * 拷贝地址栏路径到剪贴板 * @returns {Promise} */ async function copyAddress() { const text = document.getElementById("address-input").value; try { await navigator.clipboard.writeText(text); toast("路径已拷贝", "success"); } catch { // 剪贴板不可用时退化为选中输入框内容 document.getElementById("address-input").select(); toast("请手动复制(Ctrl+C)", "info"); } } /** * 绑定所有事件监听 * @returns {void} */ function bindEvents() { // 项目选择 document.getElementById("project-selector").addEventListener("change", (e) => { selectProject(e.target.value); }); // 新建项目 document.getElementById("btn-new-project").addEventListener("click", () => { document.getElementById("project-name").value = ""; document.getElementById("project-description").value = ""; document.getElementById("project-enable-idc").checked = true; document.getElementById("project-enable-env").checked = true; document.getElementById("project-enable-group").checked = true; document.getElementById("project-idc-mapping").value = "path"; document.getElementById("project-env-mapping").value = "path"; document.getElementById("project-group-mapping").value = "path"; openModal("modal-project"); }); document.getElementById("btn-save-project").addEventListener("click", saveProject); // 删除项目 document.getElementById("btn-delete-project").addEventListener("click", () => { if (!state.currentProject) { toast("请先选择项目", "error"); return; } const proj = state.currentProject; confirmAction(`确认删除项目 "${proj.name}" 及其全部配置吗?`, async () => { try { await API.deleteProject(proj.id); toast("项目已删除", "success"); closeModal("modal-confirm"); await loadProjects(); await selectProject(""); document.getElementById("project-selector").value = ""; } catch (err) { toast(err.message, "error"); } }); }); // 模式切换 document.querySelectorAll(".mode-btn").forEach((btn) => { btn.addEventListener("click", async () => { try { await API.setMode(btn.dataset.mode); state.mode = btn.dataset.mode; applyMode(); toast(`已切换到${MODE_NAMES[state.mode]}模式`, "info"); } catch (err) { toast(err.message, "error"); } }); }); // 新增配置 document.getElementById("btn-add-config").addEventListener("click", () => { if (!state.currentProject) { toast("请先选择项目", "error"); return; } if (state.mode === "readonly") { toast("只读模式下不允许新增", "error"); return; } openConfigModal(null, false); }); document.getElementById("btn-save-config").addEventListener("click", saveConfig); // 维度筛选下拉 document.getElementById("dim-idc-select").addEventListener("change", (e) => { state.dimFilter.idc = e.target.value; refreshTree(); renderConfigTable(getFilteredConfigs(), "/"); }); document.getElementById("dim-env-select").addEventListener("change", (e) => { state.dimFilter.environment = e.target.value; refreshTree(); renderConfigTable(getFilteredConfigs(), "/"); }); document.getElementById("dim-group-select").addEventListener("change", (e) => { state.dimFilter.group = e.target.value; refreshTree(); renderConfigTable(getFilteredConfigs(), "/"); }); // 维度映射配置 document.getElementById("btn-dim-mapping").addEventListener("click", openMappingModal); document.getElementById("btn-save-mapping").addEventListener("click", saveMapping); // 地址栏 document.getElementById("btn-address-go").addEventListener("click", navigateToPath); document.getElementById("btn-address-copy").addEventListener("click", copyAddress); document.getElementById("address-input").addEventListener("keydown", (e) => { if (e.key === "Enter") navigateToPath(); }); // 导出/导入 document.getElementById("btn-export-config").addEventListener("click", exportConfig); document.getElementById("btn-import-config").addEventListener("click", () => { document.getElementById("import-file-input").click(); }); document.getElementById("import-file-input").addEventListener("change", (e) => { if (e.target.files[0]) { importConfig(e.target.files[0]); e.target.value = ""; } }); // 表格操作(事件委托) document.getElementById("config-tbody").addEventListener("click", (e) => { const btn = e.target.closest(".row-btn"); if (!btn) return; const id = btn.dataset.id; const action = btn.dataset.action; const config = state.configs.find((c) => c.id === id); if (!config) return; if (action === "view") { openConfigModal(config, true); } else if (action === "edit") { if (state.mode === "readonly") { toast("只读模式下不允许编辑", "error"); return; } openConfigModal(config, false); } else if (action === "delete") { if (state.mode === "readonly") { toast("只读模式下不允许删除", "error"); return; } confirmAction(`确认删除配置项 "${config.key}" 吗?`, async () => { try { await API.deleteConfig(state.currentProject.id, id); toast("配置已删除", "success"); closeModal("modal-confirm"); await selectProject(state.currentProject.id); } catch (err) { toast(err.message, "error"); } }); } }); // 确认框 document.getElementById("btn-confirm-ok").addEventListener("click", () => { if (state.confirmCallback) state.confirmCallback(); }); // 全部折叠 document.getElementById("btn-collapse-all").addEventListener("click", () => { document.querySelectorAll(".tree-children").forEach((el) => el.classList.add("collapsed")); document.querySelectorAll(".tree-toggle").forEach((el) => { if (el.textContent === "▼") el.textContent = "▶"; }); }); // 分隔条拖拽 bindResizer(); } /** * 绑定左右面板分隔条的拖拽调整宽度功能 * @returns {void} */ function bindResizer() { const resizer = document.getElementById("resizer"); const treePanel = document.getElementById("tree-panel"); let dragging = false; resizer.addEventListener("mousedown", () => { dragging = true; document.body.style.cursor = "col-resize"; }); document.addEventListener("mousemove", (e) => { if (!dragging) return; const width = Math.max(180, Math.min(600, e.clientX)); treePanel.style.width = width + "px"; }); document.addEventListener("mouseup", () => { dragging = false; document.body.style.cursor = ""; }); } /** * 应用初始化入口 * @returns {Promise} */ async function init() { bindEvents(); try { const modeData = await API.getMode(); state.mode = modeData.mode; } catch { state.mode = "edit"; } applyMode(); await loadProjects(); setStatus("就绪"); } // 页面加载完成后启动 window.addEventListener("DOMContentLoaded", init);