722 lines
31 KiB
JavaScript
Raw Normal View History

2025-04-28 12:25:20 +08:00
/**
* 可视化编辑器模块
* @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 = `<div class="error-message">
无法编辑XML文档的根元素不是Service
请确保XML文档的根元素是Service
</div>`;
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 = '<div class="section-title">基本信息</div>';
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 = `
<div class="form-group">
<label for="serviceName">服务名称</label>
<input type="text" id="serviceName" class="form-control" placeholder="请输入服务名称" value="${XmlUtils.escapeHtml(nameElement.textContent || '')}" />
</div>
<div class="form-group">
<label for="serviceDesc">服务描述</label>
<input type="text" id="serviceDesc" class="form-control" placeholder="请输入服务描述" value="${XmlUtils.escapeHtml(descElement.textContent || '')}" />
</div>
`;
basicInfoForm.appendChild(nameDescContainer);
// 其他信息以两列布局
const otherInfoContainer = document.createElement('div');
otherInfoContainer.className = 'two-column-form';
otherInfoContainer.style.marginTop = '15px';
otherInfoContainer.innerHTML = `
<div class="form-group">
<label for="serviceAuthor">作者</label>
<input type="text" id="serviceAuthor" class="form-control" placeholder="请输入作者" value="${XmlUtils.escapeHtml(authorElement.textContent || '')}" />
</div>
<div class="form-group">
<label for="serviceVersion">版本</label>
<input type="text" id="serviceVersion" class="form-control" placeholder="请输入版本号" value="${XmlUtils.escapeHtml(versionElement.textContent || '1.0.0')}" />
</div>
<div class="form-group">
<label for="serviceCreateTime">创建时间</label>
<div class="input-container">
<input type="text" id="serviceCreateTime" class="form-control" placeholder="YYYY-MM-DD HH:MM:SS" value="${XmlUtils.escapeHtml(createTimeElement.textContent || '')}" />
<button class="icon-button calendar-button" title="选择日期和时间" id="createTimeBtn"></button>
<button class="icon-button refresh-button-sm" title="设置为当前时间" id="refreshCreateTimeBtn" style="background-image: url('assets/icons/png/refresh_b.png'); 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;"></button>
</div>
</div>
<div class="form-group">
<label for="serviceChangeTime">修改时间</label>
<div class="input-container">
<input type="text" id="serviceChangeTime" class="form-control" placeholder="YYYY-MM-DD HH:MM:SS" value="${XmlUtils.escapeHtml(changeTimeElement.textContent || '')}" />
<button class="icon-button calendar-button" title="选择日期和时间" id="changeTimeBtn"></button>
<button class="icon-button refresh-button-sm" title="设置为当前时间" id="refreshChangeTimeBtn" style="background-image: url('assets/icons/png/refresh_b.png'); 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;"></button>
</div>
</div>
`;
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 = `
<div class="section-title">指令列表</div>
<button id="addCommandBtn" class="action-button">添加指令</button>
`;
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 = `
<thead>
<tr>
<th style="width: 20%;">名称</th>
<th style="width: 25%;">调用</th>
<th style="width: 40%;">描述</th>
<th style="width: 15%;">操作</th>
</tr>
</thead>
<tbody id="commandsTableBody">
</tbody>
`;
// 创建表格内容
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 = '<td colspan="4" style="text-align: center;">暂无指令</td>';
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 = `
<td>${XmlUtils.escapeHtml(name)}</td>
<td>${XmlUtils.escapeHtml(call)}</td>
<td>${XmlUtils.escapeHtml(description)}</td>
<td>
<button class="action-button edit-btn" data-index="${index}">编辑</button>
<button class="action-button delete-btn" data-index="${index}" style="background-color: #E77979;">删除</button>
</td>
`;
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 = `
<div class="section-title">其他设置</div>
<span class="settings-hint" style="font-size: 12px; color: #777; font-style: italic;">如需添加参数请手动编辑配置文件</span>
`;
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;
}
}