/** * 可视化编辑器模块 * @type {module} */ import { XmlUtils } from './xml-utils.js'; import { DateTimeDialog } from './date-time-dialog.js'; import { CommandDialog } from './command-dialog.js'; export class VisualEditor { /** * 渲染可视化编辑器 * @param {HTMLElement} container - 容器元素 * @param {Document} xmlDoc - XML文档 * @param {Function} onEdit - 编辑回调函数 * @returns {boolean} 是否渲染成功 */ static render(container, xmlDoc, onEdit) { if (!xmlDoc || !container) return false; const style = document.createElement('style'); style.textContent = ` .section { margin-bottom: 20px; border: 1px solid #e0e0e0; border-radius: 4px; padding: 10px; background-color: white; } .section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; padding-bottom: 5px; border-bottom: 1px solid #e0e0e0; } .section-title { font-weight: bold; color: #555; } .property-table { width: 100%; border-collapse: collapse; } .property-table th, .property-table td { text-align: left; padding: 8px; border-bottom: 1px solid #e0e0e0; } .property-table th { background-color: #f5f5f5; } .form-group { margin-bottom: 15px; } .form-group label { display: block; margin-bottom: 5px; font-weight: 500; } .form-control { width: 100%; padding: 8px; box-sizing: border-box; border: 1px solid #ddd; border-radius: 4px; } .inline-form { display: flex; gap: 16px; } .inline-form .form-group { flex: 1; } .two-column-form { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; } .input-container { display: flex; align-items: center; gap: 8px; } .input-container .icon-button { min-width: 28px; height: 28px; background-size: 16px; background-position: center; background-repeat: no-repeat; border: none; cursor: pointer; background-color: transparent; opacity: 0.7; } .input-container .icon-button:hover { opacity: 1; } .calendar-button { background-image: url('assets/icons/png/calendar_b.png'); } .command-table { width: 100%; border-collapse: collapse; margin-top: 10px; } .command-table th, .command-table td { padding: 8px; text-align: left; border: 1px solid #ddd; } .command-table th { background-color: #f5f5f5; } .element-box { display: flex; flex-direction: column; padding: 10px; border: 1px solid #e0e0e0; border-radius: 4px; margin-bottom: 10px; background-color: #f9f9f9; } .element-header { display: flex; align-items: center; margin-bottom: 8px; } .element-name { font-weight: 500; color: #444; flex: 1; } .element-value { width: 100%; } .element-value input { max-width: 100%; } .element-input-row { display: flex; align-items: center; gap: 8px; width: 100%; } .element-input-row .form-control { flex: 1; } .nested-elements { padding-left: 20px; margin-top: 5px; border-left: 1px dashed #ccc; } `; const visualEditor = document.createElement('div'); visualEditor.className = 'visual-editor'; // 获取根元素 const rootElement = xmlDoc.documentElement; // 只处理Service根元素 if (rootElement.nodeName === 'Service') { // 添加基本信息部分 visualEditor.appendChild(this.createBasicInfoSection(xmlDoc, onEdit)); // 添加命令列表部分 visualEditor.appendChild(this.createCommandsSection(xmlDoc, onEdit)); // 添加其他设置部分 visualEditor.appendChild(this.createOtherSettingsSection(xmlDoc, onEdit)); container.appendChild(style); container.appendChild(visualEditor); return true; } else { // 不是Service根元素,显示错误信息 visualEditor.innerHTML = `
无法编辑:XML文档的根元素不是Service。 请确保XML文档的根元素是Service。
`; container.appendChild(style); container.appendChild(visualEditor); return false; } } /** * 创建基本信息部分 */ static createBasicInfoSection(xmlDoc, onEdit) { const rootElement = xmlDoc.documentElement; // 创建基本信息部分 const basicInfoSection = document.createElement('div'); basicInfoSection.className = 'section'; const basicInfoHeader = document.createElement('div'); basicInfoHeader.className = 'section-header'; basicInfoHeader.innerHTML = '
基本信息
'; basicInfoSection.appendChild(basicInfoHeader); // 获取基本信息数据 const nameElement = rootElement.querySelector('Name') || xmlDoc.createElement('Name'); const descElement = rootElement.querySelector('Description') || xmlDoc.createElement('Description'); const authorElement = rootElement.querySelector('Author') || xmlDoc.createElement('Author'); const versionElement = rootElement.querySelector('Version') || xmlDoc.createElement('Version'); const createTimeElement = rootElement.querySelector('CreateTime') || xmlDoc.createElement('CreateTime'); const changeTimeElement = rootElement.querySelector('ChangeTime') || xmlDoc.createElement('ChangeTime'); // 确保元素存在于XML中 if (!rootElement.querySelector('Name')) rootElement.appendChild(nameElement); if (!rootElement.querySelector('Description')) rootElement.appendChild(descElement); if (!rootElement.querySelector('Author')) rootElement.appendChild(authorElement); if (!rootElement.querySelector('Version')) rootElement.appendChild(versionElement); if (!rootElement.querySelector('CreateTime')) { createTimeElement.textContent = DateTimeDialog.getCurrentDateTime(); rootElement.appendChild(createTimeElement); } if (!rootElement.querySelector('ChangeTime')) { changeTimeElement.textContent = DateTimeDialog.getCurrentDateTime(); rootElement.appendChild(changeTimeElement); } // 创建基本信息表单 const basicInfoForm = document.createElement('div'); basicInfoForm.className = 'form-container'; // 服务名称和描述在同一行 const nameDescContainer = document.createElement('div'); nameDescContainer.className = 'inline-form'; nameDescContainer.innerHTML = `
`; basicInfoForm.appendChild(nameDescContainer); // 其他信息以两列布局 const otherInfoContainer = document.createElement('div'); otherInfoContainer.className = 'two-column-form'; otherInfoContainer.style.marginTop = '15px'; otherInfoContainer.innerHTML = `
`; basicInfoForm.appendChild(otherInfoContainer); basicInfoSection.appendChild(basicInfoForm); // 添加事件监听 basicInfoSection.addEventListener('change', (e) => { if (e.target.id === 'serviceName') { nameElement.textContent = e.target.value; if (onEdit) onEdit(); } else if (e.target.id === 'serviceDesc') { descElement.textContent = e.target.value; if (onEdit) onEdit(); } else if (e.target.id === 'serviceAuthor') { authorElement.textContent = e.target.value; if (onEdit) onEdit(); } else if (e.target.id === 'serviceVersion') { versionElement.textContent = e.target.value; if (onEdit) onEdit(); } else if (e.target.id === 'serviceCreateTime') { createTimeElement.textContent = e.target.value; if (onEdit) onEdit(); } else if (e.target.id === 'serviceChangeTime') { changeTimeElement.textContent = e.target.value; if (onEdit) onEdit(); } }); // 添加日期时间选择器事件 basicInfoSection.querySelector('#createTimeBtn').addEventListener('click', () => { const input = basicInfoSection.querySelector('#serviceCreateTime'); DateTimeDialog.show(basicInfoSection.ownerDocument.body, input, (newValue) => { input.value = newValue; createTimeElement.textContent = newValue; if (onEdit) onEdit(); }); }); basicInfoSection.querySelector('#changeTimeBtn').addEventListener('click', () => { const input = basicInfoSection.querySelector('#serviceChangeTime'); DateTimeDialog.show(basicInfoSection.ownerDocument.body, input, (newValue) => { input.value = newValue; changeTimeElement.textContent = newValue; if (onEdit) onEdit(); }); }); // 添加刷新按钮事件 basicInfoSection.querySelector('#refreshCreateTimeBtn').addEventListener('click', () => { const input = basicInfoSection.querySelector('#serviceCreateTime'); const datetimeStr = DateTimeDialog.getCurrentDateTime(); input.value = datetimeStr; createTimeElement.textContent = datetimeStr; if (onEdit) onEdit(); }); basicInfoSection.querySelector('#refreshChangeTimeBtn').addEventListener('click', () => { const input = basicInfoSection.querySelector('#serviceChangeTime'); const datetimeStr = DateTimeDialog.getCurrentDateTime(); input.value = datetimeStr; changeTimeElement.textContent = datetimeStr; if (onEdit) onEdit(); }); return basicInfoSection; } /** * 创建命令列表部分 */ static createCommandsSection(xmlDoc, onEdit) { const rootElement = xmlDoc.documentElement; // 创建命令列表部分 const commandsSection = document.createElement('div'); commandsSection.className = 'section'; const commandsHeader = document.createElement('div'); commandsHeader.className = 'section-header'; commandsHeader.innerHTML = `
指令列表
`; commandsSection.appendChild(commandsHeader); // 获取或创建命令列表 let commandListElement = rootElement.querySelector('CommandList'); if (!commandListElement) { commandListElement = xmlDoc.createElement('CommandList'); rootElement.appendChild(commandListElement); } // 创建命令表格 const commandsTable = document.createElement('table'); commandsTable.className = 'command-table'; commandsTable.innerHTML = ` 名称 调用 描述 操作 `; // 创建表格内容 const commandsTableBody = commandsTable.querySelector('#commandsTableBody'); // 更新命令表格的函数 const updateCommandTable = () => { // 清空表格 commandsTableBody.innerHTML = ''; // 获取所有命令 const commandElements = commandListElement.querySelectorAll('Command'); if (commandElements.length === 0) { // 如果没有命令,显示空行 const emptyRow = document.createElement('tr'); emptyRow.innerHTML = '暂无指令'; commandsTableBody.appendChild(emptyRow); } else { // 添加所有命令到表格 commandElements.forEach((command, index) => { const name = command.getAttribute('Name') || ''; const call = command.getAttribute('Call') || ''; const description = command.getAttribute('Description') || ''; const row = document.createElement('tr'); row.dataset.index = index; row.innerHTML = ` ${XmlUtils.escapeHtml(name)} ${XmlUtils.escapeHtml(call)} ${XmlUtils.escapeHtml(description)} `; commandsTableBody.appendChild(row); }); // 添加编辑和删除事件 commandsTableBody.querySelectorAll('.edit-btn').forEach(btn => { btn.addEventListener('click', () => { const index = parseInt(btn.dataset.index); const command = commandElements[index]; CommandDialog.showEditDialog( commandsSection.ownerDocument.body, command, index, () => { updateCommandTable(); if (onEdit) onEdit(); } ); }); }); commandsTableBody.querySelectorAll('.delete-btn').forEach(btn => { btn.addEventListener('click', () => { const index = parseInt(btn.dataset.index); const command = commandElements[index]; if (confirm(`确定要删除指令 ${command.getAttribute('Name')} 吗?`)) { commandListElement.removeChild(command); updateCommandTable(); if (onEdit) onEdit(); } }); }); } }; // 初始化表格 updateCommandTable(); // 添加"添加指令"按钮事件 commandsSection.querySelector('#addCommandBtn').addEventListener('click', () => { CommandDialog.showAddDialog( commandsSection.ownerDocument.body, commandListElement, xmlDoc, () => { updateCommandTable(); if (onEdit) onEdit(); } ); }); // 添加表格 commandsSection.appendChild(commandsTable); return commandsSection; } /** * 创建其他设置部分 */ static createOtherSettingsSection(xmlDoc, onEdit) { const rootElement = xmlDoc.documentElement; // 创建其他设置部分 const otherSettingsSection = document.createElement('div'); otherSettingsSection.className = 'section'; const otherSettingsHeader = document.createElement('div'); otherSettingsHeader.className = 'section-header'; otherSettingsHeader.innerHTML = `
其他设置
如需添加参数,请手动编辑配置文件 `; otherSettingsSection.appendChild(otherSettingsHeader); // 创建其他设置内容 const otherSettingsContent = document.createElement('div'); otherSettingsContent.className = 'other-settings-container'; // 递归处理元素及其子元素 const processElement = (parentElement, containerElement, level = 0) => { // 筛选节点元素,排除文本节点 const childElements = Array.from(parentElement.childNodes) .filter(node => node.nodeType === 1); // 只处理元素节点 if (childElements.length === 0) return; childElements.forEach(element => { // 排除已处理的基本信息元素和命令列表 const nodeName = element.nodeName; if (['Name', 'Description', 'Author', 'Version', 'CreateTime', 'ChangeTime', 'CommandList'].includes(nodeName)) { return; } // 创建元素框 const elementBox = document.createElement('div'); elementBox.className = 'element-box'; elementBox.id = `element-${getElementPath(element).replace(/\./g, '-')}`; if (level > 0) { elementBox.style.marginLeft = `${level * 10}px`; } // 创建元素头部(包含名称) const elementHeader = document.createElement('div'); elementHeader.className = 'element-header'; // 创建元素名称 const elementName = document.createElement('div'); elementName.className = 'element-name'; elementName.textContent = nodeName; elementHeader.appendChild(elementName); // 将元素头部添加到元素框 elementBox.appendChild(elementHeader); // 创建元素内容容器 const elementContent = document.createElement('div'); elementContent.className = 'element-value'; // 处理不同类型的元素内容 const hasChildElements = Array.from(element.childNodes) .some(node => node.nodeType === 1); if (hasChildElements) { // 如果有子元素,为它们创建一个嵌套容器 const childContainer = document.createElement('div'); childContainer.className = 'nested-elements'; // 递归处理子元素 processElement(element, childContainer, level + 1); elementContent.appendChild(childContainer); // 为父元素添加删除按钮 const deleteBtn = document.createElement('button'); deleteBtn.className = 'action-button'; deleteBtn.textContent = '删除'; deleteBtn.style.backgroundColor = '#e77979'; deleteBtn.style.marginTop = '8px'; deleteBtn.addEventListener('click', () => { if (confirm(`确定要删除 ${nodeName} 及其所有子元素吗?`)) { try { // 从DOM和XML中删除元素 parentElement.removeChild(element); // 移除元素框 if (elementBox.parentNode) { elementBox.parentNode.removeChild(elementBox); } // 触发编辑回调 if (onEdit) onEdit(); // 如果没有其他元素了,显示"没有其他设置项"提示 const otherSettingsContainer = otherSettingsContent; if (otherSettingsContainer && otherSettingsContainer.children.length === 0) { const noElementsMsg = document.createElement('div'); noElementsMsg.style.padding = '10px'; noElementsMsg.style.color = '#666'; noElementsMsg.textContent = '没有其他设置项'; otherSettingsContainer.appendChild(noElementsMsg); } } catch (error) { console.error('删除元素时出错:', error); alert(`删除元素失败: ${error.message}`); } } }); elementContent.appendChild(deleteBtn); } else { // 如果没有子元素,创建可编辑的文本输入 const elementValue = element.textContent || ''; const inputRow = document.createElement('div'); inputRow.className = 'element-input-row'; const input = document.createElement('input'); input.type = 'text'; input.className = 'form-control'; input.value = elementValue; input.dataset.elementPath = getElementPath(element); // 为输入框添加事件监听 input.addEventListener('change', (e) => { element.textContent = e.target.value; if (onEdit) onEdit(); }); inputRow.appendChild(input); // 添加删除按钮 const deleteBtn = document.createElement('button'); deleteBtn.className = 'action-button'; deleteBtn.textContent = '删除'; deleteBtn.style.backgroundColor = '#e77979'; deleteBtn.addEventListener('click', () => { if (confirm(`确定要删除 ${nodeName} 吗?`)) { try { // 从DOM和XML中删除元素 parentElement.removeChild(element); // 移除元素框 if (elementBox.parentNode) { elementBox.parentNode.removeChild(elementBox); } // 触发编辑回调 if (onEdit) onEdit(); // 如果没有其他元素了,显示"没有其他设置项"提示 const otherSettingsContainer = otherSettingsContent; if (otherSettingsContainer && otherSettingsContainer.querySelectorAll('.element-box').length === 0) { const noElementsMsg = document.createElement('div'); noElementsMsg.style.padding = '10px'; noElementsMsg.style.color = '#666'; noElementsMsg.textContent = '没有其他设置项'; otherSettingsContainer.appendChild(noElementsMsg); } } catch (error) { console.error('删除元素时出错:', error); alert(`删除元素失败: ${error.message}`); } } }); inputRow.appendChild(deleteBtn); elementContent.appendChild(inputRow); } // 将元素内容添加到元素框 elementBox.appendChild(elementContent); // 将元素框添加到容器 containerElement.appendChild(elementBox); }); }; // 获取元素的路径,用于唯一标识 const getElementPath = (element) => { const path = []; let current = element; while (current && current !== xmlDoc) { const nodeName = current.nodeName; path.unshift(nodeName); current = current.parentNode; } return path.join('.'); }; // 处理其他元素(排除标准元素) const standardElements = ['Name', 'Description', 'Author', 'Version', 'CreateTime', 'ChangeTime', 'CommandList']; const otherElements = Array.from(rootElement.childNodes) .filter(node => node.nodeType === 1 && !standardElements.includes(node.nodeName)); if (otherElements.length > 0) { // 处理所有其他元素 processElement(rootElement, otherSettingsContent); } else { // 如果没有其他元素,显示提示 const noElementsMsg = document.createElement('div'); noElementsMsg.style.padding = '10px'; noElementsMsg.style.color = '#666'; noElementsMsg.textContent = '没有其他设置项'; otherSettingsContent.appendChild(noElementsMsg); } otherSettingsSection.appendChild(otherSettingsContent); return otherSettingsSection; } }