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 = `
`;
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 = '';
// 按修改时间排序,最新的在前面
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 = `
`;
xmlDoc = parser.parseFromString(basicXml, 'application/xml');
this.xmlDoc = xmlDoc;
this.xmlContent = basicXml;
} else {
// 显示错误信息
const configContent = this.shadowRoot.querySelector('.config-content');
configContent.innerHTML = `
XML解析错误
文件内容不是有效的XML格式。
${this.escapeHtml(content)}
`;
return;
}
}
this.updateFileContent();
this.resetEditState();
} catch (error) {
console.error('加载文件内容失败:', error);
const configContent = this.shadowRoot.querySelector('.config-content');
configContent.innerHTML = `
`;
}
}
updateFileContent() {
const contentArea = this.shadowRoot.querySelector('#contentArea');
if (!this.xmlDoc || !this.selectedFile) {
contentArea.innerHTML = `请选择一个服务配置文件查看内容
`;
return;
}
contentArea.innerHTML = `
`;
const visualEditor = this.shadowRoot.querySelector('#visualEditor');
this.renderVisualEditor(visualEditor, this.xmlDoc);
}
render() {
this.shadowRoot.innerHTML = `
`;
}
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, "'");
}
// 确保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 = '\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 + '\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 = `
`;
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 = `
`;
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 = '基本信息
';
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 = `
`;
basicInfoForm.appendChild(nameDescContainer);
// 其他信息以两列布局
const otherInfoContainer = document.createElement('div');
otherInfoContainer.className = 'two-column-form';
otherInfoContainer.style.marginTop = '15px';
otherInfoContainer.innerHTML = `
`;
basicInfoForm.appendChild(otherInfoContainer);
basicInfoSection.appendChild(basicInfoForm);
// 添加事件监听
basicInfoSection.addEventListener('change', (e) => {
if (e.target.id === 'serviceName') {
nameElement.textContent = e.target.value;
// 直接更新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 = `
指令列表
`;
commandsSection.appendChild(commandsHeader);
// 获取或创建命令列表
let commandListElement = rootElement.querySelector('CommandList');
if (!commandListElement) {
commandListElement = xmlDoc.createElement('CommandList');
rootElement.appendChild(commandListElement);
}
// 创建命令表格
const commandsTable = document.createElement('table');
commandsTable.className = 'command-table';
commandsTable.innerHTML = `
名称 |
调用 |
描述 |
操作 |
`;
// 创建表格内容
const commandsTableBody = commandsTable.querySelector('#commandsTableBody');
// 获取所有命令
const commandElements = commandListElement.querySelectorAll('Command');
if (commandElements.length === 0) {
// 如果没有命令,显示空行
const emptyRow = document.createElement('tr');
emptyRow.innerHTML = '暂无指令 | ';
commandsTableBody.appendChild(emptyRow);
} else {
// 添加所有命令到表格
commandElements.forEach((command, index) => {
const name = command.getAttribute('Name') || '';
const call = command.getAttribute('Call') || '';
const description = command.getAttribute('Description') || '';
const row = document.createElement('tr');
row.dataset.index = index;
row.innerHTML = `
${this.escapeHtml(name)} |
${this.escapeHtml(call)} |
${this.escapeHtml(description)} |
|
`;
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 = '暂无指令 | ';
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 = `
其他设置
如需添加参数,请手动编辑配置文件
`;
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 = `
无法编辑:XML文档的根元素不是Service。
请确保XML文档的根元素是Service。
`;
}
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 += ``;
});
// 生成年份选项
let yearOptions = '';
const currentYear = new Date().getFullYear();
for (let y = currentYear - 10; y <= currentYear + 10; y++) {
yearOptions += ``;
}
// 生成日历表格
let calendarRows = '';
let dayCount = 1;
// 添加表头
calendarRows += '';
['日', '一', '二', '三', '四', '五', '六'].forEach(dayName => {
calendarRows += `${dayName} | `;
});
calendarRows += '
';
// 计算行数
const totalCells = firstDay + daysInMonth;
const rowCount = Math.ceil(totalCells / 7);
// 添加日期行
for (let i = 0; i < rowCount; i++) {
calendarRows += '';
for (let j = 0; j < 7; j++) {
if ((i === 0 && j < firstDay) || dayCount > daysInMonth) {
calendarRows += ' | ';
} else {
const isToday = dayCount === day;
calendarRows += `${dayCount} | `;
dayCount++;
}
}
calendarRows += '
';
}
modal.innerHTML = `
`;
// 添加样式
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 = `
`;
// 添加到 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 = `
`;
// 添加到 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);