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

2837 lines
106 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

class ModelConfig extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.selectedFile = null;
this.xmlContent = '';
this.xmlDoc = null;
this.isEdited = false;
this.modelFiles = [];
}
async connectedCallback() {
this.render();
await this.loadModelFiles();
this.setupEventListeners();
}
async loadModelFiles() {
try {
const response = await fetch('/api/model-files');
if (!response.ok) {
throw new Error(`服务器响应错误: ${response.status}`);
}
this.modelFiles = await response.json();
this.updateFileSelector();
} catch (error) {
console.error('加载模型文件失败:', error);
// 如果请求失败可能是API不存在等待一段时间后重试
const retryLoadFiles = async () => {
try {
console.log('尝试重新加载模型文件列表...');
const retryResponse = await fetch('/api/model-files');
if (retryResponse.ok) {
this.modelFiles = 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.modelFiles].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/model-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"?>
<Model>
<Properties>
<!-- 模型属性 -->
</Properties>
<Params>
<!-- 模型参数 -->
</Params>
</Model>`;
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;
}
.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;
}
.button-sm {
padding: 2px 8px;
font-size: 12px;
}
.modal {
display: none;
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;
}
.error-message {
color: #d32f2f;
background-color: #ffebee;
padding: 10px;
border-radius: 4px;
margin-bottom: 15px;
}
.no-file-selected {
color: #888;
font-style: italic;
text-align: center;
margin-top: 80px;
}
/* 命令表格相关样式 */
.command-table-container {
margin-top: 10px;
width: 100%;
overflow-x: auto;
}
.command-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.command-table th,
.command-table td {
padding: 8px;
text-align: left;
border: 1px solid #ddd;
}
.command-table th {
background-color: #f5f5f5;
font-weight: 600;
}
.command-table tr:nth-child(even) {
background-color: #f9f9f9;
}
.command-table tr:hover {
background-color: #f1f1f1;
}
/* 命令表格单元格和行样式 */
.command-cell {
padding: 8px;
text-align: left;
border: 1px solid #ddd;
vertical-align: middle;
word-break: break-word;
}
.command-row {
transition: background-color 0.2s;
}
.action-cell {
text-align: center;
white-space: nowrap;
}
.command-actions {
display: flex;
gap: 5px;
justify-content: center;
}
.command-action-button {
padding: 3px 8px;
font-size: 12px;
border: none;
border-radius: 3px;
cursor: pointer;
transition: background-color 0.2s;
color: white;
}
.edit-button {
background-color: #7986E7;
}
.edit-button:hover {
background-color: #6875D6;
}
.delete-button {
background-color: #E77979;
}
.delete-button:hover {
background-color: #D66868;
}
/* 输入容器样式 */
.input-container {
display: flex;
align-items: center;
}
/* 图标按钮样式 */
.icon-button {
background-color: transparent;
border: none;
cursor: pointer;
padding: 0;
width: 28px;
height: 28px;
margin-left: 8px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
opacity: 0.7;
transition: opacity 0.2s;
}
.icon-button:hover {
opacity: 1;
}
.calendar-button {
background-image: url('assets/icons/png/calendar_b.png');
width: 24px;
height: 24px;
background-size: 16px;
}
.refresh-button-sm {
background-image: url('assets/icons/png/refresh_b.png');
width: 24px;
height: 24px;
background-size: 16px;
}
.calendar-day.selected {
background-color: #7986E7;
color: white;
}
/* 其它设置部分样式 */
.other-settings-container {
margin-top: 15px;
}
.element-row {
display: flex;
margin-bottom: 8px;
align-items: flex-start;
}
.element-box {
display: flex;
align-items: center;
flex-wrap: wrap;
background-color: #f9f9f9;
border: 1px solid #ddd;
border-radius: 4px;
padding: 8px;
width: 100%;
}
.toggle-button {
background: none;
border: none;
cursor: pointer;
padding: 0;
margin-right: 5px;
font-size: 14px;
color: #666;
width: 20px;
display: flex;
justify-content: center;
}
.toggle-spacer {
width: 20px;
}
.element-name {
font-weight: 500;
margin-right: 15px;
padding: 5px 0;
color: #333;
min-width: 100px;
}
.element-value-container {
flex: 1;
display: flex;
}
.element-value-input {
flex: 1;
padding: 5px 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.element-actions {
display: flex;
gap: 5px;
margin-left: auto;
}
.element-action-button {
padding: 3px 8px;
font-size: 12px;
border: none;
border-radius: 3px;
background-color: #7986E7;
color: white;
cursor: pointer;
}
.element-action-button:hover {
background-color: #6875D6;
}
.form-info {
margin-top: 15px;
padding: 10px;
background-color: #f5f5f5;
border-radius: 4px;
}
.form-info-text {
color: #666;
margin: 0;
font-size: 14px;
}
</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.loadModelFiles().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 {
await this.saveFileContent(this.selectedFile, this.xmlContent);
this.resetEditState();
// 更新保存按钮状态
saveButton.classList.remove('modified');
} catch (error) {
alert('保存失败: ' + error.message);
}
});
// 另存为
const saveAsButton = this.shadowRoot.getElementById('saveAsConfig');
saveAsButton.addEventListener('click', () => {
if (!this.xmlContent) {
alert('没有内容可保存');
return;
}
this.showSaveAsDialog();
});
}
renderVisualEditor(container, xmlDoc) {
if (!xmlDoc || !container) return;
// 添加样式
const style = document.createElement('style');
style.textContent = `
.visual-editor {
padding: 10px;
}
.section-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 10px;
color: #333;
border-bottom: 1px solid #e0e0e0;
padding-bottom: 8px;
}
.section {
margin-bottom: 20px;
background-color: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 16px;
}
.property-row {
display: flex;
margin-bottom: 12px;
align-items: center;
}
.property-label {
width: 150px;
font-weight: 500;
color: #555;
}
.property-input {
flex: 1;
padding: 8px 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.property-input:focus {
border-color: #7986E7;
outline: none;
box-shadow: 0 0 0 2px rgba(121, 134, 231, 0.2);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.section-title-text {
font-size: 18px;
font-weight: bold;
color: #333;
}
.section-buttons {
display: flex;
gap: 8px;
}
.section-button {
background-color: #7986E7;
color: white;
border: none;
border-radius: 4px;
padding: 6px 12px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
}
.section-button:hover {
background-color: #6875D6;
}
.error-message {
color: #d32f2f;
background-color: #ffebee;
padding: 10px;
border-radius: 4px;
margin-bottom: 15px;
}
`;
const visualEditor = document.createElement('div');
visualEditor.className = 'visual-editor';
// 获取根元素
const rootElement = xmlDoc.documentElement;
// 只处理Model根元素
if (rootElement.nodeName === 'Model') {
// 创建模态对话框容器
const modalContainer = document.createElement('div');
modalContainer.className = 'modal';
modalContainer.id = 'propertyModal';
container.appendChild(modalContainer);
// 渲染基本信息部分
this.renderBasicInfoSection(visualEditor, rootElement);
// 渲染高级设置部分
this.renderAdvancedSection(visualEditor, rootElement);
} else {
// 不是Model根元素显示错误信息
visualEditor.innerHTML = `<div class="error-message">
无法编辑XML文档的根元素不是Model。
请确保XML文档的根元素是Model。
</div>`;
}
container.appendChild(style);
container.appendChild(visualEditor);
// 自动保存配置到XML
this.autoSaveToXml();
}
// 自动保存表单内容到XML
autoSaveToXml() {
// 为所有输入框添加change事件
const inputs = this.shadowRoot.querySelectorAll('.property-input');
inputs.forEach(input => {
input.addEventListener('change', () => {
this.updateXmlFromVisualEditor(); // 更新XML
this.markEdited(); // 标记已编辑
});
});
}
renderBasicInfoSection(container, rootElement) {
const section = document.createElement('div');
section.className = 'section';
const sectionHeader = document.createElement('div');
sectionHeader.className = 'section-header';
const sectionTitle = document.createElement('div');
sectionTitle.className = 'section-title-text';
sectionTitle.textContent = '基本信息';
sectionHeader.appendChild(sectionTitle);
section.appendChild(sectionHeader);
// 基本信息字段及描述
const basicInfoFields = [
{ name: 'Name', label: '模型名称', placeholder: '请输入模型名称', tooltip: '模型的名称,用于在系统中标识此模型' },
{ name: 'Description', label: '模型描述', placeholder: '请输入模型描述', tooltip: '对模型功能和用途的简要描述' },
{ name: 'Author', label: '作者', placeholder: '请输入作者', tooltip: '模型创建者或维护者的姓名' },
{ name: 'Version', label: '版本', placeholder: '请输入版本号,如: 1.0.0', tooltip: '模型的版本号,建议使用语义化版本号格式' },
{ name: 'CreateTime', label: '创建时间', placeholder: '创建时间,如: 2023-12-31 12:00:00', tooltip: '模型首次创建的日期和时间', type: 'datetime' },
{ name: 'ChangeTime', label: '修改时间', placeholder: '修改时间,如: 2023-12-31 12:00:00', tooltip: '模型最后一次修改的日期和时间', type: 'datetime' }
];
// 创建两列布局容器
const twoColumnForm = document.createElement('div');
twoColumnForm.className = 'form-container two-column-form';
twoColumnForm.style.display = 'grid';
twoColumnForm.style.gridTemplateColumns = 'repeat(2, 1fr)';
twoColumnForm.style.gap = '16px';
// 处理基本信息字段
basicInfoFields.forEach(field => {
// 获取元素值
const element = rootElement.querySelector(field.name);
const value = element ? element.textContent : '';
// 创建属性行
const row = document.createElement('div');
row.className = 'property-row';
// 创建标签
const label = document.createElement('div');
label.className = 'property-label';
label.textContent = field.label;
label.style.width = '90px'; // 设置固定宽度使标签对齐
// 创建输入容器
const inputContainer = document.createElement('div');
inputContainer.className = 'input-container';
inputContainer.style.flex = '1';
inputContainer.style.display = 'flex';
// 创建输入框
const input = document.createElement('input');
input.type = 'text';
input.className = 'property-input';
input.dataset.field = field.name;
input.value = value;
input.placeholder = field.placeholder;
input.title = field.tooltip;
// 添加输入事件处理
input.addEventListener('change', () => {
this.updateElementValue(field.name, input.value);
});
inputContainer.appendChild(input);
// 对于日期时间类型,添加日期选择按钮
if (field.type === 'datetime') {
input.style.flex = '1';
const datetimeButton = document.createElement('button');
datetimeButton.type = 'button';
datetimeButton.className = 'icon-button calendar-button';
datetimeButton.title = '选择日期和时间';
datetimeButton.addEventListener('click', () => {
this.showDateTimeDialog(input);
});
// 添加一个刷新按钮,快速设置为当前时间
const refreshButton = document.createElement('button');
refreshButton.type = 'button';
refreshButton.className = 'icon-button refresh-button-sm';
refreshButton.title = '设置为当前时间';
refreshButton.addEventListener('click', () => {
const now = new Date();
const dateStr = now.toISOString().split('T')[0];
const timeStr = now.toTimeString().split(' ')[0];
const datetimeStr = `${dateStr} ${timeStr}`;
input.value = datetimeStr;
this.updateElementValue(field.name, datetimeStr);
this.markEdited(); // 确保触发修改标志
});
inputContainer.appendChild(datetimeButton);
inputContainer.appendChild(refreshButton);
}
// 组装行
row.appendChild(label);
row.appendChild(inputContainer);
twoColumnForm.appendChild(row);
});
section.appendChild(twoColumnForm);
container.appendChild(section);
}
// 显示日期时间选择对话框
showDateTimeDialog(inputElement) {
// 解析当前日期和时间
let currentDate = new Date();
let currentTime = '00:00:00';
if (inputElement.value) {
try {
const parts = inputElement.value.split(' ');
if (parts.length >= 1) {
const dateParts = parts[0].split('-');
if (dateParts.length === 3) {
const year = parseInt(dateParts[0]);
const month = parseInt(dateParts[1]) - 1; // 月份从0开始
const day = parseInt(dateParts[2]);
currentDate = new Date(year, month, day);
}
}
if (parts.length >= 2) {
currentTime = parts[1];
}
} catch (error) {
console.error('解析日期时间失败:', error);
currentDate = new Date();
}
}
// 创建模态框
const modal = document.createElement('div');
modal.className = 'modal';
modal.id = 'dateTimeModal';
// 获取年、月、日
const year = currentDate.getFullYear();
const month = currentDate.getMonth(); // 0-11
const day = currentDate.getDate();
// 生成日历
const daysInMonth = new Date(year, month + 1, 0).getDate();
const firstDay = new Date(year, month, 1).getDay(); // 0-60表示周日
// 生成月份选项
let monthOptions = '';
const monthNames = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'];
monthNames.forEach((name, idx) => {
monthOptions += `<option value="${idx}" ${idx === month ? 'selected' : ''}>${name}</option>`;
});
// 生成年份选项
let yearOptions = '';
const currentYear = new Date().getFullYear();
for (let y = currentYear - 10; y <= currentYear + 10; y++) {
yearOptions += `<option value="${y}" ${y === year ? 'selected' : ''}>${y}</option>`;
}
// 生成日历表格
let calendarRows = '';
let dayCount = 1;
// 添加表头
calendarRows += '<tr>';
['日', '一', '二', '三', '四', '五', '六'].forEach(dayName => {
calendarRows += `<th>${dayName}</th>`;
});
calendarRows += '</tr>';
// 计算行数
const totalCells = firstDay + daysInMonth;
const rowCount = Math.ceil(totalCells / 7);
// 添加日期行
for (let i = 0; i < rowCount; i++) {
calendarRows += '<tr>';
for (let j = 0; j < 7; j++) {
if ((i === 0 && j < firstDay) || dayCount > daysInMonth) {
calendarRows += '<td></td>';
} else {
const isToday = dayCount === day;
calendarRows += `<td class="calendar-day ${isToday ? 'selected' : ''}" data-day="${dayCount}">${dayCount}</td>`;
dayCount++;
}
}
calendarRows += '</tr>';
}
modal.innerHTML = `
<div class="modal-content" style="max-width: 400px;">
<div class="modal-header">
<div class="modal-title">选择日期和时间</div>
<span class="close">&times;</span>
</div>
<div class="modal-body">
<div class="calendar-container">
<div class="calendar-header">
<select id="calendarMonth">${monthOptions}</select>
<select id="calendarYear">${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" step="1" value="${currentTime}" style="width: 100%; padding: 8px;">
</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 = `
.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;
}
.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;
}
`;
// 添加到DOM
this.shadowRoot.appendChild(style);
this.shadowRoot.appendChild(modal);
modal.style.display = 'block';
// 事件处理
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 = () => {
modal.style.display = 'none';
this.shadowRoot.removeChild(modal);
if (style.parentNode) {
this.shadowRoot.removeChild(style);
}
};
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}`;
// 触发更改事件
this.updateElementValue(inputElement.dataset.field, inputElement.value);
this.markEdited(); // 确保触发修改标志
closeModal();
});
}
renderAdvancedSection(container, rootElement) {
const section = document.createElement('div');
section.className = 'section';
const sectionHeader = document.createElement('div');
sectionHeader.className = 'section-header';
const sectionTitle = document.createElement('div');
sectionTitle.className = 'section-title-text';
sectionTitle.textContent = '运行设置';
sectionHeader.appendChild(sectionTitle);
section.appendChild(sectionHeader);
// 创建表单
const form = document.createElement('div');
form.className = 'form-container';
// 创建节点和优先级所在的行
const nodeAndPriorityRow = document.createElement('div');
nodeAndPriorityRow.className = 'property-row';
nodeAndPriorityRow.style.display = 'grid';
nodeAndPriorityRow.style.gridTemplateColumns = 'auto 1fr auto 1fr';
nodeAndPriorityRow.style.gap = '16px';
nodeAndPriorityRow.style.alignItems = 'center';
// ----- 节点部分 -----
// 创建节点标签
const nodeLabel = document.createElement('div');
nodeLabel.className = 'property-label';
nodeLabel.textContent = '运行节点';
nodeLabel.style.width = '90px'; // 与基本信息部分统一宽度
// 创建节点输入容器
const nodeContainer = document.createElement('div');
nodeContainer.className = 'input-container';
nodeContainer.style.display = 'flex';
nodeContainer.style.gap = '10px';
nodeContainer.style.width = 'calc(100% - 20px)'; // 控制整体宽度
// 获取当前节点值
const nodeElement = rootElement.querySelector('Node');
const nodeValue = nodeElement ? nodeElement.textContent : '0-0';
// 解析当前节点值,格式为"x-y"
let [freqGroup, nodeIndex] = nodeValue.split('-').map(Number);
if (isNaN(freqGroup)) freqGroup = 0;
if (isNaN(nodeIndex)) nodeIndex = 0;
// 创建运行频率下拉框
const freqGroupSelect = document.createElement('select');
freqGroupSelect.className = 'property-input custom-select';
freqGroupSelect.style.flex = '1';
freqGroupSelect.style.width = '120px';
freqGroupSelect.style.maxWidth = '120px';
freqGroupSelect.style.padding = '5px';
freqGroupSelect.style.appearance = 'none';
freqGroupSelect.style.backgroundImage = 'url("assets/icons/png/chevron-down_b.png")';
freqGroupSelect.style.backgroundRepeat = 'no-repeat';
freqGroupSelect.style.backgroundPosition = 'right 8px center';
freqGroupSelect.style.backgroundSize = '14px';
freqGroupSelect.style.paddingRight = '30px';
freqGroupSelect.title = '选择运行频率0=基频1=半频2=1/4频3=1/8频以此类推';
// 创建运行频率标签
const freqGroupLabel = document.createElement('div');
freqGroupLabel.className = 'property-sublabel';
freqGroupLabel.textContent = '运行频率';
freqGroupLabel.style.fontSize = '12px';
freqGroupLabel.style.marginBottom = '3px';
freqGroupLabel.title = '选择运行频率0=基频1=半频2=1/4频3=1/8频以此类推';
// 创建连接符
const connector = document.createElement('div');
connector.textContent = '——';
connector.style.display = 'flex';
connector.style.alignItems = 'flex-start';
connector.style.marginTop = '25px';
connector.style.fontWeight = 'bold';
connector.style.padding = '0 8px';
connector.style.fontSize = '16px';
// 创建节点索引标签
const nodeIndexLabel = document.createElement('div');
nodeIndexLabel.className = 'property-sublabel';
nodeIndexLabel.textContent = '节点号';
nodeIndexLabel.style.fontSize = '12px';
nodeIndexLabel.style.marginBottom = '3px';
nodeIndexLabel.title = '选择节点编号,范围根据运行频率决定';
// 创建运行频率容器
const freqGroupContainer = document.createElement('div');
freqGroupContainer.style.display = 'flex';
freqGroupContainer.style.flexDirection = 'column';
freqGroupContainer.style.width = '120px';
// 创建节点索引容器
const nodeIndexContainer = document.createElement('div');
nodeIndexContainer.style.display = 'flex';
nodeIndexContainer.style.flexDirection = 'column';
nodeIndexContainer.style.width = '80px';
// 设置节点行的工具提示
nodeContainer.title = '运行节点格式为"运行频率-节点编号",运行频率值越大,可用节点数越多';
// 添加运行频率选项
const freqOptions = [
{ value: 0, text: '基频' },
{ value: 1, text: '半频' },
{ value: 2, text: '1/4频' },
{ value: 3, text: '1/8频' },
{ value: 4, text: '1/16频' },
{ value: 5, text: '1/32频' }
];
freqOptions.forEach(option => {
const optionElement = document.createElement('option');
optionElement.value = option.value;
optionElement.textContent = option.text;
if (parseInt(option.value) === freqGroup) {
optionElement.selected = true;
}
freqGroupSelect.appendChild(optionElement);
});
// 创建节点索引下拉框
const nodeIndexSelect = document.createElement('select');
nodeIndexSelect.className = 'property-input custom-select';
nodeIndexSelect.style.width = '80px';
nodeIndexSelect.style.padding = '5px';
nodeIndexSelect.style.appearance = 'none';
nodeIndexSelect.style.backgroundImage = 'url("assets/icons/png/chevron-down_b.png")';
nodeIndexSelect.style.backgroundRepeat = 'no-repeat';
nodeIndexSelect.style.backgroundPosition = 'right 8px center';
nodeIndexSelect.style.backgroundSize = '14px';
nodeIndexSelect.style.paddingRight = '30px';
nodeIndexSelect.title = '选择节点编号范围从0到2^(运行频率组-1';
// 更新节点索引下拉框的选项
const updateNodeOptions = (freqGroupValue) => {
nodeIndexSelect.innerHTML = ''; // 清空现有选项
// 根据运行频率计算节点数量
const nodeCount = Math.pow(2, freqGroupValue);
// 添加节点选项
for (let i = 0; i < nodeCount; i++) {
const optionElement = document.createElement('option');
optionElement.value = i;
optionElement.textContent = i;
if (i === nodeIndex) {
optionElement.selected = true;
}
nodeIndexSelect.appendChild(optionElement);
}
// 如果当前选中的节点索引超出了范围则重置为0
if (nodeIndex >= nodeCount) {
nodeIndex = 0;
nodeIndexSelect.value = nodeIndex;
// 更新XML值
this.updateElementValue('Node', `${freqGroupValue}-${nodeIndex}`);
}
};
// 初始化节点选项
updateNodeOptions(freqGroup);
// 添加运行频率下拉框的变更事件
freqGroupSelect.addEventListener('change', () => {
const newFreqGroup = parseInt(freqGroupSelect.value);
updateNodeOptions(newFreqGroup);
// 更新XML值
this.updateElementValue('Node', `${newFreqGroup}-${nodeIndexSelect.value}`);
});
// 添加节点索引下拉框的变更事件
nodeIndexSelect.addEventListener('change', () => {
// 更新XML值
this.updateElementValue('Node', `${freqGroupSelect.value}-${nodeIndexSelect.value}`);
});
// 添加到容器
freqGroupContainer.appendChild(freqGroupLabel);
freqGroupContainer.appendChild(freqGroupSelect);
nodeIndexContainer.appendChild(nodeIndexLabel);
nodeIndexContainer.appendChild(nodeIndexSelect);
nodeContainer.appendChild(freqGroupContainer);
nodeContainer.appendChild(connector);
nodeContainer.appendChild(nodeIndexContainer);
// ----- 优先级部分 -----
// 获取优先级值
const priorityElement = rootElement.querySelector('Priority');
const priorityValue = priorityElement ? priorityElement.textContent : '';
// 创建优先级标签
const priorityLabel = document.createElement('div');
priorityLabel.className = 'property-label';
priorityLabel.textContent = '运行优先级';
priorityLabel.style.width = '90px'; // 与其他标签统一宽度
// 创建优先级输入容器
const priorityContainer = document.createElement('div');
priorityContainer.className = 'input-container';
priorityContainer.style.display = 'flex';
priorityContainer.style.width = '100%';
// 创建优先级输入框
const priorityInput = document.createElement('input');
priorityInput.type = 'text';
priorityInput.className = 'property-input';
priorityInput.dataset.field = 'Priority';
priorityInput.value = priorityValue;
priorityInput.placeholder = '运行优先级,如: 99';
priorityInput.title = '模型运行的优先级,数字越大优先级越高';
// 添加优先级输入事件处理
priorityInput.addEventListener('change', () => {
this.updateElementValue('Priority', priorityInput.value);
});
priorityContainer.appendChild(priorityInput);
// 组装节点和优先级到同一行
nodeAndPriorityRow.appendChild(nodeLabel);
nodeAndPriorityRow.appendChild(nodeContainer);
nodeAndPriorityRow.appendChild(priorityLabel);
nodeAndPriorityRow.appendChild(priorityContainer);
form.appendChild(nodeAndPriorityRow);
// 添加数据包动态库路径字段
const mathLibElement = rootElement.querySelector('MathLib');
const mathLibValue = mathLibElement ? mathLibElement.textContent : '';
// 创建数据包动态库路径行
const mathLibRow = document.createElement('div');
mathLibRow.className = 'property-row';
// 创建标签
const mathLibLabel = document.createElement('div');
mathLibLabel.className = 'property-label';
mathLibLabel.textContent = '数据包动态库路径';
mathLibLabel.style.width = '90px'; // 与其他标签统一宽度
// 创建输入容器
const mathLibContainer = document.createElement('div');
mathLibContainer.className = 'input-container';
mathLibContainer.style.flex = '1';
mathLibContainer.style.display = 'flex';
mathLibContainer.style.marginLeft = '0'; // 确保左对齐
// 创建输入框
const mathLibInput = document.createElement('input');
mathLibInput.type = 'text';
mathLibInput.className = 'property-input';
mathLibInput.dataset.field = 'MathLib';
mathLibInput.value = mathLibValue;
mathLibInput.placeholder = '数据包动态库路径';
mathLibInput.title = '模型使用的数据包动态库的相对文件路径,相对于模型库目录';
// 添加输入事件处理
mathLibInput.addEventListener('change', () => {
this.updateElementValue('MathLib', mathLibInput.value);
});
mathLibContainer.appendChild(mathLibInput);
// 组装行
mathLibRow.appendChild(mathLibLabel);
mathLibRow.appendChild(mathLibContainer);
form.appendChild(mathLibRow);
// 添加命令列表表格
this.renderCommandListTable(form, rootElement);
section.appendChild(form);
container.appendChild(section);
// 添加其它设置部分
this.renderOtherSettingsSection(container, rootElement);
}
// 渲染其它设置部分
renderOtherSettingsSection(container, rootElement) {
const section = document.createElement('div');
section.className = 'settings-section other-settings';
const header = document.createElement('div');
header.className = 'section-header';
header.innerHTML = `
<h3>其他设置</h3>
<div class="section-note" style="font-size: 12px; color: #666; margin-top: 5px;">如需添加参数,请手动编辑配置文件</div>
`;
section.appendChild(header);
// 创建元素容器
const elementsContainer = document.createElement('div');
elementsContainer.id = 'otherSettingsContainer';
elementsContainer.className = 'elements-container';
// 渲染所有元素
this.renderElements(elementsContainer, rootElement, '/' + rootElement.nodeName);
section.appendChild(elementsContainer);
container.appendChild(section);
}
// 递归渲染XML元素
renderElements(container, parentElement, parentPath, level = 0) {
// 过滤出需要在其他设置中显示的元素
const commonElements = ['Name', 'Description', 'Author', 'Version', 'CreateTime', 'ChangeTime',
'Node', 'Priority', 'MathLib', 'CommandList'];
Array.from(parentElement.children).forEach(element => {
// 跳过常见元素,这些在基本信息和高级设置中已经显示
if (commonElements.includes(element.nodeName)) {
return;
}
const elementName = element.nodeName;
const elementPath = `${parentPath}/${elementName}`;
// 创建元素行
const elementRow = document.createElement('div');
elementRow.className = 'element-row';
elementRow.dataset.path = elementPath;
// 添加缩进
const indent = level * 20; // 每级缩进20px
elementRow.style.paddingLeft = `${indent}px`;
// 创建元素框
const elementBox = document.createElement('div');
elementBox.className = 'element-box';
// 添加展开/折叠按钮(如果有子元素)
if (element.children.length > 0) {
const toggleButton = document.createElement('button');
toggleButton.className = 'toggle-button';
toggleButton.innerHTML = '▼'; // 默认展开
toggleButton.addEventListener('click', () => {
const childContainer = elementRow.nextElementSibling;
if (childContainer && childContainer.classList.contains('element-children')) {
if (childContainer.style.display === 'none') {
childContainer.style.display = 'block';
toggleButton.innerHTML = '▼';
} else {
childContainer.style.display = 'none';
toggleButton.innerHTML = '►';
}
}
});
elementBox.appendChild(toggleButton);
} else {
// 如果没有子元素,添加占位符
const toggleSpacer = document.createElement('div');
toggleSpacer.className = 'toggle-spacer';
elementBox.appendChild(toggleSpacer);
}
// 添加元素名称
const nameElement = document.createElement('div');
nameElement.className = 'element-name';
nameElement.textContent = elementName;
elementBox.appendChild(nameElement);
// 添加元素值/编辑控件
const valueContainer = document.createElement('div');
valueContainer.className = 'element-value-container';
// 获取元素值
let elementValue = '';
if (element.children.length === 0) {
elementValue = element.textContent;
}
// 创建输入框(如果是叶子节点)
if (element.children.length === 0) {
const valueInput = document.createElement('input');
valueInput.type = 'text';
valueInput.className = 'element-value-input';
valueInput.value = elementValue;
valueInput.placeholder = '输入值';
valueInput.dataset.path = elementPath;
// 添加改变事件
valueInput.addEventListener('change', (e) => {
const newValue = e.target.value;
this.updateElementByPath(elementPath, newValue);
});
valueContainer.appendChild(valueInput);
}
elementBox.appendChild(valueContainer);
// 添加操作按钮
const actionsContainer = document.createElement('div');
actionsContainer.className = 'element-actions';
// 删除按钮
const deleteButton = document.createElement('button');
deleteButton.className = 'element-action-button';
deleteButton.textContent = '删除';
deleteButton.style.backgroundColor = '#E77979';
deleteButton.addEventListener('click', () => {
this.deleteElement(element, elementPath);
});
actionsContainer.appendChild(deleteButton);
elementBox.appendChild(actionsContainer);
elementRow.appendChild(elementBox);
container.appendChild(elementRow);
// 如果有子元素,创建子元素容器
if (element.children.length > 0) {
const childContainer = document.createElement('div');
childContainer.className = 'element-children';
childContainer.dataset.parentPath = elementPath;
childContainer.style.paddingLeft = '20px'; // 子元素缩进
// 递归渲染子元素
this.renderElements(childContainer, element, elementPath, level + 1);
container.appendChild(childContainer);
}
});
}
// 显示添加元素对话框
showAddElementDialog(parentElement, parentPath) {
// 创建模态框
const modal = document.createElement('div');
modal.className = 'modal';
modal.id = 'addElementModal';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<div class="modal-title">添加元素</div>
<span class="close">&times;</span>
</div>
<div class="modal-body">
<div class="form-group">
<label for="elementName">元素名称</label>
<input type="text" id="elementName" class="form-control" placeholder="输入元素名称" />
<div id="elementNameError" class="error-message" style="display: none; font-size: 12px; margin-top: 5px;"></div>
</div>
<div class="form-group">
<label for="elementValue">元素值 <small>(如果需要)</small></label>
<input type="text" id="elementValue" class="form-control" placeholder="输入元素值" />
</div>
</div>
<div class="modal-footer">
<button id="cancelAddElement" class="action-button secondary">取消</button>
<button id="confirmAddElement" class="action-button">添加</button>
</div>
</div>
`;
this.shadowRoot.appendChild(modal);
modal.style.display = 'block';
// 事件处理
const closeBtn = modal.querySelector('.close');
const cancelBtn = modal.querySelector('#cancelAddElement');
const confirmBtn = modal.querySelector('#confirmAddElement');
const nameInput = modal.querySelector('#elementName');
const valueInput = modal.querySelector('#elementValue');
const errorDiv = modal.querySelector('#elementNameError');
// 实时验证元素名称
nameInput.addEventListener('input', () => {
const name = nameInput.value.trim();
const isValid = this.isValidElementName(name);
if (!name) {
errorDiv.textContent = '元素名称不能为空';
errorDiv.style.display = 'block';
confirmBtn.disabled = true;
} else if (!isValid) {
errorDiv.textContent = '无效的元素名称。元素名称必须以字母或下划线开头,不能包含空格或特殊字符。';
errorDiv.style.display = 'block';
confirmBtn.disabled = true;
} else {
errorDiv.style.display = 'none';
confirmBtn.disabled = false;
}
});
const closeModal = () => {
modal.style.display = 'none';
this.shadowRoot.removeChild(modal);
};
closeBtn.addEventListener('click', closeModal);
cancelBtn.addEventListener('click', closeModal);
confirmBtn.addEventListener('click', () => {
const name = nameInput.value.trim();
const value = valueInput.value;
if (!name) {
alert('元素名称不能为空');
return;
}
if (!this.isValidElementName(name)) {
alert('无效的元素名称。元素名称必须以字母或下划线开头,不能包含空格或特殊字符。');
return;
}
// 添加元素
this.addElement(parentElement, name, value);
// 重新渲染元素列表
const container = this.shadowRoot.querySelector('#otherSettingsContainer');
if (container) {
container.innerHTML = '';
this.renderElements(container, this.xmlDoc.documentElement, '/' + this.xmlDoc.documentElement.nodeName);
}
closeModal();
});
}
// 验证元素名称是否符合XML规范
isValidElementName(name) {
// XML元素名称规则:
// 1. 不能以数字或标点符号开头
// 2. 不能以xml大写、小写、混合大小写开头
// 3. 不能包含空格
// 4. 只能包含字母、数字、下划线、连字符和点
if (!name) return false;
// 检查开头是否为字母或下划线
if (!/^[a-zA-Z_]/.test(name)) {
return false;
}
// 检查是否以xml开头不区分大小写
if (/^xml/i.test(name)) {
return false;
}
// 检查是否包含有效字符
if (!/^[a-zA-Z0-9_\-\.]+$/.test(name)) {
return false;
}
return true;
}
// 显示编辑元素对话框
showEditElementDialog(element, elementPath) {
// 创建模态框
const modal = document.createElement('div');
modal.className = 'modal';
modal.id = 'editElementModal';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<div class="modal-title">编辑元素</div>
<span class="close">&times;</span>
</div>
<div class="modal-body">
<div class="form-group">
<label for="elementPath">元素路径</label>
<input type="text" id="elementPath" class="form-control" value="${elementPath}" readonly />
</div>
<div class="form-group">
<label for="elementValue">元素值</label>
<input type="text" id="elementValue" class="form-control" value="${this.escapeHtml(element.textContent)}" placeholder="输入元素值" />
</div>
</div>
<div class="modal-footer">
<button id="cancelEditElement" class="action-button secondary">取消</button>
<button id="confirmEditElement" class="action-button">保存</button>
</div>
</div>
`;
this.shadowRoot.appendChild(modal);
modal.style.display = 'block';
// 事件处理
const closeBtn = modal.querySelector('.close');
const cancelBtn = modal.querySelector('#cancelEditElement');
const confirmBtn = modal.querySelector('#confirmEditElement');
const valueInput = modal.querySelector('#elementValue');
const closeModal = () => {
modal.style.display = 'none';
this.shadowRoot.removeChild(modal);
};
closeBtn.addEventListener('click', closeModal);
cancelBtn.addEventListener('click', closeModal);
confirmBtn.addEventListener('click', () => {
const value = valueInput.value;
// 更新元素值
this.updateElementByPath(elementPath, value);
// 更新UI中的值
const inputElement = this.shadowRoot.querySelector(`input[data-path="${elementPath}"]`);
if (inputElement) {
inputElement.value = value;
}
closeModal();
});
}
// 根据路径更新元素值
updateElementByPath(path, value) {
if (!this.xmlDoc) return;
try {
// 分割路径
const pathParts = path.split('/').filter(p => p);
let currentElement = this.xmlDoc.documentElement;
// 跳过根元素
for (let i = 1; i < pathParts.length; i++) {
const part = pathParts[i];
// 查找子元素
let found = false;
for (let j = 0; j < currentElement.children.length; j++) {
if (currentElement.children[j].nodeName === part) {
currentElement = currentElement.children[j];
found = true;
break;
}
}
if (!found) {
throw new Error(`找不到元素: ${path}`);
}
}
// 更新元素值
currentElement.textContent = value;
// 更新XML内容
this.updateXmlFromVisualEditor();
this.markEdited();
} catch (error) {
console.error('更新元素失败:', error);
alert('更新元素失败: ' + error.message);
}
}
// 添加新元素
addElement(parentElement, name, value) {
if (!this.xmlDoc) return;
try {
// 创建新元素
const newElement = this.xmlDoc.createElement(name);
// 设置值
if (value) {
newElement.textContent = value;
}
// 添加到父元素
parentElement.appendChild(newElement);
// 更新XML内容
this.updateXmlFromVisualEditor();
this.markEdited();
} catch (error) {
console.error('添加元素失败:', error);
alert('添加元素失败: ' + error.message);
}
}
// 删除元素
deleteElement(element, elementPath) {
if (!this.xmlDoc) return;
// 确认删除
if (!confirm(`确定要删除元素 ${element.nodeName} 吗?此操作不可撤销。`)) {
return;
}
try {
// 删除元素
element.parentNode.removeChild(element);
// 更新XML内容
this.updateXmlFromVisualEditor();
this.markEdited();
// 重新渲染元素列表
const container = this.shadowRoot.querySelector('#otherSettingsContainer');
if (container) {
container.innerHTML = '';
this.renderElements(container, this.xmlDoc.documentElement, '/' + this.xmlDoc.documentElement.nodeName);
}
} catch (error) {
console.error('删除元素失败:', error);
alert('删除元素失败: ' + error.message);
}
}
// 更新元素值
updateElementValue(elementName, value) {
if (!this.xmlDoc) return;
const root = this.xmlDoc.documentElement;
let element = null;
// 查找元素
for (let i = 0; i < root.children.length; i++) {
if (root.children[i].nodeName === elementName) {
element = root.children[i];
break;
}
}
// 如果元素不存在则创建
if (!element) {
element = this.xmlDoc.createElement(elementName);
root.appendChild(element);
}
// 更新值
element.textContent = value;
// 更新XML内容
const modifiedXml = new XMLSerializer().serializeToString(this.xmlDoc);
this.xmlContent = modifiedXml;
// 标记已编辑
this.markEdited();
}
// 渲染命令列表表格
renderCommandListTable(container, rootElement) {
// 查找CommandList元素
const commandListElement = rootElement.querySelector('CommandList');
if (!commandListElement) {
// 创建CommandList部分
const commandListSection = document.createElement('div');
commandListSection.className = 'form-group';
commandListSection.innerHTML = `
<label for="commandListTable">命令列表</label>
<div id="commandListContainer" class="command-table-container">
<table id="commandListTable" class="command-table">
<thead>
<tr>
<th>名称</th>
<th>调用</th>
<th>描述</th>
<th style="width: 100px;">操作</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="4" style="text-align: center;">
<button id="addCommandButton" class="action-button">
添加命令
</button>
</td>
</tr>
</tbody>
</table>
</div>
`;
container.appendChild(commandListSection);
// 添加命令事件监听
const addCommandButton = commandListSection.querySelector('#addCommandButton');
addCommandButton.addEventListener('click', () => {
this.showAddCommandDialog(rootElement);
});
return;
}
// 创建CommandList部分
const commandListSection = document.createElement('div');
commandListSection.className = 'form-group';
commandListSection.innerHTML = `
<label for="commandListTable">命令列表</label>
<div id="commandListContainer" class="command-table-container">
<table id="commandListTable" class="command-table">
<thead>
<tr>
<th>名称</th>
<th>调用</th>
<th>描述</th>
<th style="width: 100px;">操作</th>
</tr>
</thead>
<tbody>
<!-- 命令会在这里动态添加 -->
</tbody>
</table>
</div>
`;
container.appendChild(commandListSection);
const tableBody = commandListSection.querySelector('tbody');
// 获取命令列表
const commandElements = commandListElement.querySelectorAll('Command');
// 如果没有命令,添加"添加命令"按钮
if (commandElements.length === 0) {
tableBody.innerHTML = `
<tr>
<td colspan="4" style="text-align: center;">
<button id="addCommandButton" class="action-button">
添加命令
</button>
</td>
</tr>
`;
// 添加命令事件监听
const addCommandButton = tableBody.querySelector('#addCommandButton');
addCommandButton.addEventListener('click', () => {
this.showAddCommandDialog(rootElement);
});
return;
}
// 添加命令到表格
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.className = 'command-row';
row.dataset.index = index;
row.innerHTML = `
<td class="command-cell">${this.escapeHtml(name)}</td>
<td class="command-cell">${this.escapeHtml(call)}</td>
<td class="command-cell">${this.escapeHtml(description)}</td>
<td class="action-cell">
<div class="command-actions">
<button class="command-action-button edit-button">编辑</button>
<button class="command-action-button delete-button">删除</button>
</div>
</td>
`;
// 编辑按钮事件
row.querySelector('.edit-button').addEventListener('click', () => {
this.showEditCommandDialog(command, index);
});
// 删除按钮事件
row.querySelector('.delete-button').addEventListener('click', () => {
this.deleteCommand(command, index);
});
tableBody.appendChild(row);
});
// 添加"添加命令"按钮行
const addRow = document.createElement('tr');
addRow.innerHTML = `
<td colspan="4" style="text-align: center;">
<button id="addCommandButton" class="action-button">
添加命令
</button>
</td>
`;
// 添加命令事件监听
addRow.querySelector('#addCommandButton').addEventListener('click', () => {
this.showAddCommandDialog(rootElement);
});
tableBody.appendChild(addRow);
}
markEdited() {
this.isEdited = true;
// 更新保存按钮样式
const saveButton = this.shadowRoot.getElementById('saveConfig');
if (saveButton) {
saveButton.classList.add('modified');
}
}
resetEditState() {
this.isEdited = false;
// 更新保存按钮样式
const saveButton = this.shadowRoot.getElementById('saveConfig');
if (saveButton) {
saveButton.classList.remove('modified');
}
}
formatXml(xml) {
if (!xml) return '';
try {
// 使用DOMParser解析XML并使用XMLSerializer重新序列化
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xml, 'application/xml');
// 检查解析错误
const parseError = xmlDoc.querySelector('parsererror');
if (parseError) {
return xml; // 如果有解析错误返回原始XML
}
// 创建格式化函数
const PADDING = ' '; // 两个空格作为缩进
const reg = /(>)(<)(\/*)/g;
let pad = 0;
// 替换XML中的>< 为 >\n<
let formatted = new XMLSerializer().serializeToString(xmlDoc)
.replace(reg, '$1\n$2$3')
.split('\n');
let result = '';
// 根据标签添加缩进
formatted.forEach(line => {
// 检查是否是结束标签
if (line.match(/<\//)) {
pad--;
}
result += Array(pad + 1).join(PADDING) + line + '\n';
// 检查是否是开始标签且不是自闭合标签
if (line.match(/<[^\/].*[^\/]>$/)) {
pad++;
}
});
return result.trim();
} catch (error) {
console.error('XML格式化错误:', error);
return xml; // 如果有错误返回原始XML
}
}
escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
async saveFileContent(filePath, content) {
try {
const response = await fetch('/api/save-model-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-model-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.loadModelFiles();
// 加载新创建的文件
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 = `
<div class="modal-content">
<div class="modal-header">
<div class="modal-title">创建新模型配置</div>
<span class="close">&times;</span>
</div>
<div class="modal-body">
<div class="form-group">
<label for="newFileName">文件名</label>
<input type="text" id="newFileName" class="form-control" placeholder="输入文件名 (例如: mymodel.mcfg)" />
</div>
</div>
<div class="modal-footer">
<button id="cancelNewConfig" class="action-button secondary">取消</button>
<button id="confirmNewConfig" class="action-button">创建</button>
</div>
</div>
`;
this.shadowRoot.appendChild(modal);
modal.style.display = 'block';
// 事件处理
const closeBtn = modal.querySelector('.close');
const cancelBtn = modal.querySelector('#cancelNewConfig');
const confirmBtn = modal.querySelector('#confirmNewConfig');
const closeModal = () => {
modal.style.display = 'none';
this.shadowRoot.removeChild(modal);
};
closeBtn.addEventListener('click', closeModal);
cancelBtn.addEventListener('click', closeModal);
confirmBtn.addEventListener('click', async () => {
const fileName = modal.querySelector('#newFileName').value.trim();
if (!fileName) {
alert('文件名不能为空');
return;
}
// 检查文件扩展名
if (!fileName.endsWith('.mcfg')) {
alert('文件名必须以 .mcfg 结尾');
return;
}
try {
await this.createNewConfig(fileName);
closeModal();
} catch (error) {
if (error.message.includes('文件已存在')) {
if (confirm('文件已存在,是否覆盖?')) {
try {
await this.createNewConfig(fileName, true);
closeModal();
} catch (retryError) {
alert('创建文件失败: ' + retryError.message);
}
}
} else {
alert('创建文件失败: ' + error.message);
}
}
});
}
showSaveAsDialog() {
// 创建模态框
const modal = document.createElement('div');
modal.className = 'modal';
modal.id = 'saveAsModal';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<div class="modal-title">另存为</div>
<span class="close">&times;</span>
</div>
<div class="modal-body">
<div class="form-group">
<label for="saveAsFileName">文件名</label>
<input type="text" id="saveAsFileName" class="form-control" placeholder="输入文件名 (例如: mymodel.mcfg)" />
</div>
</div>
<div class="modal-footer">
<button id="cancelSaveAs" class="action-button secondary">取消</button>
<button id="confirmSaveAs" class="action-button">保存</button>
</div>
</div>
`;
this.shadowRoot.appendChild(modal);
modal.style.display = 'block';
// 事件处理
const closeBtn = modal.querySelector('.close');
const cancelBtn = modal.querySelector('#cancelSaveAs');
const confirmBtn = modal.querySelector('#confirmSaveAs');
const closeModal = () => {
modal.style.display = 'none';
this.shadowRoot.removeChild(modal);
};
closeBtn.addEventListener('click', closeModal);
cancelBtn.addEventListener('click', closeModal);
confirmBtn.addEventListener('click', async () => {
const fileName = modal.querySelector('#saveAsFileName').value.trim();
if (!fileName) {
alert('文件名不能为空');
return;
}
// 检查文件扩展名
if (!fileName.endsWith('.mcfg')) {
alert('文件名必须以 .mcfg 结尾');
return;
}
try {
const response = await fetch('/api/save-model-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-model-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.loadModelFiles();
closeModal();
}
return;
}
throw new Error(errorData.error || `保存失败: ${response.status}`);
}
const result = await response.json();
this.selectedFile = result.path;
this.resetEditState();
await this.loadModelFiles();
closeModal();
} catch (error) {
console.error('另存为失败:', error);
alert('另存为失败: ' + error.message);
}
});
}
// 显示添加命令对话框
showAddCommandDialog(rootElement) {
// 创建模态框
const modal = document.createElement('div');
modal.className = 'modal';
modal.id = 'addCommandModal';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<div class="modal-title">添加命令</div>
<span class="close">&times;</span>
</div>
<div class="modal-body">
<div class="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>
`;
this.shadowRoot.appendChild(modal);
modal.style.display = 'block';
// 事件处理
const closeBtn = modal.querySelector('.close');
const cancelBtn = modal.querySelector('#cancelAddCommand');
const confirmBtn = modal.querySelector('#confirmAddCommand');
const closeModal = () => {
modal.style.display = 'none';
this.shadowRoot.removeChild(modal);
};
closeBtn.addEventListener('click', closeModal);
cancelBtn.addEventListener('click', closeModal);
confirmBtn.addEventListener('click', () => {
const name = modal.querySelector('#commandName').value.trim();
const call = modal.querySelector('#commandCall').value.trim();
const description = modal.querySelector('#commandDescription').value.trim();
if (!name) {
alert('命令名称不能为空');
return;
}
if (!call) {
alert('命令调用不能为空');
return;
}
try {
// 查找或创建CommandList元素
let commandListElement = rootElement.querySelector('CommandList');
if (!commandListElement) {
commandListElement = this.xmlDoc.createElement('CommandList');
rootElement.appendChild(commandListElement);
}
// 创建新Command元素
const commandElement = this.xmlDoc.createElement('Command');
commandElement.setAttribute('Name', name);
commandElement.setAttribute('Call', call);
commandElement.setAttribute('Description', description || `${name}描述`);
// 添加到CommandList
commandListElement.appendChild(commandElement);
// 更新XML内容
this.updateXmlFromVisualEditor();
this.markEdited();
// 重新渲染命令列表
const commandListContainer = this.shadowRoot.querySelector('#commandListContainer');
const visualEditor = this.shadowRoot.querySelector('#visualEditor');
if (commandListContainer && visualEditor) {
// 清空当前渲染
visualEditor.innerHTML = '';
// 重新渲染整个编辑器
this.renderVisualEditor(visualEditor, this.xmlDoc);
}
closeModal();
} catch (error) {
console.error('添加命令失败:', error);
alert('添加命令失败: ' + error.message);
}
});
}
// 显示编辑命令对话框
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 = `
<div class="modal-content">
<div class="modal-header">
<div class="modal-title">编辑命令</div>
<span class="close">&times;</span>
</div>
<div class="modal-body">
<div class="form-group">
<label for="commandName">命令名称</label>
<input type="text" id="commandName" class="form-control" value="${this.escapeHtml(name)}" placeholder="输入命令名称" />
</div>
<div class="form-group">
<label for="commandCall">命令调用</label>
<input type="text" id="commandCall" class="form-control" value="${this.escapeHtml(call)}" placeholder="输入命令调用" />
</div>
<div class="form-group">
<label for="commandDescription">命令描述</label>
<input type="text" id="commandDescription" class="form-control" value="${this.escapeHtml(description)}" placeholder="输入命令描述" />
</div>
</div>
<div class="modal-footer">
<button id="cancelEditCommand" class="action-button secondary">取消</button>
<button id="confirmEditCommand" class="action-button">保存</button>
</div>
</div>
`;
this.shadowRoot.appendChild(modal);
modal.style.display = 'block';
// 事件处理
const closeBtn = modal.querySelector('.close');
const cancelBtn = modal.querySelector('#cancelEditCommand');
const confirmBtn = modal.querySelector('#confirmEditCommand');
const closeModal = () => {
modal.style.display = 'none';
this.shadowRoot.removeChild(modal);
};
closeBtn.addEventListener('click', closeModal);
cancelBtn.addEventListener('click', closeModal);
confirmBtn.addEventListener('click', () => {
const newName = modal.querySelector('#commandName').value.trim();
const newCall = modal.querySelector('#commandCall').value.trim();
const newDescription = modal.querySelector('#commandDescription').value.trim();
if (!newName) {
alert('命令名称不能为空');
return;
}
if (!newCall) {
alert('命令调用不能为空');
return;
}
try {
// 更新命令属性
commandElement.setAttribute('Name', newName);
commandElement.setAttribute('Call', newCall);
commandElement.setAttribute('Description', newDescription || `${newName}描述`);
// 更新XML内容
this.updateXmlFromVisualEditor();
this.markEdited();
// 更新表格行
const row = this.shadowRoot.querySelector(`.command-row[data-index="${index}"]`);
if (row) {
row.querySelectorAll('.command-cell')[0].textContent = newName;
row.querySelectorAll('.command-cell')[1].textContent = newCall;
row.querySelectorAll('.command-cell')[2].textContent = newDescription || `${newName}描述`;
} else {
// 如果找不到行,重新渲染整个编辑器
const visualEditor = this.shadowRoot.querySelector('#visualEditor');
if (visualEditor) {
visualEditor.innerHTML = '';
this.renderVisualEditor(visualEditor, this.xmlDoc);
}
}
closeModal();
} catch (error) {
console.error('编辑命令失败:', error);
alert('编辑命令失败: ' + error.message);
}
});
}
// 删除命令
deleteCommand(commandElement, index) {
if (!confirm('确定要删除此命令吗?此操作不可撤销。')) {
return;
}
try {
// 从DOM中移除命令元素
commandElement.parentNode.removeChild(commandElement);
// 更新XML内容
this.updateXmlFromVisualEditor();
this.markEdited();
// 从表格中移除行
const row = this.shadowRoot.querySelector(`.command-row[data-index="${index}"]`);
if (row) {
row.parentNode.removeChild(row);
// 更新索引
const rows = this.shadowRoot.querySelectorAll('.command-row');
rows.forEach((r, i) => {
r.dataset.index = i;
});
} else {
// 如果找不到行,重新渲染整个编辑器
const visualEditor = this.shadowRoot.querySelector('#visualEditor');
if (visualEditor) {
visualEditor.innerHTML = '';
this.renderVisualEditor(visualEditor, this.xmlDoc);
}
}
} catch (error) {
console.error('删除命令失败:', error);
alert('删除命令失败: ' + error.message);
}
}
// 从可视化编辑器更新XML
updateXmlFromVisualEditor() {
if (!this.xmlDoc) return;
const modifiedXml = new XMLSerializer().serializeToString(this.xmlDoc);
this.xmlContent = modifiedXml;
}
// 渲染命令列表表格
renderCommandList(container, commandListElement, rootElement) {
// 创建命令列表容器
const commandListContainer = document.createElement('div');
commandListContainer.id = 'commandListContainer';
commandListContainer.className = 'section';
// 创建标题和添加按钮
const header = document.createElement('div');
header.className = 'section-header';
header.innerHTML = `
<h3>命令列表</h3>
<button id="addCommandBtn" class="action-button">添加命令</button>
`;
commandListContainer.appendChild(header);
// 添加按钮事件
const addBtn = header.querySelector('#addCommandBtn');
addBtn.addEventListener('click', () => {
this.showAddCommandDialog(rootElement);
});
// 创建表格
const table = document.createElement('table');
table.className = 'command-table';
// 创建表头
const thead = document.createElement('thead');
thead.innerHTML = `
<tr>
<th>命令名称</th>
<th>命令调用</th>
<th>描述</th>
<th>操作</th>
</tr>
`;
table.appendChild(thead);
// 创建表体
const tbody = document.createElement('tbody');
// 如果存在命令列表元素,则渲染命令
if (commandListElement) {
const commands = commandListElement.querySelectorAll('Command');
if (commands.length === 0) {
// 如果没有命令,显示空行
const emptyRow = document.createElement('tr');
emptyRow.innerHTML = '<td colspan="4" style="text-align: center;">没有命令记录</td>';
tbody.appendChild(emptyRow);
} else {
// 渲染命令列表
commands.forEach((command, index) => {
const name = command.getAttribute('Name') || '';
const call = command.getAttribute('Call') || '';
const description = command.getAttribute('Description') || '';
const row = document.createElement('tr');
row.className = 'command-row';
row.dataset.index = index;
row.innerHTML = `
<td class="command-cell">${this.escapeHtml(name)}</td>
<td class="command-cell">${this.escapeHtml(call)}</td>
<td class="command-cell">${this.escapeHtml(description)}</td>
<td class="command-actions">
<button class="action-button edit-command">编辑</button>
<button class="action-button delete-command">删除</button>
</td>
`;
tbody.appendChild(row);
// 添加编辑按钮事件
const editBtn = row.querySelector('.edit-command');
editBtn.addEventListener('click', () => {
this.showEditCommandDialog(command, index);
});
// 添加删除按钮事件
const deleteBtn = row.querySelector('.delete-command');
deleteBtn.addEventListener('click', () => {
this.deleteCommand(command, index);
});
});
}
} else {
// 如果没有命令列表元素,显示空行
const emptyRow = document.createElement('tr');
emptyRow.innerHTML = '<td colspan="4" style="text-align: center;">没有命令记录</td>';
tbody.appendChild(emptyRow);
}
table.appendChild(tbody);
commandListContainer.appendChild(table);
container.appendChild(commandListContainer);
return commandListContainer;
}
// 确保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;
}
// 更新命令列表部分
updateCommandListSection() {
if (!this.xmlDoc) return;
const visualEditorContainer = document.getElementById('visualEditorContainer');
if (!visualEditorContainer) return;
// 移除已有的命令列表容器
const existingContainer = document.getElementById('commandListContainer');
if (existingContainer) {
existingContainer.remove();
}
// 获取根元素和命令列表元素
const rootElement = this.xmlDoc.documentElement;
const commandListElement = this.ensureCommandListFormat(this.xmlDoc);
// 渲染命令列表
this.renderCommandList(visualEditorContainer, commandListElement, rootElement);
}
}
customElements.define('model-config', ModelConfig);