2025-04-28 12:25:20 +08:00

722 lines
31 KiB
JavaScript
Raw 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.

/**
* 可视化编辑器模块
* @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;
}
}