XNSim/XNSimHtml/bak/service-config.js
2025-04-28 12:25:20 +08:00

2808 lines
108 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.

class ServiceConfig extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.selectedFile = null;
this.xmlContent = '';
this.xmlDoc = null;
this.isEdited = false;
this.serviceFiles = [];
this.isActive = true; // 添加活动状态标记
}
async connectedCallback() {
this.isActive = true;
this.render();
await this.loadServiceFiles();
this.setupEventListeners();
// 延迟调用renderComplete确保DOM已更新
setTimeout(() => {
this.renderComplete();
}, 100);
}
disconnectedCallback() {
this.isActive = false;
}
// 在组件完成渲染后恢复状态
renderComplete() {
// 在DOM渲染完成后更新文件选择器
setTimeout(() => {
// 如果有选中的文件,尝试恢复它
if (this.selectedFile && (this.xmlContent || this.xmlDoc)) {
const fileSelector = this.shadowRoot.getElementById('fileSelector');
if (fileSelector) {
// 确认文件在列表中
const fileExists = Array.from(fileSelector.options).some(opt => opt.value === this.selectedFile);
if (fileExists) {
// 设置选中的文件
fileSelector.value = this.selectedFile;
// 更新内容显示
const contentArea = this.shadowRoot.querySelector('#contentArea');
if (contentArea && this.xmlDoc) {
contentArea.innerHTML = `
<div class="editor-container">
<div class="editor-panel active" id="visualEditor"></div>
</div>
`;
const visualEditor = this.shadowRoot.querySelector('#visualEditor');
if (visualEditor) {
this.renderVisualEditor(visualEditor, this.xmlDoc);
// 恢复编辑状态标记
if (this.isEdited) {
this.markEdited();
}
}
} else {
console.log('ServiceConfig: 无法找到内容区域或XML文档不存在');
}
} else {
// 文件不存在,清除状态
this.selectedFile = null;
this.xmlContent = '';
this.xmlDoc = null;
this.isEdited = false;
}
} else {
console.log('ServiceConfig: 未找到文件选择器');
}
} else {
console.log('ServiceConfig: 没有选中的文件或XML内容');
}
}, 50); // 增加延迟确保DOM已完全加载
}
// 重新激活组件的方法(当标签页重新被选中时调用)
async reactivate() {
if (this.isActive) {
return;
}
this.isActive = true;
try {
// 先重新渲染一次UI以确保Shadow DOM结构完整
this.render();
// 加载文件列表
await this.loadServiceFiles();
// 设置事件监听器
this.setupEventListeners();
// 调用renderComplete来恢复状态
this.renderComplete();
} catch (error) {
console.error('ServiceConfig: 重新激活组件时出错:', error);
}
}
async loadServiceFiles() {
try {
const response = await fetch('/api/service-files');
if (!response.ok) {
throw new Error(`服务器响应错误: ${response.status}`);
}
this.serviceFiles = await response.json();
this.updateFileSelector();
} catch (error) {
console.error('加载服务文件失败:', error);
// 如果请求失败可能是API不存在等待一段时间后重试
const retryLoadFiles = async () => {
try {
console.log('尝试重新加载服务文件列表...');
const retryResponse = await fetch('/api/service-files');
if (retryResponse.ok) {
this.serviceFiles = await retryResponse.json();
this.updateFileSelector();
} else {
console.error('重试加载服务文件失败');
}
} catch (retryError) {
console.error('重试加载服务文件失败:', retryError);
}
};
// 延迟3秒后重试
setTimeout(retryLoadFiles, 3000);
}
}
updateFileSelector() {
const fileSelector = this.shadowRoot.getElementById('fileSelector');
if (!fileSelector) return;
// 清空现有选项
fileSelector.innerHTML = '<option value="">-- 选择服务配置文件 --</option>';
// 按修改时间排序,最新的在前面
const sortedFiles = [...this.serviceFiles].sort((a, b) =>
new Date(b.mtime) - new Date(a.mtime)
);
// 添加文件到选择器
sortedFiles.forEach(file => {
const option = document.createElement('option');
option.value = file.path;
option.textContent = file.name;
fileSelector.appendChild(option);
});
}
async loadFileContent(filePath) {
try {
const response = await fetch(`/api/service-file-content?path=${encodeURIComponent(filePath)}`);
if (!response.ok) {
throw new Error(`服务器响应错误: ${response.status}`);
}
const content = await response.text();
this.selectedFile = filePath;
this.xmlContent = content;
// 解析XML内容
const parser = new DOMParser();
let xmlDoc;
try {
xmlDoc = parser.parseFromString(content, 'application/xml');
// 检查解析错误
const parseError = xmlDoc.querySelector('parsererror');
if (parseError) {
throw new Error('XML解析错误');
}
this.xmlDoc = xmlDoc;
// 特殊处理CommandList元素确保命令以属性方式存储
this.ensureCommandListFormat(xmlDoc);
} catch (parseError) {
console.error('XML解析错误:', parseError);
// 如果内容为空或解析失败创建一个基本的XML文档
if (!content.trim()) {
const basicXml = `<?xml version="1.0" encoding="UTF-8"?>
<Service>
<Properties>
<!-- 服务属性 -->
</Properties>
<Params>
<!-- 服务参数 -->
</Params>
</Service>`;
xmlDoc = parser.parseFromString(basicXml, 'application/xml');
this.xmlDoc = xmlDoc;
this.xmlContent = basicXml;
} else {
// 显示错误信息
const configContent = this.shadowRoot.querySelector('.config-content');
configContent.innerHTML = `
<div class="error-message">
<h3>XML解析错误</h3>
<p>文件内容不是有效的XML格式。</p>
<pre>${this.escapeHtml(content)}</pre>
</div>
`;
return;
}
}
this.updateFileContent();
this.resetEditState();
} catch (error) {
console.error('加载文件内容失败:', error);
const configContent = this.shadowRoot.querySelector('.config-content');
configContent.innerHTML = `
<div class="error-message">
<h3>加载失败</h3>
<p>${error.message}</p>
</div>
`;
}
}
updateFileContent() {
const contentArea = this.shadowRoot.querySelector('#contentArea');
if (!this.xmlDoc || !this.selectedFile) {
contentArea.innerHTML = `<div class="no-file-selected">请选择一个服务配置文件查看内容</div>`;
return;
}
contentArea.innerHTML = `
<div class="editor-container">
<div class="editor-panel active" id="visualEditor"></div>
</div>
`;
const visualEditor = this.shadowRoot.querySelector('#visualEditor');
this.renderVisualEditor(visualEditor, this.xmlDoc);
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
height: 100%;
overflow: auto;
padding: 16px;
box-sizing: border-box;
}
.config-container {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 16px;
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.config-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #e0e0e0;
}
.file-selector-header {
flex-grow: 1;
margin-right: 16px;
display: flex;
align-items: center;
}
.file-selector-label {
font-size: 18px;
font-weight: bold;
color: #333;
margin-right: 10px;
white-space: nowrap;
}
.file-selector-header select {
flex-grow: 1;
padding: 6px;
border-radius: 4px;
border: 1px solid #e0e0e0;
appearance: none;
padding-right: 30px;
background-image: url('assets/icons/png/chevron-down_b.png');
background-repeat: no-repeat;
background-position: right 8px center;
background-size: 14px;
}
.refresh-button {
width: 28px;
height: 28px;
cursor: pointer;
margin-left: 8px;
border: none;
background-color: transparent;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
background-image: url('assets/icons/png/refresh_b.png');
transition: transform 0.3s ease;
opacity: 0.7;
}
.refresh-button:hover {
opacity: 1;
}
.refresh-button.refreshing {
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.action-buttons {
display: flex;
gap: 8px;
}
.action-button {
background-color: #7986E7;
color: white;
border: none;
border-radius: 4px;
padding: 6px 12px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
}
.action-button.save-button.modified {
background-color: #E77979;
}
.action-button.save-button.modified:hover {
background-color: #D66868;
}
.action-button:hover {
background-color: #6875D6;
}
.action-button.secondary {
background-color: #f0f0f0;
color: #333;
}
.action-button.secondary:hover {
background-color: #e0e0e0;
}
.config-content {
padding: 0;
flex-grow: 1;
display: flex;
flex-direction: column;
overflow: auto;
}
.editor-container {
height: 100%;
display: flex;
flex-direction: column;
}
.editor-panel {
flex: 1;
height: 100%;
overflow: auto;
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 12px;
background-color: #f9f9f9;
}
.editor-panel.active {
display: block;
}
.no-file-selected {
color: #888;
font-style: italic;
text-align: center;
margin-top: 80px;
}
.error-message {
color: #d32f2f;
background-color: #ffebee;
padding: 10px;
border-radius: 4px;
margin-bottom: 15px;
}
.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;
}
</style>
<div class="config-container">
<div class="config-header">
<div class="file-selector-header">
<div class="file-selector-label">服务配置文件:</div>
<select id="fileSelector">
<option value="">-- 请选择服务配置文件 --</option>
</select>
<button class="refresh-button" id="refreshFiles" title="刷新文件列表"></button>
</div>
<div class="action-buttons">
<button class="action-button" id="newConfig">新建</button>
<button class="action-button save-button" id="saveConfig">保存</button>
<button class="action-button" id="saveAsConfig">另存为</button>
</div>
</div>
<div class="config-content">
<div id="contentArea">
<div class="no-file-selected">请选择一个服务配置文件查看内容</div>
</div>
</div>
</div>
`;
}
setupEventListeners() {
// 文件选择器
const fileSelector = this.shadowRoot.getElementById('fileSelector');
fileSelector.addEventListener('change', (e) => {
const filePath = e.target.value;
if (filePath) {
this.loadFileContent(filePath);
}
});
// 刷新文件列表
const refreshButton = this.shadowRoot.getElementById('refreshFiles');
refreshButton.addEventListener('click', () => {
refreshButton.classList.add('refreshing');
this.loadServiceFiles().finally(() => {
setTimeout(() => {
refreshButton.classList.remove('refreshing');
}, 500);
});
});
// 新建配置
const newButton = this.shadowRoot.getElementById('newConfig');
newButton.addEventListener('click', () => {
this.showNewConfigDialog();
});
// 保存配置
const saveButton = this.shadowRoot.getElementById('saveConfig');
saveButton.addEventListener('click', async () => {
if (!this.selectedFile) {
alert('请先选择一个文件或创建新文件');
return;
}
try {
// 确保获取当前可视化编辑器的所有值更新到XML文档
this.updateXmlFromVisualEditor();
// 保存文件
await this.saveFileContent(this.selectedFile, this.xmlContent);
this.resetEditState();
// 更新保存按钮状态
saveButton.classList.remove('modified');
alert('保存成功');
} catch (error) {
console.error('保存出错:', error);
alert('保存失败: ' + error.message);
}
});
// 另存为
const saveAsButton = this.shadowRoot.getElementById('saveAsConfig');
saveAsButton.addEventListener('click', () => {
if (!this.xmlContent) {
alert('没有内容可保存');
return;
}
this.showSaveAsDialog();
});
}
// 标记编辑状态
markEdited() {
this.isEdited = true;
// 更新保存按钮样式
const saveButton = this.shadowRoot.querySelector('.save-button');
if (saveButton) {
saveButton.classList.add('modified');
saveButton.title = '文件已修改,请保存';
}
}
// 重置编辑状态
resetEditState() {
this.isEdited = false;
// 更新保存按钮样式
const saveButton = this.shadowRoot.querySelector('.save-button');
if (saveButton) {
saveButton.classList.remove('modified');
saveButton.title = '保存';
}
}
// HTML转义
escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// 确保CommandList元素符合特定格式
ensureCommandListFormat(xmlDoc) {
// 如果没有rootElement返回null
if (!xmlDoc) return null;
const rootElement = xmlDoc.documentElement;
// 查找或创建CommandList元素
let commandListElement = rootElement.querySelector('CommandList');
if (!commandListElement) {
commandListElement = xmlDoc.createElement('CommandList');
rootElement.appendChild(commandListElement);
}
return commandListElement;
}
// 更新XML
updateXmlFromVisualEditor() {
if (!this.xmlDoc) return;
// 处理基本信息
const rootElement = this.xmlDoc.documentElement;
const visualEditor = this.shadowRoot.querySelector('#visualEditor');
if (visualEditor) {
// 获取输入字段的值
const nameInput = visualEditor.querySelector('#serviceName');
const descInput = visualEditor.querySelector('#serviceDesc');
const authorInput = visualEditor.querySelector('#serviceAuthor');
const versionInput = visualEditor.querySelector('#serviceVersion');
const createTimeInput = visualEditor.querySelector('#serviceCreateTime');
const changeTimeInput = visualEditor.querySelector('#serviceChangeTime');
// 处理基本信息元素
this.updateElementIfExists(rootElement, 'Name', nameInput ? nameInput.value : '');
this.updateElementIfExists(rootElement, 'Description', descInput ? descInput.value : '');
this.updateElementIfExists(rootElement, 'Author', authorInput ? authorInput.value : '');
this.updateElementIfExists(rootElement, 'Version', versionInput ? versionInput.value : '1.0.0');
// 处理CreateTime元素
this.updateElementIfExists(rootElement, 'CreateTime', createTimeInput ? createTimeInput.value : '');
// 处理ChangeTime元素 - 总是设置为当前时间
const now = new Date();
const dateStr = now.toISOString().split('T')[0];
const timeStr = now.toTimeString().split(' ')[0];
const datetimeStr = `${dateStr} ${timeStr}`;
this.updateElementIfExists(rootElement, 'ChangeTime', datetimeStr);
// 更新界面上的修改时间
if (changeTimeInput) {
changeTimeInput.value = datetimeStr;
}
// 确保CommandList元素存在
this.ensureCommandListFormat(this.xmlDoc);
// 更新其他元素
visualEditor.querySelectorAll('.element-input-row input').forEach(input => {
if (input.dataset.elementPath) {
const parts = input.dataset.elementPath.split('.');
let current = this.xmlDoc;
let lastElement = null;
let lastPart = '';
// 遍历路径找到或创建元素
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (!current) break;
if (i === parts.length - 1) {
lastPart = part;
lastElement = current;
current = current.querySelector(part);
if (!current && lastElement) {
current = this.xmlDoc.createElement(part);
lastElement.appendChild(current);
}
} else {
current = current.querySelector(part);
}
}
if (current) {
current.textContent = input.value;
}
}
});
}
// 重新序列化XML内容
const serializer = new XMLSerializer();
this.xmlContent = this.formatXml(serializer.serializeToString(this.xmlDoc));
this.markEdited();
}
// 更新元素,如果不存在则创建
updateElementIfExists(parentElement, elementName, value) {
if (!parentElement) return;
let element = parentElement.querySelector(elementName);
if (!element) {
element = this.xmlDoc.createElement(elementName);
parentElement.appendChild(element);
}
element.textContent = value;
return element;
}
// 格式化XML
formatXml(xml) {
// 定义缩进层级
const indent = (level) => {
// 确保level不为负数
const safeLevel = Math.max(0, level);
return ' '.repeat(safeLevel);
};
// 使用正则表达式格式化XML
let formatted = '';
let level = 0;
// 使用更安全的方式分割和处理XML
// 不使用正则表达式,避免可能破坏元素内容
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xml, 'application/xml');
// 使用XMLSerializer获取格式化的XML
const serializer = new XMLSerializer();
const prettyXml = this.formatNode(xmlDoc, 0);
return prettyXml;
}
// 递归处理XML节点
formatNode(node, level) {
if (!node) return '';
let result = '';
const indentStr = ' '.repeat(level);
// 处理不同类型的节点
switch (node.nodeType) {
case Node.DOCUMENT_NODE:
// 文档节点,处理子节点
result = '<?xml version="1.0" encoding="UTF-8"?>\n';
for (let i = 0; i < node.childNodes.length; i++) {
result += this.formatNode(node.childNodes[i], level);
}
break;
case Node.ELEMENT_NODE:
// 元素节点
result += indentStr + '<' + node.nodeName;
// 处理属性
for (let i = 0; i < node.attributes.length; i++) {
const attr = node.attributes[i];
result += ' ' + attr.name + '="' + attr.value + '"';
}
if (node.childNodes.length === 0) {
// 没有子节点,使用自闭合标签
result += ' />\n';
} else {
result += '>';
let hasElement = false;
let textContent = '';
// 检查是否只有文本内容
for (let i = 0; i < node.childNodes.length; i++) {
if (node.childNodes[i].nodeType === Node.ELEMENT_NODE) {
hasElement = true;
break;
} else if (node.childNodes[i].nodeType === Node.TEXT_NODE) {
textContent += node.childNodes[i].nodeValue;
}
}
if (hasElement) {
// 有元素子节点,换行并增加缩进
result += '\n';
for (let i = 0; i < node.childNodes.length; i++) {
result += this.formatNode(node.childNodes[i], level + 1);
}
result += indentStr;
} else {
// 只有文本内容,不换行
result += textContent.trim();
}
result += '</' + node.nodeName + '>\n';
}
break;
case Node.TEXT_NODE:
// 文本节点,去除空白文本节点
const text = node.nodeValue.trim();
if (text) {
result += text;
}
break;
case Node.COMMENT_NODE:
// 注释节点
result += indentStr + '<!--' + node.nodeValue + '-->\n';
break;
default:
// 其他类型节点
break;
}
return result;
}
// 文件保存功能
async saveFileContent(filePath, content) {
try {
const response = await fetch('/api/save-service-file', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
path: filePath,
content: content
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `保存失败: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('保存文件失败:', error);
throw error;
}
}
// 创建新配置
async createNewConfig(fileName) {
try {
const response = await fetch('/api/create-service-file', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ fileName })
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `创建失败: ${response.status}`);
}
const result = await response.json();
// 重新加载文件列表
await this.loadServiceFiles();
// 加载新创建的文件
await this.loadFileContent(result.path);
return result;
} catch (error) {
console.error('创建新配置文件失败:', error);
throw error;
}
}
// 显示新建配置对话框
showNewConfigDialog() {
// 创建模态框
const modal = document.createElement('div');
modal.className = 'modal';
modal.id = 'newConfigModal';
modal.innerHTML = `
<style>
.modal {
display: block;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.4);
}
.modal-content {
background-color: #fefefe;
margin: 15% auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
max-width: 500px;
border-radius: 5px;
position: relative;
}
.modal-title {
margin-top: 0;
color: #333;
}
.close {
color: #aaa;
position: absolute;
right: 20px;
top: 10px;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close:hover {
color: #000;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input {
width: 100%;
padding: 8px;
box-sizing: border-box;
border: 1px solid #ddd;
border-radius: 4px;
}
.form-footer {
text-align: right;
margin-top: 20px;
}
.btn {
padding: 8px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-left: 10px;
}
.btn-cancel {
background-color: #f5f5f5;
color: #333;
}
.btn-confirm {
background-color: #7986E7;
color: white;
}
</style>
<div class="modal-content">
<span class="close">&times;</span>
<h3 class="modal-title">新建服务配置文件</h3>
<div class="form-group">
<label for="newFileName">文件名</label>
<input type="text" id="newFileName" placeholder="请输入文件名" />
</div>
<div class="form-footer">
<button class="btn btn-cancel" id="cancelBtn">取消</button>
<button class="btn btn-confirm" id="confirmBtn">创建</button>
</div>
</div>
`;
document.body.appendChild(modal);
const closeBtn = modal.querySelector('.close');
const cancelBtn = modal.querySelector('#cancelBtn');
const confirmBtn = modal.querySelector('#confirmBtn');
const fileNameInput = modal.querySelector('#newFileName');
const closeModal = () => {
document.body.removeChild(modal);
};
closeBtn.addEventListener('click', closeModal);
cancelBtn.addEventListener('click', closeModal);
confirmBtn.addEventListener('click', async () => {
let fileName = fileNameInput.value.trim();
if (!fileName) {
alert('请输入文件名');
return;
}
// 确保文件名以.scfg结尾
if (!fileName.toLowerCase().endsWith('.scfg')) {
fileName += '.scfg';
}
try {
await this.createNewConfig(fileName);
closeModal();
} catch (error) {
alert('创建失败: ' + error.message);
}
});
// 处理回车键
fileNameInput.addEventListener('keyup', (e) => {
if (e.key === 'Enter') {
confirmBtn.click();
}
});
// 打开后聚焦输入框
setTimeout(() => {
fileNameInput.focus();
}, 100);
}
// 显示另存为对话框
showSaveAsDialog() {
// 创建模态框
const modal = document.createElement('div');
modal.className = 'modal';
modal.id = 'saveAsModal';
modal.innerHTML = `
<style>
.modal {
display: block;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.4);
}
.modal-content {
background-color: #fefefe;
margin: 15% auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
max-width: 500px;
border-radius: 5px;
position: relative;
}
.modal-title {
margin-top: 0;
color: #333;
}
.close {
color: #aaa;
position: absolute;
right: 20px;
top: 10px;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close:hover {
color: #000;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input {
width: 100%;
padding: 8px;
box-sizing: border-box;
border: 1px solid #ddd;
border-radius: 4px;
}
.form-footer {
text-align: right;
margin-top: 20px;
}
.btn {
padding: 8px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-left: 10px;
}
.btn-cancel {
background-color: #f5f5f5;
color: #333;
}
.btn-confirm {
background-color: #7986E7;
color: white;
}
</style>
<div class="modal-content">
<span class="close">&times;</span>
<h3 class="modal-title">另存为新文件</h3>
<div class="form-group">
<label for="saveAsFileName">文件名</label>
<input type="text" id="saveAsFileName" placeholder="请输入文件名" />
</div>
<div class="form-footer">
<button class="btn btn-cancel" id="cancelBtn">取消</button>
<button class="btn btn-confirm" id="confirmBtn">保存</button>
</div>
</div>
`;
document.body.appendChild(modal);
const closeBtn = modal.querySelector('.close');
const cancelBtn = modal.querySelector('#cancelBtn');
const confirmBtn = modal.querySelector('#confirmBtn');
const fileNameInput = modal.querySelector('#saveAsFileName');
// 填充当前文件名
if (this.selectedFile) {
const fileName = this.selectedFile.split('/').pop();
fileNameInput.value = fileName;
}
const closeModal = () => {
document.body.removeChild(modal);
};
closeBtn.addEventListener('click', closeModal);
cancelBtn.addEventListener('click', closeModal);
confirmBtn.addEventListener('click', async () => {
let fileName = fileNameInput.value.trim();
if (!fileName) {
alert('请输入文件名');
return;
}
// 确保文件名以.scfg结尾
if (!fileName.toLowerCase().endsWith('.scfg')) {
fileName += '.scfg';
}
try {
const response = await fetch('/api/save-service-as', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
fileName,
content: this.xmlContent,
currentFile: this.selectedFile
})
});
if (!response.ok) {
const errorData = await response.json();
if (errorData.code === 'FILE_EXISTS') {
if (confirm('文件已存在,是否覆盖?')) {
// 尝试带覆盖标志重新保存
const retryResponse = await fetch('/api/save-service-as', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
fileName,
content: this.xmlContent,
currentFile: this.selectedFile,
overwrite: true
})
});
if (!retryResponse.ok) {
const retryErrorData = await retryResponse.json();
throw new Error(retryErrorData.error || `保存失败: ${retryResponse.status}`);
}
const result = await retryResponse.json();
this.selectedFile = result.path;
this.resetEditState();
await this.loadServiceFiles();
closeModal();
}
return;
}
throw new Error(errorData.error || `保存失败: ${response.status}`);
}
const result = await response.json();
this.selectedFile = result.path;
this.resetEditState();
await this.loadServiceFiles();
closeModal();
} catch (error) {
alert('保存失败: ' + error.message);
}
});
// 处理回车键
fileNameInput.addEventListener('keyup', (e) => {
if (e.key === 'Enter') {
confirmBtn.click();
}
});
// 打开后聚焦输入框
setTimeout(() => {
fileNameInput.focus();
fileNameInput.select(); // 全选文本,方便修改
}, 100);
}
// 可视化编辑器渲染
renderVisualEditor(container, xmlDoc) {
if (!xmlDoc || !container) return;
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') {
// 创建基本信息部分
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')) {
const now = new Date();
createTimeElement.textContent = `${now.toISOString().split('T')[0]} ${now.toTimeString().split(' ')[0]}`;
rootElement.appendChild(createTimeElement);
}
if (!rootElement.querySelector('ChangeTime')) {
const now = new Date();
changeTimeElement.textContent = `${now.toISOString().split('T')[0]} ${now.toTimeString().split(' ')[0]}`;
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="${this.escapeHtml(nameElement.textContent || '')}" />
</div>
<div class="form-group">
<label for="serviceDesc">服务描述</label>
<input type="text" id="serviceDesc" class="form-control" placeholder="请输入服务描述" value="${this.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="${this.escapeHtml(authorElement.textContent || '')}" />
</div>
<div class="form-group">
<label for="serviceVersion">版本</label>
<input type="text" id="serviceVersion" class="form-control" placeholder="请输入版本号" value="${this.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="${this.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="${this.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;
// 直接更新XML内容和状态
const serializer = new XMLSerializer();
this.xmlContent = this.formatXml(serializer.serializeToString(this.xmlDoc));
this.markEdited();
} else if (e.target.id === 'serviceDesc') {
descElement.textContent = e.target.value;
// 直接更新XML内容和状态
const serializer = new XMLSerializer();
this.xmlContent = this.formatXml(serializer.serializeToString(this.xmlDoc));
this.markEdited();
} else if (e.target.id === 'serviceAuthor') {
authorElement.textContent = e.target.value;
// 直接更新XML内容和状态
const serializer = new XMLSerializer();
this.xmlContent = this.formatXml(serializer.serializeToString(this.xmlDoc));
this.markEdited();
} else if (e.target.id === 'serviceVersion') {
versionElement.textContent = e.target.value;
// 直接更新XML内容和状态
const serializer = new XMLSerializer();
this.xmlContent = this.formatXml(serializer.serializeToString(this.xmlDoc));
this.markEdited();
} else if (e.target.id === 'serviceCreateTime') {
createTimeElement.textContent = e.target.value;
// 直接更新XML内容和状态
const serializer = new XMLSerializer();
this.xmlContent = this.formatXml(serializer.serializeToString(this.xmlDoc));
this.markEdited();
} else if (e.target.id === 'serviceChangeTime') {
changeTimeElement.textContent = e.target.value;
// 直接更新XML内容和状态
const serializer = new XMLSerializer();
this.xmlContent = this.formatXml(serializer.serializeToString(this.xmlDoc));
this.markEdited();
}
});
// 添加日期时间选择器事件
basicInfoSection.querySelector('#createTimeBtn').addEventListener('click', () => {
const input = basicInfoSection.querySelector('#serviceCreateTime');
this.showDateTimeDialog(input);
});
basicInfoSection.querySelector('#changeTimeBtn').addEventListener('click', () => {
const input = basicInfoSection.querySelector('#serviceChangeTime');
this.showDateTimeDialog(input);
});
// 添加刷新按钮事件
basicInfoSection.querySelector('#refreshCreateTimeBtn').addEventListener('click', () => {
const input = basicInfoSection.querySelector('#serviceCreateTime');
const now = new Date();
const dateStr = now.toISOString().split('T')[0];
const timeStr = now.toTimeString().split(' ')[0];
const datetimeStr = `${dateStr} ${timeStr}`;
input.value = datetimeStr;
createTimeElement.textContent = datetimeStr;
// 直接更新XML内容和状态
const serializer = new XMLSerializer();
this.xmlContent = this.formatXml(serializer.serializeToString(this.xmlDoc));
this.markEdited();
});
basicInfoSection.querySelector('#refreshChangeTimeBtn').addEventListener('click', () => {
const input = basicInfoSection.querySelector('#serviceChangeTime');
const now = new Date();
const dateStr = now.toISOString().split('T')[0];
const timeStr = now.toTimeString().split(' ')[0];
const datetimeStr = `${dateStr} ${timeStr}`;
input.value = datetimeStr;
changeTimeElement.textContent = datetimeStr;
// 直接更新XML内容和状态
const serializer = new XMLSerializer();
this.xmlContent = this.formatXml(serializer.serializeToString(this.xmlDoc));
this.markEdited();
});
// 添加到可视化编辑器
visualEditor.appendChild(basicInfoSection);
// 创建命令列表部分
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 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>${this.escapeHtml(name)}</td>
<td>${this.escapeHtml(call)}</td>
<td>${this.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];
this.showEditCommandDialog(command, index);
});
});
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);
// 直接更新XML内容和状态
const serializer = new XMLSerializer();
const xmlString = serializer.serializeToString(this.xmlDoc);
this.xmlContent = this.formatXml(xmlString);
this.markEdited();
// 移除表格行
const row = commandsTableBody.querySelector(`tr[data-index="${index}"]`);
if (row) {
row.remove();
// 更新其余行的索引
commandsTableBody.querySelectorAll('tr').forEach((row, newIndex) => {
row.dataset.index = newIndex;
row.querySelectorAll('button').forEach(button => {
button.dataset.index = newIndex;
});
});
// 如果没有命令了,添加空行提示
if (commandsTableBody.children.length === 0) {
const emptyRow = document.createElement('tr');
emptyRow.innerHTML = '<td colspan="4" style="text-align: center;">暂无指令</td>';
commandsTableBody.appendChild(emptyRow);
}
}
}
});
});
}
// 添加"添加指令"按钮事件
commandsSection.querySelector('#addCommandBtn').addEventListener('click', () => {
this.showAddCommandDialog(commandListElement);
});
// 添加表格
commandsSection.appendChild(commandsTable);
// 添加到可视化编辑器
visualEditor.appendChild(commandsSection);
// 创建其他设置部分包括UDP
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);
// 添加额外的CSS样式
const otherSettingsStyle = document.createElement('style');
otherSettingsStyle.textContent = `
.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;
}
`;
otherSettingsSection.appendChild(otherSettingsStyle);
// 创建其他设置内容
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);
// 直接更新XML内容和状态
const serializer = new XMLSerializer();
const xmlString = serializer.serializeToString(this.xmlDoc);
this.xmlContent = this.formatXml(xmlString);
this.markEdited();
// 移除元素框
if (elementBox.parentNode) {
elementBox.parentNode.removeChild(elementBox);
}
// 如果没有其他元素了,显示"没有其他设置项"提示
const otherSettingsContainer = this.shadowRoot.querySelector('.other-settings-container');
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}`);
// 出错时重新渲染整个编辑器以恢复状态
this.renderVisualEditor(container, this.xmlDoc);
}
}
});
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;
// 直接更新XML内容和状态
const serializer = new XMLSerializer();
this.xmlContent = this.formatXml(serializer.serializeToString(this.xmlDoc));
this.markEdited();
});
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);
// 直接更新XML内容和状态
const serializer = new XMLSerializer();
const xmlString = serializer.serializeToString(this.xmlDoc);
this.xmlContent = this.formatXml(xmlString);
this.markEdited();
// 移除元素框
if (elementBox.parentNode) {
elementBox.parentNode.removeChild(elementBox);
}
// 如果没有其他元素了,显示"没有其他设置项"提示
const otherSettingsContainer = this.shadowRoot.querySelector('.other-settings-container');
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}`);
// 出错时重新渲染整个编辑器以恢复状态
this.renderVisualEditor(container, this.xmlDoc);
}
}
});
inputRow.appendChild(deleteBtn);
elementContent.appendChild(inputRow);
}
// 将元素内容添加到元素框
elementBox.appendChild(elementContent);
// 将元素框添加到容器
containerElement.appendChild(elementBox);
});
};
// 获取元素的路径,用于唯一标识
const getElementPath = (element) => {
const path = [];
let current = element;
while (current && current !== this.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);
// 添加到可视化编辑器
visualEditor.appendChild(otherSettingsSection);
} else {
// 不是Service根元素显示错误信息
visualEditor.innerHTML = `<div class="error-message">
无法编辑XML文档的根元素不是Service。
请确保XML文档的根元素是Service。
</div>`;
}
container.appendChild(style);
container.appendChild(visualEditor);
}
// 显示日期时间选择对话框
showDateTimeDialog(inputElement) {
// 解析当前日期和时间
let currentDate = new Date();
let currentTime = '00:00:00';
if (inputElement.value) {
try {
const parts = inputElement.value.split(' ');
if (parts.length >= 1) {
const dateParts = parts[0].split('-');
if (dateParts.length === 3) {
const year = parseInt(dateParts[0]);
const month = parseInt(dateParts[1]) - 1; // 月份从0开始
const day = parseInt(dateParts[2]);
currentDate = new Date(year, month, day);
}
}
if (parts.length >= 2) {
currentTime = parts[1];
}
} catch (error) {
console.error('解析日期时间失败:', error);
currentDate = new Date();
}
}
// 创建模态框
const modal = document.createElement('div');
modal.className = 'modal';
modal.id = 'dateTimeModal';
// 获取年、月、日
const year = currentDate.getFullYear();
const month = currentDate.getMonth(); // 0-11
const day = currentDate.getDate();
// 生成日历
const daysInMonth = new Date(year, month + 1, 0).getDate();
const firstDay = new Date(year, month, 1).getDay(); // 0-60表示周日
// 生成月份选项
let monthOptions = '';
const monthNames = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'];
monthNames.forEach((name, idx) => {
monthOptions += `<option value="${idx}" ${idx === month ? 'selected' : ''}>${name}</option>`;
});
// 生成年份选项
let yearOptions = '';
const currentYear = new Date().getFullYear();
for (let y = currentYear - 10; y <= currentYear + 10; y++) {
yearOptions += `<option value="${y}" ${y === year ? 'selected' : ''}>${y}</option>`;
}
// 生成日历表格
let calendarRows = '';
let dayCount = 1;
// 添加表头
calendarRows += '<tr>';
['日', '一', '二', '三', '四', '五', '六'].forEach(dayName => {
calendarRows += `<th>${dayName}</th>`;
});
calendarRows += '</tr>';
// 计算行数
const totalCells = firstDay + daysInMonth;
const rowCount = Math.ceil(totalCells / 7);
// 添加日期行
for (let i = 0; i < rowCount; i++) {
calendarRows += '<tr>';
for (let j = 0; j < 7; j++) {
if ((i === 0 && j < firstDay) || dayCount > daysInMonth) {
calendarRows += '<td></td>';
} else {
const isToday = dayCount === day;
calendarRows += `<td class="calendar-day ${isToday ? 'selected' : ''}" data-day="${dayCount}">${dayCount}</td>`;
dayCount++;
}
}
calendarRows += '</tr>';
}
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<div class="modal-title">选择日期和时间</div>
<span class="close">&times;</span>
</div>
<div class="modal-body">
<div class="calendar-container">
<div class="calendar-header">
<select id="calendarMonth" class="form-control" style="max-width: 120px;">${monthOptions}</select>
<select id="calendarYear" class="form-control" style="max-width: 100px;">${yearOptions}</select>
</div>
<table class="calendar-table">
<thead>
${calendarRows}
</thead>
</table>
</div>
<div class="time-container" style="margin-top: 15px;">
<label for="timeInput">时间:</label>
<input type="time" id="timeInput" class="form-control" step="1" value="${currentTime}">
</div>
</div>
<div class="modal-footer">
<button id="cancelDateTime" class="action-button secondary">取消</button>
<button id="confirmDateTime" class="action-button">确定</button>
</div>
</div>
`;
// 添加样式
const style = document.createElement('style');
style.textContent = `
.modal {
display: block;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.4);
}
.modal-content {
background-color: #fefefe;
margin: 10% auto;
padding: 20px;
border: 1px solid #888;
width: 50%;
max-width: 500px;
border-radius: 5px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.modal-title {
font-weight: bold;
font-size: 18px;
}
.close {
color: #aaa;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close:hover {
color: black;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 15px;
}
.calendar-container {
width: 100%;
}
.calendar-header {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.calendar-header select {
padding: 5px;
border: 1px solid #ddd;
border-radius: 4px;
}
.calendar-table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
.calendar-table th,
.calendar-table td {
padding: 8px;
text-align: center;
border: 1px solid #ddd;
}
.calendar-day {
cursor: pointer;
}
.calendar-day:hover {
background-color: #f0f0f0;
}
.calendar-day.selected {
background-color: #7986E7;
color: white;
}
.form-control {
width: 100%;
padding: 8px;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 4px;
}
.action-button {
background-color: #7986E7;
color: white;
border: none;
border-radius: 4px;
padding: 6px 12px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
}
.action-button:hover {
background-color: #6875D6;
}
.action-button.secondary {
background-color: #f0f0f0;
color: #333;
}
.action-button.secondary:hover {
background-color: #e0e0e0;
}
`;
// 添加到DOM
document.body.appendChild(style);
document.body.appendChild(modal);
// 事件处理
const closeBtn = modal.querySelector('.close');
const cancelBtn = modal.querySelector('#cancelDateTime');
const confirmBtn = modal.querySelector('#confirmDateTime');
const calendarMonth = modal.querySelector('#calendarMonth');
const calendarYear = modal.querySelector('#calendarYear');
const calendarDays = modal.querySelectorAll('.calendar-day');
const timeInput = modal.querySelector('#timeInput');
// 选择日期事件
calendarDays.forEach(cell => {
cell.addEventListener('click', (e) => {
// 移除所有选中状态
calendarDays.forEach(day => day.classList.remove('selected'));
// 添加新选中状态
e.target.classList.add('selected');
});
});
// 月份和年份变化时重新渲染日历
const updateCalendar = () => {
const selectedYear = parseInt(calendarYear.value);
const selectedMonth = parseInt(calendarMonth.value);
// 关闭当前对话框
closeModal();
// 更新日期参数并重新显示对话框
currentDate = new Date(selectedYear, selectedMonth, 1);
this.showDateTimeDialog(inputElement);
};
calendarMonth.addEventListener('change', updateCalendar);
calendarYear.addEventListener('change', updateCalendar);
const closeModal = () => {
if (this.shadowRoot.contains(modal)) {
this.shadowRoot.removeChild(modal);
}
};
closeBtn.addEventListener('click', closeModal);
cancelBtn.addEventListener('click', closeModal);
confirmBtn.addEventListener('click', () => {
// 获取选中的日期
const selectedDay = modal.querySelector('.calendar-day.selected');
if (!selectedDay) {
closeModal();
return;
}
const day = selectedDay.dataset.day;
const month = parseInt(calendarMonth.value) + 1; // 月份从0开始显示时+1
const year = calendarYear.value;
// 格式化日期
const formattedDate = `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
// 获取时间
const time = timeInput.value || '00:00:00';
// 更新输入框值
inputElement.value = `${formattedDate} ${time}`;
// 触发更改事件
const changeEvent = new Event('change', { bubbles: true });
inputElement.dispatchEvent(changeEvent);
closeModal();
});
// 点击模态窗口外部关闭
modal.addEventListener('click', (event) => {
if (event.target === modal) {
closeModal();
}
});
}
// 显示添加指令对话框
showAddCommandDialog(commandListElement) {
// 创建模态框
const modal = document.createElement('div');
modal.className = 'modal';
modal.id = 'addCommandModal';
modal.innerHTML = `
<style>
.modal {
display: block;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.4);
}
.modal-content {
background-color: #fefefe;
margin: 10% auto;
padding: 20px;
border: 1px solid #888;
width: 50%;
max-width: 500px;
border-radius: 5px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.modal-title {
font-weight: bold;
font-size: 18px;
}
.close {
color: #aaa;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close:hover {
color: black;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 15px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
}
.form-control {
width: 100%;
padding: 8px;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 4px;
}
.action-button {
background-color: #7986E7;
color: white;
border: none;
border-radius: 4px;
padding: 6px 12px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
}
.action-button:hover {
background-color: #6875D6;
}
.action-button.secondary {
background-color: #f0f0f0;
color: #333;
}
.action-button.secondary:hover {
background-color: #e0e0e0;
}
</style>
<div class="modal-content">
<div class="modal-header">
<div class="modal-title">添加指令</div>
<span class="close">&times;</span>
</div>
<div class="modal-body">
<div class="form-group">
<label for="commandName">指令名称</label>
<input type="text" id="commandName" class="form-control" placeholder="输入指令名称" />
</div>
<div class="form-group">
<label for="commandCall">指令调用</label>
<input type="text" id="commandCall" class="form-control" placeholder="输入指令调用" />
</div>
<div class="form-group">
<label for="commandDescription">指令描述</label>
<input type="text" id="commandDescription" class="form-control" placeholder="输入指令描述" />
</div>
</div>
<div class="modal-footer">
<button id="cancelAddCommand" class="action-button secondary">取消</button>
<button id="confirmAddCommand" class="action-button">添加</button>
</div>
</div>
`;
// 添加到 shadowRoot 而不是 document.body
this.shadowRoot.appendChild(modal);
// 事件处理
const closeBtn = modal.querySelector('.close');
const cancelBtn = modal.querySelector('#cancelAddCommand');
const confirmBtn = modal.querySelector('#confirmAddCommand');
const nameInput = modal.querySelector('#commandName');
const callInput = modal.querySelector('#commandCall');
const descInput = modal.querySelector('#commandDescription');
const closeModal = () => {
if (this.shadowRoot.contains(modal)) {
this.shadowRoot.removeChild(modal);
}
};
closeBtn.addEventListener('click', closeModal);
cancelBtn.addEventListener('click', closeModal);
confirmBtn.addEventListener('click', () => {
const name = nameInput.value.trim();
const call = callInput.value.trim();
const description = descInput.value.trim();
if (!name) {
alert('指令名称不能为空');
return;
}
try {
// 创建新命令元素
const newCommand = this.xmlDoc.createElement('Command');
newCommand.setAttribute('Name', name);
newCommand.setAttribute('Call', call);
newCommand.setAttribute('Description', description || `${name}描述`);
// 添加到命令列表
commandListElement.appendChild(newCommand);
// 直接更新XML内容和状态
const serializer = new XMLSerializer();
const xmlString = serializer.serializeToString(this.xmlDoc);
this.xmlContent = this.formatXml(xmlString);
this.markEdited();
// 重新渲染可视化编辑器
this.updateFileContent();
closeModal();
} catch (error) {
console.error('添加指令失败:', error);
alert('添加指令失败: ' + error.message);
}
});
// 点击模态窗口外部关闭
modal.addEventListener('click', (event) => {
if (event.target === modal) {
closeModal();
}
});
}
// 显示编辑指令对话框
showEditCommandDialog(commandElement, index) {
// 获取当前命令属性
const name = commandElement.getAttribute('Name') || '';
const call = commandElement.getAttribute('Call') || '';
const description = commandElement.getAttribute('Description') || '';
// 创建模态框
const modal = document.createElement('div');
modal.className = 'modal';
modal.id = 'editCommandModal';
modal.innerHTML = `
<style>
.modal {
display: block;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.4);
}
.modal-content {
background-color: #fefefe;
margin: 10% auto;
padding: 20px;
border: 1px solid #888;
width: 50%;
max-width: 500px;
border-radius: 5px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.modal-title {
font-weight: bold;
font-size: 18px;
}
.close {
color: #aaa;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close:hover {
color: black;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 15px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
}
.form-control {
width: 100%;
padding: 8px;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 4px;
}
.action-button {
background-color: #7986E7;
color: white;
border: none;
border-radius: 4px;
padding: 6px 12px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
}
.action-button:hover {
background-color: #6875D6;
}
.action-button.secondary {
background-color: #f0f0f0;
color: #333;
}
.action-button.secondary:hover {
background-color: #e0e0e0;
}
</style>
<div class="modal-content">
<div class="modal-header">
<div class="modal-title">编辑指令</div>
<span class="close">&times;</span>
</div>
<div class="modal-body">
<div class="form-group">
<label for="commandName">指令名称</label>
<input type="text" id="commandName" class="form-control" value="${this.escapeHtml(name)}" placeholder="输入指令名称" />
</div>
<div class="form-group">
<label for="commandCall">指令调用</label>
<input type="text" id="commandCall" class="form-control" value="${this.escapeHtml(call)}" placeholder="输入指令调用" />
</div>
<div class="form-group">
<label for="commandDescription">指令描述</label>
<input type="text" id="commandDescription" class="form-control" value="${this.escapeHtml(description)}" placeholder="输入指令描述" />
</div>
</div>
<div class="modal-footer">
<button id="cancelEditCommand" class="action-button secondary">取消</button>
<button id="confirmEditCommand" class="action-button">保存</button>
</div>
</div>
`;
// 添加到 shadowRoot 而不是 document.body
this.shadowRoot.appendChild(modal);
// 事件处理
const closeBtn = modal.querySelector('.close');
const cancelBtn = modal.querySelector('#cancelEditCommand');
const confirmBtn = modal.querySelector('#confirmEditCommand');
const nameInput = modal.querySelector('#commandName');
const callInput = modal.querySelector('#commandCall');
const descInput = modal.querySelector('#commandDescription');
const closeModal = () => {
if (this.shadowRoot.contains(modal)) {
this.shadowRoot.removeChild(modal);
}
};
closeBtn.addEventListener('click', closeModal);
cancelBtn.addEventListener('click', closeModal);
confirmBtn.addEventListener('click', () => {
const newName = nameInput.value.trim();
const newCall = callInput.value.trim();
const newDescription = descInput.value.trim();
if (!newName) {
alert('指令名称不能为空');
return;
}
try {
// 更新命令属性
commandElement.setAttribute('Name', newName);
commandElement.setAttribute('Call', newCall);
commandElement.setAttribute('Description', newDescription || `${newName}描述`);
// 直接更新XML内容和状态
const serializer = new XMLSerializer();
const xmlString = serializer.serializeToString(this.xmlDoc);
this.xmlContent = this.formatXml(xmlString);
this.markEdited();
// 重新渲染可视化编辑器
this.updateFileContent();
closeModal();
} catch (error) {
console.error('编辑指令失败:', error);
alert('编辑指令失败: ' + error.message);
}
});
// 点击模态窗口外部关闭
modal.addEventListener('click', (event) => {
if (event.target === modal) {
closeModal();
}
});
}
}
customElements.define('service-config', ServiceConfig);