2808 lines
108 KiB
JavaScript
2808 lines
108 KiB
JavaScript
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, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """)
|
||
.replace(/'/g, "'");
|
||
}
|
||
|
||
// 确保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">×</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">×</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-6,0表示周日
|
||
|
||
// 生成月份选项
|
||
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">×</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">×</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">×</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);
|