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

947 lines
34 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

/**
* 模型配置组件
* @type {module}
*/
import {
formatXml, escapeHtml, isValidElementName
} from './model-config/utils.js';
import {
loadModelFiles as apiLoadModelFiles,
loadFileContent as apiLoadFileContent,
saveFileContent as apiSaveFileContent,
createNewConfig as apiCreateNewConfig,
saveFileAs as apiSaveFileAs
} from './model-config/api.js';
import {
parseXmlContent, updateXmlFromVisualEditor, ensureCommandListFormat,
updateElementByPath, updateElementValue, addElement, deleteElement
} from './model-config/xml-handler.js';
import {
renderBasicInfoSection, renderAdvancedSection, renderElements, renderOtherSettingsSection
} from './model-config/ui-elements.js';
import {
renderCommandListTable, renderCommandList
} from './model-config/command-handler.js';
import {
showDateTimeDialog
} from './model-config/datetime.js';
import {
showAddElementDialog, showEditElementDialog, showNewConfigDialog,
showSaveAsDialog, showAddCommandDialog, showEditCommandDialog
} from './model-config/dialogs.js';
import {
getStyles
} from './model-config/styles.js';
class ModelConfig extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.selectedFile = null;
this.xmlContent = '';
this.xmlDoc = null;
this.isEdited = false;
this.modelFiles = [];
this.isActive = true; // 添加活动状态标记
}
async connectedCallback() {
this.isActive = true;
this.render();
await this.loadModelFiles();
this.setupEventListeners();
// 延迟调用renderComplete确保DOM已更新
setTimeout(() => {
this.renderComplete();
}, 100);
}
disconnectedCallback() {
this.isActive = false;
}
// 在组件完成渲染后恢复状态与run-env-config组件一致
renderComplete() {
// 在DOM渲染完成后更新文件选择器
setTimeout(() => {
// 如果有选中的文件,尝试恢复它
if (this.selectedFile && (this.xmlContent || this.xmlDoc)) {
const fileSelector = this.shadowRoot.getElementById('fileSelector');
if (fileSelector) {
// 确认文件在列表中
const fileExists = Array.from(fileSelector.options).some(opt => opt.value === this.selectedFile);
if (fileExists) {
// 设置选中的文件
fileSelector.value = this.selectedFile;
// 更新内容显示
const contentArea = this.shadowRoot.querySelector('#contentArea');
if (contentArea && this.xmlDoc) {
contentArea.innerHTML = `
<div class="editor-container">
<div class="editor-panel active" id="visualEditor"></div>
</div>
`;
const visualEditor = this.shadowRoot.querySelector('#visualEditor');
if (visualEditor) {
this.renderVisualEditor(visualEditor, this.xmlDoc);
// 恢复编辑状态标记
if (this.isEdited) {
this.markEdited();
}
}
} else {
console.log('ModelConfig: 无法找到内容区域或XML文档不存在');
}
} else {
// 文件不存在,清除状态
this.selectedFile = null;
this.xmlContent = '';
this.xmlDoc = null;
this.isEdited = false;
}
} else {
console.log('ModelConfig: 未找到文件选择器');
}
}
}, 50); // 增加延迟确保DOM已经完全加载
}
// 重新激活组件的方法(当标签页重新被选中时调用)
async reactivate() {
if (this.isActive) {
return;
}
this.isActive = true;
try {
// 先重新渲染一次UI以确保Shadow DOM结构完整
this.render();
// 加载文件列表
await this.loadModelFiles();
// 设置事件监听器
this.setupEventListeners();
// 调用renderComplete来恢复状态这是核心改进
this.renderComplete();
} catch (error) {
console.error('ModelConfig: 重新激活组件时出错:', error);
}
}
async loadModelFiles() {
try {
this.modelFiles = await apiLoadModelFiles();
this.updateFileSelector();
} catch (error) {
console.error('加载模型文件失败:', error);
// 如果请求失败可能是API不存在等待一段时间后重试
const retryLoadFiles = async () => {
try {
console.log('尝试重新加载模型文件列表...');
this.modelFiles = await apiLoadModelFiles();
this.updateFileSelector();
} 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 content = await apiLoadFileContent(filePath);
this.selectedFile = filePath;
this.xmlContent = content;
// 解析XML内容
const { xmlDoc, error, basicXml } = parseXmlContent(content);
if (error && !basicXml) {
// 显示错误信息
const configContent = this.shadowRoot.querySelector('.config-content');
configContent.innerHTML = `
<div class="error-message">
<h3>XML解析错误</h3>
<p>文件内容不是有效的XML格式。</p>
<pre>${escapeHtml(content)}</pre>
</div>
`;
return;
}
this.xmlDoc = xmlDoc;
if (basicXml) {
this.xmlContent = basicXml;
}
// 特殊处理CommandList元素确保命令以属性方式存储
ensureCommandListFormat(this.xmlDoc);
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>${getStyles()}</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', async (e) => {
const filePath = e.target.value;
if (filePath) {
// 检查是否需要保存当前文件
if (await this.checkSaveNeeded()) {
this.loadFileContent(filePath);
} else {
// 如果用户取消或保存失败,恢复选择器的值
fileSelector.value = this.selectedFile || '';
}
}
});
// 刷新文件列表
const refreshButton = this.shadowRoot.getElementById('refreshFiles');
refreshButton.addEventListener('click', async () => {
// 检查是否需要保存当前文件
if (await this.checkSaveNeeded()) {
refreshButton.classList.add('refreshing');
try {
// 重新加载文件列表
await this.loadModelFiles();
// 清除编辑状态和内容
this.resetEditState();
// 清空内容区域
this.xmlContent = '';
this.xmlDoc = null;
const contentArea = this.shadowRoot.querySelector('#contentArea');
if (contentArea) {
contentArea.innerHTML = `<div class="no-file-selected">请选择一个模型配置文件查看内容</div>`;
}
// 清空文件选择
const fileSelector = this.shadowRoot.getElementById('fileSelector');
if (fileSelector) {
fileSelector.value = '';
}
// 重置选中的文件
this.selectedFile = null;
} catch (error) {
console.error('刷新文件列表失败:', error);
alert('刷新文件列表失败: ' + error.message);
} finally {
// 无论成功失败,都移除刷新动画
setTimeout(() => {
refreshButton.classList.remove('refreshing');
}, 500);
}
}
// 如果用户点击取消,什么也不做,保留当前内容
});
// 新建配置
const newButton = this.shadowRoot.getElementById('newConfig');
newButton.addEventListener('click', async () => {
// 检查是否需要保存当前文件
if (await this.checkSaveNeeded()) {
showNewConfigDialog(async (result) => {
await this.loadModelFiles();
await this.loadFileContent(result.path);
}, this);
}
});
// 保存配置
const saveButton = this.shadowRoot.getElementById('saveConfig');
saveButton.addEventListener('click', async () => {
if (!this.selectedFile) {
alert('请先选择一个文件或创建新文件');
return;
}
await this.saveCurrentFile();
});
// 另存为
const saveAsButton = this.shadowRoot.getElementById('saveAsConfig');
saveAsButton.addEventListener('click', () => {
if (!this.xmlContent) {
alert('没有内容可保存');
return;
}
showSaveAsDialog(this.xmlContent, this.selectedFile, async (result) => {
this.selectedFile = result.path;
this.resetEditState();
await this.loadModelFiles();
}, this);
});
}
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);
// 渲染基本信息部分
renderBasicInfoSection(
visualEditor,
rootElement,
xmlDoc,
(inputElement) => this.showDateTimeDialogWrapper(inputElement),
() => this.markEdited()
);
// 渲染高级设置部分
renderAdvancedSection(
visualEditor,
rootElement,
xmlDoc,
(container, rootElement) => this.renderCommandListTableWrapper(container, rootElement),
() => this.markEdited()
);
// 渲染其他设置部分
renderOtherSettingsSection(
visualEditor,
rootElement,
(container, parentElement, parentPath, level = 0) => this.renderElementsWrapper(
container, parentElement, parentPath, level
)
);
} 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(); // 标记已编辑
});
});
}
// 显示日期时间对话框包装方法
showDateTimeDialogWrapper(inputElement) {
showDateTimeDialog(inputElement, this.xmlDoc, () => this.markEdited(), this);
}
// 渲染命令列表表格包装方法
renderCommandListTableWrapper(container, rootElement) {
renderCommandListTable(
container,
rootElement,
(rootElement) => this.showAddCommandDialogWrapper(rootElement),
(command, index) => this.showEditCommandDialogWrapper(command, index),
(command, index) => this.deleteCommandWrapper(command, index)
);
}
// 渲染元素包装方法
renderElementsWrapper(container, parentElement, parentPath, level = 0) {
renderElements(
container,
parentElement,
parentPath,
level,
isValidElementName,
(path, value) => this.updateElementByPathWrapper(path, value),
(element, elementPath) => this.deleteElementWrapper(element, elementPath),
(parentElement, name, value) => this.addElementWrapper(parentElement, name, value),
(parentElement, parentPath) => this.showAddElementDialogWrapper(parentElement, parentPath),
(element, elementPath) => this.showEditElementDialogWrapper(element, elementPath)
);
}
// 更新元素路径包装方法
updateElementByPathWrapper(path, value) {
if (updateElementByPath(this.xmlDoc, path, value)) {
this.updateXmlFromVisualEditor();
this.markEdited();
return true;
}
return false;
}
// 删除元素包装方法
deleteElementWrapper(element, elementPath) {
// 确认删除
if (!confirm(`确定要删除元素 ${element.nodeName} 吗?此操作不可撤销。`)) {
return;
}
if (deleteElement(element)) {
this.updateXmlFromVisualEditor();
this.markEdited();
// 重新渲染元素列表
const container = this.shadowRoot.querySelector('#otherSettingsContainer');
if (container) {
container.innerHTML = '';
this.renderElementsWrapper(container, this.xmlDoc.documentElement, '/' + this.xmlDoc.documentElement.nodeName);
}
return true;
}
return false;
}
// 添加元素包装方法
addElementWrapper(parentElement, name, value) {
const newElement = addElement(this.xmlDoc, parentElement, name, value);
if (newElement) {
this.updateXmlFromVisualEditor();
this.markEdited();
return true;
}
return false;
}
// 显示添加元素对话框包装方法
showAddElementDialogWrapper(parentElement, parentPath) {
showAddElementDialog(parentElement, parentPath, this.xmlDoc, () => {
this.updateXmlFromVisualEditor();
this.markEdited();
// 重新渲染元素列表
const container = this.shadowRoot.querySelector('#otherSettingsContainer');
if (container) {
container.innerHTML = '';
this.renderElementsWrapper(container, this.xmlDoc.documentElement, '/' + this.xmlDoc.documentElement.nodeName);
}
}, this);
}
// 显示编辑元素对话框包装方法
showEditElementDialogWrapper(element, elementPath) {
showEditElementDialog(element, elementPath, this.xmlDoc, () => {
this.updateXmlFromVisualEditor();
this.markEdited();
// 更新UI中的值
const inputElement = this.shadowRoot.querySelector(`input[data-path="${elementPath}"]`);
if (inputElement) {
inputElement.value = element.textContent;
}
}, this);
}
// 显示添加命令对话框包装方法
showAddCommandDialogWrapper(rootElement) {
showAddCommandDialog(rootElement, this.xmlDoc, () => {
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);
}
}, this);
}
// 显示编辑命令对话框包装方法
showEditCommandDialogWrapper(commandElement, index) {
showEditCommandDialog(commandElement, index, () => {
this.updateXmlFromVisualEditor();
this.markEdited();
// 更新表格行
const row = this.shadowRoot.querySelector(`.command-row[data-index="${index}"]`);
if (row) {
const name = commandElement.getAttribute('Name') || '';
const call = commandElement.getAttribute('Call') || '';
const description = commandElement.getAttribute('Description') || '';
row.querySelectorAll('.command-cell')[0].textContent = name;
row.querySelectorAll('.command-cell')[1].textContent = call;
row.querySelectorAll('.command-cell')[2].textContent = description || `${name}描述`;
} else {
// 如果找不到行,重新渲染整个编辑器
const visualEditor = this.shadowRoot.querySelector('#visualEditor');
if (visualEditor) {
visualEditor.innerHTML = '';
this.renderVisualEditor(visualEditor, this.xmlDoc);
}
}
}, this);
}
// 删除命令包装方法
deleteCommandWrapper(commandElement, index) {
if (!confirm('确定要删除此命令吗?此操作不可撤销。')) {
return;
}
if (deleteElement(commandElement)) {
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);
}
}
return true;
}
return false;
}
// 从可视化编辑器更新XML
updateXmlFromVisualEditor() {
if (!this.xmlDoc) return;
this.xmlContent = updateXmlFromVisualEditor(this.xmlDoc);
}
// 标记为已编辑
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');
}
}
// 更新命令列表部分
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 = ensureCommandListFormat(this.xmlDoc);
// 渲染命令列表
renderCommandList(
visualEditorContainer,
commandListElement,
rootElement,
(rootElement) => this.showAddCommandDialogWrapper(rootElement),
(command, index) => this.showEditCommandDialogWrapper(command, index),
(command, index) => this.deleteCommandWrapper(command, index)
);
}
// 添加检查未保存更改的方法
async checkSaveNeeded() {
if (this.isEdited) {
// 使用自定义对话框替代简单的confirm
const dialogResult = await new Promise(resolve => {
// 创建模态框
const modal = document.createElement('div');
modal.className = 'modal';
modal.id = 'saveConfirmModal';
modal.innerHTML = `
<style>
.modal {
display: block;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.4);
}
.modal-content {
background-color: #fefefe;
margin: 15% auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
max-width: 400px;
border-radius: 5px;
position: relative;
text-align: center;
}
.modal-title {
margin-top: 0;
color: #333;
font-size: 18px;
margin-bottom: 20px;
}
.modal-buttons {
display: flex;
justify-content: center;
gap: 10px;
margin-top: 20px;
}
.btn {
padding: 8px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
min-width: 80px;
}
.btn-save {
background-color: #7986E7;
color: white;
}
.btn-save:hover {
background-color: #6875D6;
}
.btn-dont-save {
background-color: #E77979;
color: white;
}
.btn-dont-save:hover {
background-color: #D66868;
}
.btn-cancel {
background-color: #f5f5f5;
color: #333;
}
.btn-cancel:hover {
background-color: #e0e0e0;
}
</style>
<div class="modal-content">
<h3 class="modal-title">当前文件有未保存的更改</h3>
<p>您想要保存这些更改吗?</p>
<div class="modal-buttons">
<button class="btn btn-save" id="saveBtn">保存</button>
<button class="btn btn-dont-save" id="dontSaveBtn">不保存</button>
<button class="btn btn-cancel" id="cancelBtn">取消</button>
</div>
</div>
`;
document.body.appendChild(modal);
// 添加事件监听
const saveBtn = modal.querySelector('#saveBtn');
const dontSaveBtn = modal.querySelector('#dontSaveBtn');
const cancelBtn = modal.querySelector('#cancelBtn');
const closeModal = () => {
document.body.removeChild(modal);
};
saveBtn.addEventListener('click', () => {
closeModal();
resolve('save');
});
dontSaveBtn.addEventListener('click', () => {
closeModal();
resolve('dont-save');
});
cancelBtn.addEventListener('click', () => {
closeModal();
resolve('cancel');
});
// 点击模态窗口外部也取消
modal.addEventListener('click', (event) => {
if (event.target === modal) {
closeModal();
resolve('cancel');
}
});
});
// 根据对话框结果执行相应操作
if (dialogResult === 'save') {
try {
await this.saveCurrentFile();
return true; // 继续执行后续操作
} catch (error) {
console.error('保存出错:', error);
return false; // 保存失败,不继续执行
}
} else if (dialogResult === 'dont-save') {
// 不保存,但继续执行后续操作
return true;
} else {
// 用户取消,不执行后续操作
return false;
}
}
// 没有编辑状态直接返回true允许继续操作
return true;
}
// 检查未保存更改 (兼容旧版的使用)
checkUnsavedChanges() {
// 由于无法同步返回Promise结果这个方法保留为返回布尔值
// 但为了向前兼容,仍保留这个方法
return this.isEdited;
}
// 保存当前文件
async saveCurrentFile() {
if (!this.selectedFile || !this.isEdited) return false;
try {
await apiSaveFileContent(this.selectedFile, this.xmlContent);
this.resetEditState();
return true;
} catch (error) {
alert('保存失败: ' + error.message);
return false;
}
}
}
customElements.define('model-config', ModelConfig);