registry/public/js/app.js
Cheney f9f3184f98
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 31s
fix
2026-08-07 14:46:52 +08:00

867 lines
30 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 主应用逻辑
* 负责状态管理、事件绑定、界面渲染,以及维度筛选、地址栏导航与三种模式的差异化控制
*/
// 全局应用状态
const state = {
projects: [], // 项目列表
currentProject: null, // 当前选中项目
configs: [], // 当前项目全部配置(含 path
visibleConfigs: [], // 右侧面板当前展示的配置
mode: "edit", // 当前模式: plan/edit/readonly
editingConfigId: null, // 正在编辑的配置项 IDnull 表示新增)
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: "标签" };
/**
* string 类型值范围的常见格式正则预设(前端选择后填入值范围输入框,后端按 /正则/ 校验)
* regex 为去掉首尾斜杠与修饰符的正则体
*/
const RANGE_PRESETS = {
phone: { label: "手机号", regex: "^1[3-9]\\d{9}$" },
email: { label: "邮箱", regex: "^[\\w.%+-]+@[\\w.-]+\\.[A-Za-z]{2,}$" },
idcard: { label: "身份证", regex: "^\\d{17}[\\dXx]$" },
ipv4: { label: "IPv4", regex: "^(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}$" },
url: { label: "URL", regex: "^https?:\\/\\/[\\w.-]+(?:\\.[a-zA-Z]{2,})?(?:\\/[^\\s]*)?$" },
port: { label: "端口号", regex: "^([1-9]\\d{0,3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])$" },
alphanumeric: { label: "字母数字", regex: "^[A-Za-z0-9]+$" },
};
/**
* 应用当前模式到界面(更新样式类、按钮状态、状态栏)
* @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<void>}
*/
async function loadProjects() {
state.projects = await API.getProjects();
const selector = document.getElementById("project-selector");
selector.innerHTML = '<option value="">-- 选择项目 --</option>';
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<void>}
*/
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<string>} 去重后的取值列表
*/
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 = "";
// 填充筛选下拉(用 DOM 构建避免属性上下文注入)
const sel = document.getElementById(it.sel);
const values = collectDimValues(it.field);
sel.innerHTML = '<option value="">全部</option>';
values.forEach((v) => {
const opt = document.createElement("option");
opt.value = v;
opt.textContent = v;
sel.appendChild(opt);
});
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 = '<tr class="empty-row"><td colspan="6">当前节点暂无配置项</td></tr>';
return;
}
configs.forEach((cfg) => {
const exists = cfg.exists !== false;
const tr = document.createElement("tr");
if (!exists) tr.classList.add("row-not-exists");
const statusHtml = [];
if (cfg.required === true) statusHtml.push('<span class="status-badge badge-required">必填</span>');
statusHtml.push(
exists
? '<span class="status-badge badge-exists">存在</span>'
: '<span class="status-badge badge-missing">不存在</span>'
);
tr.innerHTML = `
<td class="config-key-cell">${escapeHtml(cfg.key)}</td>
<td class="config-value-cell">${buildValueCell(cfg, exists)}</td>
<td><span class="type-badge">${escapeHtml(cfg.type)}</span></td>
<td class="config-status-cell">${statusHtml.join("")}</td>
<td>${escapeHtml(cfg.description)}</td>
<td>
<button class="row-btn view" data-action="view" data-id="${cfg.id}">查看</button>
<button class="row-btn" data-action="edit" data-id="${cfg.id}">编辑</button>
<button class="row-btn danger" data-action="delete" data-id="${cfg.id}">删除</button>
</td>
`;
tbody.appendChild(tr);
});
}
/**
* 构建表格值列 HTML存在且值非空显示实际值值为空但配置了默认值时回退展示默认值不存在显示 —
* @param {Object} cfg - 配置项
* @param {boolean} exists - 是否存在
* @returns {string} 值列 HTML
*/
function buildValueCell(cfg, exists) {
if (!exists) return '<span class="value-missing">—</span>';
const value = String(cfg.value);
if (value.trim() !== "") return escapeHtml(value);
const hasDefault =
cfg.defaultValue !== undefined && cfg.defaultValue !== null && String(cfg.defaultValue).trim() !== "";
if (hasDefault) {
return `<span class="value-default" title="配置值为空,回退展示默认值">默认 ${escapeHtml(String(cfg.defaultValue))}</span>`;
}
return "";
}
/**
* 根据当前项目维度开关,控制配置模态框中维度字段的显示
* @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-default-value").value =
config && config.defaultValue !== undefined && config.defaultValue !== null ? config.defaultValue : "";
// 编辑模式下提示默认值(留空则回退展示默认值)
document.getElementById("config-value").placeholder =
config && config.defaultValue !== undefined && config.defaultValue !== null && String(config.defaultValue).trim() !== ""
? `留空则使用默认值:${config.defaultValue}`
: "配置值";
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 || "" : "";
document.getElementById("config-required").checked = config ? config.required === true : false;
document.getElementById("config-exists").checked = config ? config.exists !== false : true;
document.getElementById("config-range-preset").value = "";
updateDimensionFields();
updateRangePresetVisibility();
applyFieldPermissions(readonly);
openModal("modal-config");
}
/**
* 根据配置类型显示/隐藏“常见格式”正则预设下拉(仅 string 类型提供)
* @returns {void}
*/
function updateRangePresetVisibility() {
const isString = document.getElementById("config-type").value === "string";
document.getElementById("config-range-preset-group").style.display = isString ? "" : "none";
}
/**
* 根据模式和是否只读查看,设置配置模态框各字段的可编辑性
* 规划模式:结构字段(类型/说明/用途/范围/键名/维度)可改,值不可改
* 编辑模式:值可改,已有项的结构字段锁定
* 只读查看:全部禁用
* @param {boolean} readonly - 是否只读查看
* @returns {void}
*/
function applyFieldPermissions(readonly) {
const structFields = [
"config-key", "config-type", "config-value-range", "config-required",
"config-default-value",
"config-description", "config-purpose",
"config-idc", "config-environment", "config-group",
];
const valueField = "config-value";
const existsField = "config-exists";
const presetField = "config-range-preset";
const saveBtn = document.getElementById("btn-save-config");
const disableAll = (disabled) => {
structFields.forEach((id) => (document.getElementById(id).disabled = disabled));
document.getElementById(valueField).disabled = disabled;
document.getElementById(existsField).disabled = disabled;
document.getElementById(presetField).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;
document.getElementById(existsField).disabled = true;
document.getElementById(presetField).disabled = false;
} 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-required").disabled = !isNew;
document.getElementById("config-default-value").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;
document.getElementById(existsField).disabled = false;
document.getElementById(presetField).disabled = !isNew;
}
}
/**
* 收集配置模态框表单数据
* @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(),
defaultValue: document.getElementById("config-default-value").value,
required: document.getElementById("config-required").checked,
exists: document.getElementById("config-exists").checked,
description: document.getElementById("config-description").value.trim(),
purpose: document.getElementById("config-purpose").value.trim(),
};
}
/**
* 保存配置项(新增或更新)
* @returns {Promise<void>}
*/
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<void>}
*/
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<void>}
*/
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<void>}
*/
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<void>}
*/
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);
// 类型切换:仅 string 类型展示“常见格式”预设
document.getElementById("config-type").addEventListener("change", updateRangePresetVisibility);
// 常见格式预设:选择后自动填入值范围(/正则/ 格式)
document.getElementById("config-range-preset").addEventListener("change", (e) => {
const key = e.target.value;
if (!key) return;
const preset = RANGE_PRESETS[key];
if (!preset) return;
document.getElementById("config-value-range").value = `/${preset.regex}/`;
toast(`已填入${preset.label}格式正则,可继续编辑`, "info");
});
// 维度筛选下拉
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<void>}
*/
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);