XNSim/XNSimHtml/components/configuration-config.js
2025-05-28 16:20:01 +08:00

1977 lines
90 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 ConfigurationConfig
* @classdesc 构型配置开发组件,采用卡片风格,展示机型信息和构型信息。
* @extends HTMLElement
*/
class ConfigurationConfig extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.currentView = 'planes'; // 'planes' 或 'configurations' 或 'editor'
this.selectedPlane = null;
this.selectedConfiguration = null;
this.userAccessLevel = 0; // 添加用户权限级别属性
/**
* 初始化DOM结构采用卡片风格展示机型信息和构型信息。
*/
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
padding: 20px;
color: #333;
box-sizing: border-box;
height: calc(100vh - 40px);
overflow: hidden;
}
.page-header {
margin-bottom: 20px;
}
.page-title {
color: #5c6bc0;
font-weight: 500;
font-size: 24px;
margin-bottom: 8px;
}
.back-section {
margin-bottom: 15px;
}
.back-button {
display: inline-flex;
align-items: center;
cursor: pointer;
color: #5c6bc0;
font-size: 14px;
text-decoration: none;
}
.back-button:hover {
color: #3949ab;
}
.back-icon {
margin-right: 5px;
font-size: 16px;
}
.config-dev-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
max-height: calc(100vh - 160px);
overflow-y: auto;
padding-right: 10px;
padding-top: 10px;
padding-bottom: 30px;
width: 100%;
}
.config-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 20px;
transition: transform 0.3s ease, box-shadow 0.3s ease;
cursor: pointer;
min-height: 280px;
display: flex;
flex-direction: column;
align-items: center;
overflow: hidden;
}
.config-card:hover {
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15);
}
.icon-container {
width: 160px;
height: 160px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
flex-shrink: 0;
}
.plane-icon {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.text-container {
width: 100%;
text-align: center;
flex-grow: 1;
}
.config-title {
font-size: 22px;
font-weight: 600;
margin-bottom: 12px;
color: #333;
height: 26px;
line-height: 26px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.config-desc {
font-size: 16px;
color: #666;
text-align: center;
line-height: 1.4;
padding: 0 10px;
}
.loading {
text-align: center;
padding: 20px;
color: #666;
}
.error {
color: #f44336;
text-align: center;
padding: 20px;
}
.config-info {
font-size: 14px;
color: #666;
margin-top: 8px;
}
/* 构型卡片特定样式 */
.config-dev-container.configurations {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
}
.config-card.configuration {
min-height: auto;
height: 80px;
padding: 0 20px;
flex-direction: row;
justify-content: space-between;
align-items: center;
position: relative;
}
.config-card.configuration .config-title {
font-size: 18px;
margin: 0;
height: auto;
flex-grow: 1;
text-align: left;
}
.delete-button {
width: 24px;
height: 24px;
border: none;
background: none;
cursor: pointer;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
color: #999;
transition: color 0.3s;
margin-left: 10px;
}
.delete-button:hover {
color: #f44336;
}
.delete-button svg {
width: 20px;
height: 20px;
}
/* 新建构型卡片样式 */
.config-card.new-config {
border: 2px dashed #5c6bc0;
background: #f8f9ff;
box-shadow: none;
min-height: auto;
height: 80px;
padding: 0 20px;
flex-direction: row;
justify-content: center;
}
.config-card.new-config:hover {
background: #f0f2ff;
box-shadow: none;
}
.config-card.new-config .config-title {
color: #5c6bc0;
font-size: 18px;
margin: 0;
height: auto;
}
/* 构型编辑视图样式 */
.editor-container {
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 30px;
height: calc(100vh - 160px);
overflow-y: auto;
max-width: 1600px;
margin: 0 auto;
box-sizing: border-box;
display: flex;
gap: 30px;
}
.editor-main {
flex: 1;
overflow-y: auto;
min-width: 0; /* 防止内容溢出 */
padding-right: 30px;
border-right: 1px solid #e8eaf6;
}
.editor-sidebar {
width: 300px;
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 20px;
position: sticky;
top: 30px;
height: fit-content;
}
.status-card {
background: #f8f9ff;
border: 1px solid #e8eaf6;
border-radius: 8px;
padding: 20px;
}
.status-card-title {
font-size: 16px;
font-weight: 500;
color: #5c6bc0;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 8px;
}
.status-card-icon {
width: 24px;
height: 24px;
color: #5c6bc0;
}
.status-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.status-item {
background: white;
border: 1px solid #e8eaf6;
border-radius: 4px;
padding: 10px;
font-size: 14px;
color: #333;
}
.status-item.empty {
color: #999;
font-style: italic;
}
.section {
margin-bottom: 30px;
padding-right: 10px;
}
.section-title {
font-size: 18px;
font-weight: 500;
color: #5c6bc0;
margin-bottom: 15px;
padding-bottom: 8px;
border-bottom: 2px solid #e8eaf6;
}
.form-group {
margin-bottom: 15px;
}
.form-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 25px;
margin-bottom: 15px;
}
.form-row-2 {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 30px;
margin-bottom: 15px;
}
.form-row-3 {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 25px;
margin-bottom: 15px;
}
.form-label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #555;
}
.form-input {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
.form-input:focus {
border-color: #5c6bc0;
outline: none;
box-shadow: 0 0 0 2px rgba(92, 107, 192, 0.2);
}
.form-input[readonly] {
background-color: #f5f5f5;
border-color: #e0e0e0;
color: #666;
cursor: not-allowed;
}
.form-input[readonly]:focus {
border-color: #e0e0e0;
box-shadow: none;
}
.form-textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
min-height: 100px;
resize: vertical;
}
.form-textarea:focus {
border-color: #5c6bc0;
outline: none;
box-shadow: 0 0 0 2px rgba(92, 107, 192, 0.2);
}
.checkbox-group {
display: flex;
gap: 20px;
margin-top: 10px;
}
.checkbox-item {
display: flex;
align-items: center;
gap: 5px;
}
.checkbox-item input[type="checkbox"] {
width: 16px;
height: 16px;
}
.checkbox-item label {
font-size: 14px;
color: #555;
}
.button-group {
display: flex;
gap: 10px;
margin-top: 20px;
}
.button {
padding: 8px 16px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.3s;
}
.button-primary {
background-color: #5c6bc0;
color: white;
}
.button-primary:hover {
background-color: #3949ab;
}
.button-secondary {
background-color: #e8eaf6;
color: #5c6bc0;
}
.button-secondary:hover {
background-color: #c5cae9;
}
.log-section {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 30px;
}
.log-box {
background: #f5f5f5;
border-radius: 8px;
padding: 20px;
}
.log-box-title {
font-size: 16px;
font-weight: 500;
color: #5c6bc0;
margin-bottom: 15px;
}
</style>
<div class="config-development">
<div class="page-header">
<h2 class="page-title" id="pageTitle">机型</h2>
<div class="back-section" id="backSection" style="display: none;">
<a class="back-button" id="backButton">
<span class="back-icon">←</span>
<span>返回机型列表</span>
</a>
</div>
</div>
<div class="config-dev-container" id="contentContainer">
<div class="loading">加载中...</div>
</div>
<div class="editor-container" id="editorContainer" style="display: none;">
<div class="editor-main">
<div class="section">
<h3 class="section-title">基本信息</h3>
<div class="form-row-3">
<div class="form-group">
<label class="form-label">操作系统</label>
<input type="text" class="form-input" id="osName" />
</div>
<div class="form-group">
<label class="form-label">系统版本</label>
<input type="text" class="form-input" id="osVersion" />
</div>
<div class="form-group">
<label class="form-label">RTX版本</label>
<input type="text" class="form-input" id="rtxVersion" />
</div>
</div>
</div>
<div class="section">
<h3 class="section-title">系统配置</h3>
<div class="form-row-3">
<div class="form-group">
<label class="form-label">CPU亲和性</label>
<input type="text" class="form-input" id="cpuAffinity" />
</div>
<div class="form-group">
<label class="form-label">域ID</label>
<input type="text" class="form-input" id="domainId" />
</div>
<div class="form-group">
<label class="form-label">工作路径</label>
<input type="text" class="form-input" id="workPath" />
</div>
</div>
<div class="form-row-3">
<div class="form-group">
<label class="form-label">模型路径</label>
<input type="text" class="form-input" id="modelsPath" />
</div>
<div class="form-group">
<label class="form-label">服务路径</label>
<input type="text" class="form-input" id="servicesPath" />
</div>
</div>
</div>
<div class="section">
<h3 class="section-title">输出配置</h3>
<div class="log-section">
<div class="log-box">
<div class="log-box-title">控制台输出配置</div>
<div class="checkbox-group">
<div class="checkbox-item">
<input type="checkbox" id="consoleDebug" />
<label for="consoleDebug">Debug</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="consoleInfo" />
<label for="consoleInfo">Info</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="consoleWarning" />
<label for="consoleWarning">Warning</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="consoleError" />
<label for="consoleError">Error</label>
</div>
</div>
</div>
<div class="log-box">
<div class="log-box-title">日志输出配置</div>
<div class="checkbox-group">
<div class="checkbox-item">
<input type="checkbox" id="logDebug" />
<label for="logDebug">Debug</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="logInfo" />
<label for="logInfo">Info</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="logWarning" />
<label for="logWarning">Warning</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="logError" />
<label for="logError">Error</label>
</div>
</div>
</div>
</div>
</div>
<div class="button-group">
<button class="button button-primary" id="saveButton">保存构型</button>
<button class="button button-secondary" id="cancelButton">取消</button>
</div>
</div>
<div class="editor-sidebar">
<div class="status-card">
<div class="status-card-title" style="position:relative;">
加载模型
<img src="/assets/icons/png/plus_b.png" alt="添加模型组" title="添加模型组" id="addModelGroupBtn" style="position:absolute;top:0;right:0;width:20px;height:20px;cursor:pointer;z-index:2;" />
</div>
<div class="status-list" id="pendingModelsList">
<div class="status-item empty">暂无待加载模型</div>
</div>
</div>
<div class="status-card">
<div class="status-card-title">
加载服务
</div>
<div class="status-list" id="pendingServicesList">
<div class="status-item empty">暂无待加载服务</div>
</div>
</div>
</div>
</div>
</div>
`;
// 绑定事件
this.shadowRoot.getElementById('backButton').addEventListener('click', (e) => {
e.preventDefault();
if (this.currentView === 'editor') {
this.showConfigurations(this.selectedPlane);
} else {
this.showPlanes();
}
});
// 绑定编辑器按钮事件
this.shadowRoot.getElementById('saveButton').addEventListener('click', () => this.saveConfiguration());
this.shadowRoot.getElementById('cancelButton').addEventListener('click', () => {
this.showConfigurations(this.selectedPlane);
});
// 添加输入验证
this.shadowRoot.getElementById('cpuAffinity').addEventListener('input', (e) => {
// 只允许数字和英文逗号
e.target.value = e.target.value.replace(/[^0-9,]/g, '');
});
this.shadowRoot.getElementById('domainId').addEventListener('input', (e) => {
// 只允许数字
e.target.value = e.target.value.replace(/[^0-9]/g, '');
// 限制范围在0-232之间
const value = parseInt(e.target.value);
if (value > 232) {
e.target.value = '232';
}
});
// 获取机型数据
this.loadPlanes();
// 渲染sidebar后绑定添加模型组按钮事件
const addModelGroupBtn = this.shadowRoot.getElementById('addModelGroupBtn');
if (addModelGroupBtn) {
addModelGroupBtn.onclick = () => this.showAddModelGroupDialog();
}
}
/**
* @method loadPlanes
* @description 从服务器获取机型数据并更新UI
*/
async loadPlanes() {
try {
const response = await fetch('/api/planes');
if (!response.ok) {
throw new Error('获取机型数据失败');
}
const planes = await response.json();
this.renderPlanes(planes);
} catch (error) {
console.error('加载机型数据失败:', error);
this.showError('加载机型数据失败,请稍后重试');
}
}
/**
* @method loadConfigurations
* @description 从服务器获取构型数据并更新UI
* @param {string} planeName - 机型名称
*/
async loadConfigurations(planeName) {
try {
const response = await fetch(`/api/configurations?plane=${encodeURIComponent(planeName)}`);
if (!response.ok) {
throw new Error('获取构型数据失败');
}
const configs = await response.json();
this.renderConfigurations(configs, planeName);
} catch (error) {
console.error('加载构型数据失败:', error);
this.showError('加载构型数据失败,请稍后重试');
}
}
/**
* @method renderPlanes
* @description 渲染机型卡片
* @param {Array} planes - 机型数据数组
*/
renderPlanes(planes) {
const container = this.shadowRoot.getElementById('contentContainer');
if (!planes || planes.length === 0) {
container.innerHTML = '<div class="error">暂无机型数据</div>';
return;
}
container.innerHTML = planes.map(plane => `
<div class="config-card" data-plane="${plane.PlaneName}">
<div class="icon-container">
${plane.Icon ? `<img class="plane-icon" src="${plane.Icon}" alt="${plane.PlaneName}">` : ''}
</div>
<div class="text-container">
<div class="config-title">${plane.PlaneName || '未知机型'}</div>
<div class="config-desc">${plane.Description || '暂无描述'}</div>
</div>
</div>
`).join('');
// 添加点击事件
container.querySelectorAll('.config-card').forEach(card => {
card.addEventListener('click', () => {
const planeName = card.dataset.plane;
this.showConfigurations(planeName);
});
});
}
/**
* @method showBaseConfigDialog
* @description 显示基准构型选择对话框
* @param {Array} configs - 构型数据数组
* @param {string} planeName - 机型名称
* @returns {Promise<Object|undefined>} 返回选择的基准构型或undefined表示不使用基准构型
*/
showBaseConfigDialog(configs, planeName) {
return new Promise((resolve) => {
const dialog = document.createElement('div');
dialog.className = 'base-config-dialog';
dialog.innerHTML = `
<style>
.base-config-dialog {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.dialog-content {
background: white;
padding: 20px;
border-radius: 8px;
width: 400px;
max-width: 90%;
}
.dialog-title {
font-size: 18px;
font-weight: 500;
color: #333;
margin-bottom: 15px;
}
.select-container {
margin-bottom: 20px;
}
.select-label {
display: block;
margin-bottom: 8px;
color: #555;
font-size: 14px;
}
.select-input {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
color: #333;
background-color: white;
box-sizing: border-box;
}
.select-input:focus {
border-color: #5c6bc0;
outline: none;
box-shadow: 0 0 0 2px rgba(92, 107, 192, 0.2);
}
.select-input[type="text"] {
cursor: text;
}
.select-input[type="text"]::placeholder {
color: #999;
}
.select-input[type="select"] {
cursor: pointer;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 8px center;
background-size: 16px;
}
.dialog-buttons {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.dialog-button {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.button-primary {
background: #5c6bc0;
color: white;
}
.button-secondary {
background: #e8eaf6;
color: #5c6bc0;
}
</style>
<div class="dialog-content">
<div class="dialog-title">新建构型</div>
<div class="select-container">
<label class="select-label">构型名称:</label>
<input type="text" class="select-input" id="newConfName" placeholder="请输入构型名称" />
</div>
<div class="select-container">
<label class="select-label">请选择基准构型:</label>
<select class="select-input" id="baseConfigSelect">
<option value="none">不使用基准构型</option>
${configs.map(config => `
<option value="${config.ConfID}">${config.ConfName}</option>
`).join('')}
</select>
</div>
<div class="dialog-buttons">
<button class="dialog-button button-secondary" id="cancelButton">取消</button>
<button class="dialog-button button-primary" id="confirmButton">确认</button>
</div>
</div>
`;
const select = dialog.querySelector('#baseConfigSelect');
const newConfName = dialog.querySelector('#newConfName');
dialog.querySelector('#cancelButton').addEventListener('click', () => {
document.body.removeChild(dialog);
resolve(null); // 取消时返回null
});
dialog.querySelector('#confirmButton').addEventListener('click', () => {
const selectedValue = select.value;
const confName = newConfName.value.trim();
if (!confName) {
alert('请输入构型名称');
return;
}
const selectedConfig = selectedValue === 'none' ?
undefined : // 选择"不使用基准构型"时返回undefined
configs.find(c => c.ConfID === parseInt(selectedValue));
document.body.removeChild(dialog);
resolve({
...selectedConfig,
ConfName: confName
});
});
document.body.appendChild(dialog);
});
}
/**
* @method checkUserAccessLevel
* @description 检查用户访问权限级别
*/
async checkUserAccessLevel() {
try {
const response = await fetch('/api/check-auth', {
credentials: 'include'
});
const result = await response.json();
if (result.success) {
this.userAccessLevel = parseInt(result.user.access_level);
// 重新渲染构型列表以更新删除按钮的显示状态
if (this.currentView === 'configurations' && this.selectedPlane) {
this.loadConfigurations(this.selectedPlane);
}
}
} catch (error) {
console.error('获取用户权限失败:', error);
}
}
/**
* @method renderConfigurations
* @description 渲染构型卡片
* @param {Array} configs - 构型数据数组
* @param {string} planeName - 机型名称
*/
renderConfigurations(configs, planeName) {
const container = this.shadowRoot.getElementById('contentContainer');
if (!configs || configs.length === 0) {
container.innerHTML = '<div class="error">暂无构型数据</div>';
return;
}
// 添加新建构型卡片(根据权限显示)
container.innerHTML = `
${this.userAccessLevel >= 2 ? `
<div class="config-card new-config" id="newConfigCard">
<div class="config-title">+ 新建构型</div>
</div>
` : ''}
${configs.map(config => `
<div class="config-card configuration" data-config-id="${config.ConfID}">
<div class="config-title">${config.ConfName || '未知构型'}</div>
${this.userAccessLevel >= 2 ? `
<button class="delete-button" data-config-id="${config.ConfID}" title="删除构型">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
` : ''}
</div>
`).join('')}
`;
// 添加新建构型卡片点击事件(仅在权限足够时添加)
if (this.userAccessLevel >= 2) {
container.querySelector('#newConfigCard')?.addEventListener('click', async () => {
const baseConfig = await this.showBaseConfigDialog(configs, planeName);
if (baseConfig !== null) {
const defaultConfig = {
ConfName: '',
OSName: baseConfig?.OSName || '',
OSVersion: baseConfig?.OSVersion || '',
RTXVersion: baseConfig?.RTXVersion || '',
CPUAffinity: baseConfig?.CPUAffinity || '',
DomainID: baseConfig?.DomainID || '',
WorkPath: baseConfig?.WorkPath || '',
ModelsPath: baseConfig?.ModelsPath || '',
ServicesPath: baseConfig?.ServicesPath || '',
ConsoleDebug: baseConfig?.ConsoleDebug ?? 1,
ConsoleInfo: baseConfig?.ConsoleInfo ?? 1,
ConsoleWarning: baseConfig?.ConsoleWarning ?? 1,
ConsoleError: baseConfig?.ConsoleError ?? 1,
LogDebug: baseConfig?.LogDebug ?? 0,
LogInfo: baseConfig?.LogInfo ?? 1,
LogWarning: baseConfig?.LogWarning ?? 1,
LogError: baseConfig?.LogError ?? 1,
sourceConfId: baseConfig?.ConfID // 添加源构型ID
};
this.showEditor(defaultConfig, planeName, true);
}
});
}
// 添加构型卡片点击事件
container.querySelectorAll('.config-card:not(.new-config)').forEach(card => {
card.addEventListener('click', (e) => {
// 如果点击的是删除按钮,不触发编辑
if (e.target.closest('.delete-button')) {
return;
}
const configId = parseInt(card.dataset.configId, 10);
const config = configs.find(c => c.ConfID === configId);
if (config) {
this.showEditor(config, planeName, false);
}
});
});
// 添加删除按钮点击事件
container.querySelectorAll('.delete-button').forEach(button => {
button.addEventListener('click', async (e) => {
e.stopPropagation(); // 阻止事件冒泡
// 再次检查权限
if (this.userAccessLevel < 2) {
alert('您没有删除构型的权限');
return;
}
const configId = parseInt(button.dataset.configId, 10);
const config = configs.find(c => c.ConfID === configId);
if (config && confirm(`确定要删除构型"${config.ConfName}"吗?\n注意:删除后将无法恢复,且会同时删除该构型下的所有模型组、模型和服务。`)) {
try {
const response = await fetch(`/api/configurations/${configId}`, {
method: 'DELETE',
credentials: 'include'
});
const responseData = await response.json();
if (!response.ok) {
throw new Error(responseData.error || '删除构型失败');
}
// 重新加载构型列表
this.loadConfigurations(planeName);
} catch (error) {
console.error('删除构型失败:', error);
alert(error.message || '删除构型失败,请稍后重试');
}
}
});
});
}
/**
* @method showEditor
* @description 显示构型编辑器
* @param {Object} configuration - 构型数据
* @param {string} planeName - 机型名称
* @param {boolean} isNew - 是否是新建构型
*/
async showEditor(configuration, planeName, isNew) {
this.currentView = 'editor';
this.selectedConfiguration = configuration;
this.isNewConfiguration = isNew;
// 更新UI
const pageTitle = this.shadowRoot.getElementById('pageTitle');
const backSection = this.shadowRoot.getElementById('backSection');
const backButtonText = this.shadowRoot.querySelector('#backButton span:last-child');
const contentContainer = this.shadowRoot.getElementById('contentContainer');
const editorContainer = this.shadowRoot.getElementById('editorContainer');
if (!pageTitle || !backSection || !contentContainer || !editorContainer) {
return;
}
pageTitle.textContent = isNew ? `${planeName} - 新建构型` : `${planeName} - ${configuration.ConfName} 构型`;
backSection.style.display = 'block';
backButtonText.textContent = '返回构型列表';
contentContainer.style.display = 'none';
editorContainer.style.display = 'flex';
// 填充其他表单数据
this.shadowRoot.getElementById('osName').value = configuration.OSName || '';
this.shadowRoot.getElementById('osVersion').value = configuration.OSVersion || '';
this.shadowRoot.getElementById('rtxVersion').value = configuration.RTXVersion || '';
this.shadowRoot.getElementById('cpuAffinity').value = configuration.CPUAffinity || '';
this.shadowRoot.getElementById('domainId').value = configuration.DomainID !== undefined ? Math.floor(configuration.DomainID) : '';
this.shadowRoot.getElementById('modelsPath').value = configuration.ModelsPath || '';
this.shadowRoot.getElementById('servicesPath').value = configuration.ServicesPath || '';
this.shadowRoot.getElementById('workPath').value = configuration.WorkPath || '';
// 设置复选框状态
this.shadowRoot.getElementById('consoleDebug').checked = configuration.ConsoleDebug === 1;
this.shadowRoot.getElementById('consoleInfo').checked = configuration.ConsoleInfo === 1;
this.shadowRoot.getElementById('consoleWarning').checked = configuration.ConsoleWarning === 1;
this.shadowRoot.getElementById('consoleError').checked = configuration.ConsoleError === 1;
this.shadowRoot.getElementById('logDebug').checked = configuration.LogDebug === 1;
this.shadowRoot.getElementById('logInfo').checked = configuration.LogInfo === 1;
this.shadowRoot.getElementById('logWarning').checked = configuration.LogWarning === 1;
this.shadowRoot.getElementById('logError').checked = configuration.LogError === 1;
// 在加载模型卡片右上角增加添加模型组按钮
const modelCard = this.shadowRoot.querySelectorAll('.editor-sidebar .status-card')[0];
const modelCardTitle = modelCard.querySelector('.status-card-title');
if (modelCard && modelCardTitle && this.userAccessLevel >= 2) {
// 移除已存在的按钮(如果有)
const existingBtn = this.shadowRoot.getElementById('addModelGroupBtn');
if (existingBtn) {
existingBtn.remove();
}
// 添加新按钮
const addBtn = document.createElement('img');
addBtn.src = '/assets/icons/png/plus_b.png';
addBtn.alt = '添加模型组';
addBtn.title = '添加模型组';
addBtn.id = 'addModelGroupBtn';
addBtn.style.cssText = 'width:20px;height:20px;cursor:pointer;position:absolute;top:18px;right:18px;z-index:2;';
modelCard.style.position = 'relative';
modelCard.appendChild(addBtn);
addBtn.addEventListener('click', () => this.showAddModelGroupDialog());
}
// 在加载服务卡片右上角增加添加按钮
const serviceCard = this.shadowRoot.querySelectorAll('.editor-sidebar .status-card')[1];
const serviceCardTitle = serviceCard.querySelector('.status-card-title');
if (serviceCard && serviceCardTitle && this.userAccessLevel >= 2) {
// 移除已存在的按钮(如果有)
const existingBtn = this.shadowRoot.getElementById('addServiceBtn');
if (existingBtn) {
existingBtn.remove();
}
// 添加新按钮
const addBtn = document.createElement('img');
addBtn.src = '/assets/icons/png/plus_b.png';
addBtn.alt = '添加服务';
addBtn.title = '添加服务';
addBtn.id = 'addServiceBtn';
addBtn.style.cssText = 'width:20px;height:20px;cursor:pointer;position:absolute;top:18px;right:18px;z-index:2;';
serviceCard.style.position = 'relative';
serviceCard.appendChild(addBtn);
addBtn.addEventListener('click', () => this.showAddServiceDialog());
}
// 如果不是新建构型,获取模型组数据和服务列表
if (!isNew && configuration.ConfID) {
try {
// 并行请求模型组数据和服务列表
const [modelGroupsResponse, servicesResponse] = await Promise.all([
fetch(`/api/configurations/${configuration.ConfID}/model-groups`),
fetch(`/api/configurations/${configuration.ConfID}/services`)
]);
if (!modelGroupsResponse.ok) {
throw new Error('获取模型组数据失败');
}
const modelGroups = await modelGroupsResponse.json();
// 更新加载模型卡片
const pendingModelsList = this.shadowRoot.getElementById('pendingModelsList');
if (modelGroups && modelGroups.length > 0) {
// 获取每个模型组下的模型
const modelGroupsWithModels = await Promise.all(modelGroups.map(async (group) => {
try {
const modelsResponse = await fetch(`/api/model-groups/${group.GroupID}/models`);
if (!modelsResponse.ok) {
throw new Error(`获取模型组 ${group.GroupName} 的模型失败`);
}
const models = await modelsResponse.json();
return { ...group, models };
} catch (error) {
console.error(`获取模型组 ${group.GroupName} 的模型失败:`, error);
return { ...group, models: [] };
}
}));
pendingModelsList.innerHTML = modelGroupsWithModels.map(group => `
<div class="status-item" data-group-id="${group.GroupID}" style="position:relative;">
<div style="font-weight: 500; margin-bottom: 4px; display: flex; align-items: center; justify-content: space-between;">
<span>${group.GroupName}</span>
${this.userAccessLevel >= 2 ? `
<span style="display: flex; gap: 8px; align-items: center; position: absolute; right: 12px; top: 10px;">
<img src="/assets/icons/png/plus_b.png" alt="添加模型" title="添加模型" class="group-add-btn" style="width:18px;height:18px;cursor:pointer;" data-group-id="${group.GroupID}" />
<img src="/assets/icons/png/sliders_b.png" alt="编辑" title="编辑模型组" class="group-edit-btn" style="width:18px;height:18px;cursor:pointer;" data-group-id="${group.GroupID}" />
<img src="/assets/icons/png/delete_b.png" alt="删除" title="删除模型组" class="group-delete-btn" style="width:18px;height:18px;cursor:pointer;" data-group-id="${group.GroupID}" />
</span>
` : ''}
</div>
<div style="font-size: 12px; color: #666; margin-bottom: 8px;">
频率:${group.Freq || 0} Hz / 优先级:${group.Priority || 0} / 亲和性:${group.CPUAff || '无'}
</div>
<div style="margin-top: 8px; border-top: 1px solid #e8eaf6; padding-top: 8px;">
${group.models && group.models.length > 0 ?
group.models.map(model => `
<div style=\"margin-bottom: 6px; padding: 4px 8px; background: #f8f9ff; border-radius: 4px; display: flex; align-items: center; justify-content: space-between;\">
<div style=\"font-size: 14px;\">${model.ModelName || '未知模型'} ${model.ModelVersion || '未知版本'}</div>
${this.userAccessLevel >= 2 ? `
<img src="/assets/icons/png/delete_b.png" alt="删除" title="删除" class="model-delete-btn" style="width:20px;height:20px;cursor:pointer;margin-left:8px;" data-group-id="${group.GroupID}" data-class-name="${model.ClassName}" />
` : ''}
</div>
`).join('')
: '<div style=\"font-size: 12px; color: #999; font-style: italic;\">暂无模型</div>'
}
</div>
</div>
`).join('');
// 绑定模型组按钮事件
if (this.userAccessLevel >= 2) {
// 添加模型按钮
this.shadowRoot.querySelectorAll('.group-add-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const groupId = btn.getAttribute('data-group-id');
this.onAddModel && this.onAddModel(groupId);
});
});
// 编辑模型组按钮
this.shadowRoot.querySelectorAll('.group-edit-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const groupId = btn.getAttribute('data-group-id');
const group = modelGroupsWithModels.find(g => String(g.GroupID) === String(groupId));
if (group) this.showEditGroupDialog(group);
});
});
// 删除模型组按钮
this.shadowRoot.querySelectorAll('.group-delete-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
const groupId = btn.getAttribute('data-group-id');
if (!groupId) return;
if (!confirm('确定要删除该模型组吗?')) return;
try {
const resp = await fetch(`/api/model-groups/${groupId}`, {
method: 'DELETE',
credentials: 'include'
});
if (!resp.ok) {
const data = await resp.json();
throw new Error(data.error || '删除失败');
}
this.showEditor(this.selectedConfiguration, this.selectedPlane, false);
} catch (err) {
alert('删除失败:' + err.message);
}
});
});
// 删除模型按钮
this.shadowRoot.querySelectorAll('.model-delete-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
e.stopPropagation();
const groupId = btn.getAttribute('data-group-id');
const className = btn.getAttribute('data-class-name');
if (!groupId || !className) return;
if (!confirm('确定要删除该模型吗?')) return;
try {
const resp = await fetch(`/api/model-groups/${groupId}/models/${encodeURIComponent(className)}`, {
method: 'DELETE',
credentials: 'include'
});
if (!resp.ok) {
const data = await resp.json();
throw new Error(data.error || '删除失败');
}
this.showEditor(this.selectedConfiguration, this.selectedPlane, false);
} catch (err) {
alert('删除失败:' + err.message);
}
});
});
}
} else {
pendingModelsList.innerHTML = '<div class="status-item empty">暂无待加载模型</div>';
}
// 更新服务列表
const pendingServicesList = this.shadowRoot.getElementById('pendingServicesList');
if (!servicesResponse.ok) {
throw new Error('获取服务列表失败');
}
const services = await servicesResponse.json();
if (services && services.length > 0) {
pendingServicesList.innerHTML = services.map(service => `
<div class="status-item">
<div style="margin-bottom: 6px; padding: 4px 8px; background: #f8f9ff; border-radius: 4px; display: flex; align-items: center; justify-content: space-between;">
<div style="font-size: 14px;">${service.ServiceName || service.ClassName || '未知服务'} ${service.ServiceVersion || '未知'}</div>
${this.userAccessLevel >= 2 ? `
<img src="/assets/icons/png/delete_b.png" alt="删除" title="删除" class="service-delete-btn" style="width:20px;height:20px;cursor:pointer;margin-left:8px;" data-service-name="${service.ClassName}" />
` : ''}
</div>
</div>
`).join('');
// 绑定服务删除按钮事件
if (this.userAccessLevel >= 2) {
this.shadowRoot.querySelectorAll('.service-delete-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
e.stopPropagation();
const serviceName = btn.getAttribute('data-service-name');
if (!serviceName) return;
if (!confirm('确定要删除该服务吗?')) return;
try {
const resp = await fetch(`/api/configurations/${configuration.ConfID}/services/${encodeURIComponent(serviceName)}`, {
method: 'DELETE',
credentials: 'include'
});
if (!resp.ok) {
const data = await resp.json();
throw new Error(data.error || '删除失败');
}
this.showEditor(this.selectedConfiguration, this.selectedPlane, false);
} catch (err) {
alert('删除失败:' + err.message);
}
});
});
}
} else {
pendingServicesList.innerHTML = '<div class="status-item empty">暂无待加载服务</div>';
}
} catch (error) {
console.error('获取数据失败:', error);
const pendingModelsList = this.shadowRoot.getElementById('pendingModelsList');
const pendingServicesList = this.shadowRoot.getElementById('pendingServicesList');
pendingModelsList.innerHTML = '<div class="status-item empty">获取模型组数据失败</div>';
pendingServicesList.innerHTML = '<div class="status-item empty">获取服务列表失败</div>';
}
}
// 控制右侧卡片显示:新建构型时隐藏
const editorSidebar = this.shadowRoot.querySelector('.editor-sidebar');
if (isNew) {
editorSidebar.style.display = 'none';
} else {
editorSidebar.style.display = 'flex';
}
}
/**
* @method ensurePathEndsWithSlash
* @description 确保路径以斜杠结尾
* @param {string} path - 要处理的路径
* @returns {string} 处理后的路径
*/
ensurePathEndsWithSlash(path) {
if (!path) return path;
return path.endsWith('/') ? path : path + '/';
}
/**
* @method saveConfiguration
* @description 保存构型配置
*/
async saveConfiguration() {
// 检查权限
if (this.userAccessLevel < 2) {
alert('您没有保存构型的权限');
return;
}
// 验证域ID
const domainId = parseInt(this.shadowRoot.getElementById('domainId').value);
if (isNaN(domainId) || domainId < 0 || domainId > 232) {
alert('域ID必须在0-232之间');
return;
}
// 获取构型名称
const confName = this.selectedConfiguration.ConfName || '';
const updatedConfig = {
...this.selectedConfiguration,
ConfName: confName, // 保留构型名称
OSName: this.shadowRoot.getElementById('osName').value,
OSVersion: this.shadowRoot.getElementById('osVersion').value,
RTXVersion: this.shadowRoot.getElementById('rtxVersion').value,
CPUAffinity: this.shadowRoot.getElementById('cpuAffinity').value,
DomainID: domainId,
ModelsPath: this.ensurePathEndsWithSlash(this.shadowRoot.getElementById('modelsPath').value),
ServicesPath: this.ensurePathEndsWithSlash(this.shadowRoot.getElementById('servicesPath').value),
WorkPath: this.ensurePathEndsWithSlash(this.shadowRoot.getElementById('workPath').value),
ConsoleDebug: this.shadowRoot.getElementById('consoleDebug').checked ? 1 : 0,
ConsoleInfo: this.shadowRoot.getElementById('consoleInfo').checked ? 1 : 0,
ConsoleWarning: this.shadowRoot.getElementById('consoleWarning').checked ? 1 : 0,
ConsoleError: this.shadowRoot.getElementById('consoleError').checked ? 1 : 0,
LogDebug: this.shadowRoot.getElementById('logDebug').checked ? 1 : 0,
LogInfo: this.shadowRoot.getElementById('logInfo').checked ? 1 : 0,
LogWarning: this.shadowRoot.getElementById('logWarning').checked ? 1 : 0,
LogError: this.shadowRoot.getElementById('logError').checked ? 1 : 0
};
// 如果是新建构型保留源构型ID
if (this.isNewConfiguration && this.selectedConfiguration.sourceConfId) {
updatedConfig.sourceConfId = this.selectedConfiguration.sourceConfId;
}
try {
const url = this.isNewConfiguration ?
'/api/configurations' :
`/api/configurations/${this.selectedConfiguration.ConfID}`;
const method = this.isNewConfiguration ? 'POST' : 'PUT';
const response = await fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(updatedConfig),
credentials: 'include'
});
const responseData = await response.json();
if (!response.ok) {
if (responseData.error === '路径验证失败') {
const errorMessages = responseData.details.map(detail =>
`${detail.path === 'workPath' ? '工作路径' :
detail.path === 'modelsPath' ? '模型路径' : '服务路径'}: ${detail.error}`
).join('\n');
alert(`路径验证失败:\n${errorMessages}`);
} else {
throw new Error(responseData.error || '保存构型失败');
}
return;
}
this.selectedConfiguration = responseData;
this.showConfigurations(this.selectedPlane);
} catch (error) {
console.error('保存构型失败:', error);
alert(error.message || '保存构型失败,请稍后重试');
}
}
/**
* @method showConfigurations
* @description 显示指定机型的构型列表
* @param {string} planeName - 机型名称
*/
async showConfigurations(planeName) {
this.currentView = 'configurations';
this.selectedPlane = planeName;
// 更新UI
this.shadowRoot.getElementById('pageTitle').textContent = `${planeName} - 构型列表`;
this.shadowRoot.getElementById('backSection').style.display = 'block';
this.shadowRoot.querySelector('#backButton span:last-child').textContent = '返回机型列表';
this.shadowRoot.getElementById('contentContainer').classList.add('configurations');
this.shadowRoot.getElementById('contentContainer').style.display = 'grid';
this.shadowRoot.getElementById('editorContainer').style.display = 'none';
// 检查用户权限
await this.checkUserAccessLevel();
// 加载构型数据
this.loadConfigurations(planeName);
}
/**
* @method showPlanes
* @description 显示机型列表
*/
showPlanes() {
this.currentView = 'planes';
this.selectedPlane = null;
this.selectedConfiguration = null;
// 更新UI
this.shadowRoot.getElementById('pageTitle').textContent = '机型';
this.shadowRoot.getElementById('backSection').style.display = 'none';
this.shadowRoot.getElementById('contentContainer').classList.remove('configurations');
this.shadowRoot.getElementById('contentContainer').style.display = 'grid';
this.shadowRoot.getElementById('editorContainer').style.display = 'none';
// 重新加载机型数据
this.loadPlanes();
}
/**
* @method showError
* @description 显示错误信息
* @param {string} message - 错误信息
*/
showError(message) {
const container = this.shadowRoot.getElementById('contentContainer');
container.innerHTML = `<div class="error">${message}</div>`;
}
/**
* @method onAddModel
* @description 添加模型功能弹窗选择ATA章节、模型、版本确认后添加
* @param {string} groupId - 模型组ID
*/
async onAddModel(groupId) {
const planeName = this.selectedPlane;
const confID = this.selectedConfiguration.ConfID;
if (!planeName || !confID) {
alert('缺少机型或构型信息');
return;
}
// 创建弹窗
const dialog = document.createElement('div');
dialog.className = 'add-model-dialog';
dialog.innerHTML = `
<style>
.add-model-dialog { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.4); display: flex; align-items: center; justify-content: center; z-index: 1002; }
.add-model-content { background: #fff; border-radius: 8px; padding: 24px 32px; min-width: 340px; box-shadow: 0 2px 16px rgba(0,0,0,0.15); }
.add-model-title { font-size: 18px; font-weight: 500; margin-bottom: 18px; color: #333; }
.add-model-row { margin-bottom: 16px; }
.add-model-label { display: block; margin-bottom: 6px; color: #555; font-size: 14px; }
.add-model-select { width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; box-sizing: border-box; }
.add-model-buttons { display: flex; justify-content: flex-end; gap: 12px; margin-top: 10px; }
.add-model-btn { padding: 7px 18px; border: none; border-radius: 4px; font-size: 14px; cursor: pointer; }
.add-model-btn-primary { background: #5c6bc0; color: #fff; }
.add-model-btn-secondary { background: #e8eaf6; color: #5c6bc0; }
</style>
<div class="add-model-content">
<div class="add-model-title">添加模型</div>
<div class="add-model-row">
<label class="add-model-label">ATA章节</label>
<select class="add-model-select" id="ataSelect"><option value="">加载中...</option></select>
</div>
<div class="add-model-row">
<label class="add-model-label">模型</label>
<select class="add-model-select" id="modelSelect" disabled><option value="">请先选择章节</option></select>
</div>
<div class="add-model-row">
<label class="add-model-label">版本</label>
<select class="add-model-select" id="versionSelect" disabled><option value="">请先选择模型</option></select>
</div>
<div class="add-model-buttons">
<button class="add-model-btn add-model-btn-secondary" id="cancelAddModel">取消</button>
<button class="add-model-btn add-model-btn-primary" id="confirmAddModel">确认添加</button>
</div>
</div>
`;
document.body.appendChild(dialog);
// 关闭弹窗
dialog.querySelector('#cancelAddModel').onclick = () => document.body.removeChild(dialog);
// 获取ATA章节
const ataSelect = dialog.querySelector('#ataSelect');
const modelSelect = dialog.querySelector('#modelSelect');
const versionSelect = dialog.querySelector('#versionSelect');
let ataList = [], modelList = [], versionList = [];
try {
const ataResp = await fetch('/api/ata-chapters');
ataList = await ataResp.json();
ataSelect.innerHTML = '<option value="">请选择章节</option>' + ataList.map(a => `<option value="${a.ID}">${a.ID}</option>`).join('');
} catch {
ataSelect.innerHTML = '<option value="">加载失败</option>';
}
// 章节选择事件
ataSelect.onchange = async () => {
const chapterId = ataSelect.value;
modelSelect.disabled = true;
versionSelect.disabled = true;
modelSelect.innerHTML = '<option value="">加载中...</option>';
versionSelect.innerHTML = '<option value="">请先选择模型</option>';
if (!chapterId) {
modelSelect.innerHTML = '<option value="">请先选择章节</option>';
return;
}
try {
const modelResp = await fetch(`/api/chapter-models/${chapterId}?planeName=${encodeURIComponent(planeName)}`);
modelList = await modelResp.json();
modelSelect.innerHTML = '<option value="">请选择模型</option>' + modelList.map(m => `<option value="${m.ClassName}">${m.ModelName_CN}</option>`).join('');
modelSelect.disabled = false;
} catch {
modelSelect.innerHTML = '<option value="">加载失败</option>';
}
};
// 模型选择事件
modelSelect.onchange = async () => {
const className = modelSelect.value;
versionSelect.disabled = true;
versionSelect.innerHTML = '<option value="">加载中...</option>';
if (!className) {
versionSelect.innerHTML = '<option value="">请先选择模型</option>';
return;
}
try {
const versionResp = await fetch(`/api/model-versions/${className}?planeName=${encodeURIComponent(planeName)}&confID=${encodeURIComponent(confID)}`);
versionList = await versionResp.json();
versionSelect.innerHTML = '<option value="">请选择版本</option>' + versionList.map(v => `<option value="${v.Version}">${v.Version}</option>`).join('');
versionSelect.disabled = false;
} catch {
versionSelect.innerHTML = '<option value="">加载失败</option>';
}
};
// 确认添加
dialog.querySelector('#confirmAddModel').onclick = async () => {
const chapterId = ataSelect.value;
const className = modelSelect.value;
const version = versionSelect.value;
if (!chapterId || !className || !version) {
alert('请选择完整信息');
return;
}
// 获取选中模型的详细信息
const model = modelList.find(m => m.ClassName === className);
if (!model) {
alert('模型信息异常');
return;
}
// 组装要添加的数据
const postData = {
GroupID: groupId,
ClassName: className,
ModelName: model.ModelName,
ModelName_CN: model.ModelName_CN,
ModelVersion: version
};
try {
const resp = await fetch(`/api/model-groups/${groupId}/models`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(postData)
});
if (!resp.ok) {
const data = await resp.json();
throw new Error(data.error || '添加失败');
}
document.body.removeChild(dialog);
this.showEditor(this.selectedConfiguration, this.selectedPlane, false);
} catch (err) {
alert('添加失败:' + err.message);
}
};
}
/**
* @method showEditGroupDialog
* @description 显示模型组编辑对话框,编辑频率、优先级、亲和性
* @param {Object} group - 模型组对象
*/
showEditGroupDialog(group) {
const dialog = document.createElement('div');
dialog.className = 'edit-group-dialog';
dialog.innerHTML = `
<style>
.edit-group-dialog {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.4);
display: flex; align-items: center; justify-content: center;
z-index: 1001;
}
.edit-group-content {
background: #fff;
border-radius: 8px;
padding: 24px 32px;
min-width: 320px;
box-shadow: 0 2px 16px rgba(0,0,0,0.15);
}
.edit-group-title {
font-size: 18px;
font-weight: 500;
margin-bottom: 18px;
color: #333;
}
.edit-group-row {
margin-bottom: 16px;
}
.edit-group-label {
display: block;
margin-bottom: 6px;
color: #555;
font-size: 14px;
}
.edit-group-input {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
.edit-group-buttons {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 10px;
}
.edit-group-btn {
padding: 7px 18px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
}
.edit-group-btn-primary {
background: #5c6bc0;
color: #fff;
}
.edit-group-btn-secondary {
background: #e8eaf6;
color: #5c6bc0;
}
</style>
<div class="edit-group-content">
<div class="edit-group-title">编辑模型组参数</div>
<div class="edit-group-row">
<label class="edit-group-label">频率 (Hz)</label>
<input type="number" class="edit-group-input" id="editFreq" value="${group.Freq || 0}" min="0" />
</div>
<div class="edit-group-row">
<label class="edit-group-label">优先级</label>
<input type="number" class="edit-group-input" id="editPriority" value="${group.Priority || 0}" min="0" />
</div>
<div class="edit-group-row">
<label class="edit-group-label">亲和性</label>
<input type="text" class="edit-group-input" id="editCPUAff" value="${group.CPUAff || ''}" />
</div>
<div class="edit-group-buttons">
<button class="edit-group-btn edit-group-btn-secondary" id="cancelEditGroup">取消</button>
<button class="edit-group-btn edit-group-btn-primary" id="confirmEditGroup">保存</button>
</div>
</div>
`;
document.body.appendChild(dialog);
dialog.querySelector('#cancelEditGroup').onclick = () => document.body.removeChild(dialog);
dialog.querySelector('#confirmEditGroup').onclick = async () => {
const freq = parseInt(dialog.querySelector('#editFreq').value) || 0;
const priority = parseInt(dialog.querySelector('#editPriority').value) || 0;
const cpuAff = dialog.querySelector('#editCPUAff').value.trim();
try {
const resp = await fetch(`/api/model-groups/${group.GroupID}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ ...group, Freq: freq, Priority: priority, CPUAff: cpuAff })
});
if (!resp.ok) {
const data = await resp.json();
throw new Error(data.error || '保存失败');
}
document.body.removeChild(dialog);
this.showEditor(this.selectedConfiguration, this.selectedPlane, false);
} catch (err) {
alert('保存失败:' + err.message);
}
};
}
/**
* @method showAddModelGroupDialog
* @description 显示添加模型组对话框
*/
showAddModelGroupDialog() {
const cpuAffinityStr = this.shadowRoot.getElementById('cpuAffinity').value || '';
const cpuOptions = cpuAffinityStr.split(',').map(s => s.trim()).filter(s => s !== '' && !isNaN(Number(s)));
const dialog = document.createElement('div');
dialog.className = 'add-model-group-dialog';
dialog.innerHTML = `
<style>
.add-model-group-dialog {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.4);
display: flex; align-items: center; justify-content: center;
z-index: 1001;
}
.add-model-group-content {
background: #fff;
border-radius: 8px;
padding: 24px 32px;
min-width: 320px;
box-shadow: 0 2px 16px rgba(0,0,0,0.15);
}
.add-model-group-title {
font-size: 18px;
font-weight: 500;
margin-bottom: 18px;
color: #333;
}
.add-model-group-row {
margin-bottom: 16px;
}
.add-model-group-label {
display: block;
margin-bottom: 6px;
color: #555;
font-size: 14px;
}
.add-model-group-input, .add-model-group-multiselect {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
.add-model-group-multiselect { height: 80px; }
.add-model-group-selected {
margin-top: 6px;
font-size: 13px;
color: #5c6bc0;
min-height: 18px;
}
.add-model-group-buttons {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 10px;
}
.add-model-group-btn {
padding: 7px 18px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
}
.add-model-group-btn-primary {
background: #5c6bc0;
color: #fff;
}
.add-model-group-btn-secondary {
background: #e8eaf6;
color: #5c6bc0;
}
</style>
<div class="add-model-group-content">
<div class="add-model-group-title">添加模型组</div>
<div class="add-model-group-row">
<label class="add-model-group-label">模型组名称</label>
<input type="text" class="add-model-group-input" id="newModelGroupName" placeholder="请输入模型组名称" />
</div>
<div class="add-model-group-row">
<label class="add-model-group-label">运行频率 (Hz)</label>
<input type="number" step="0.01" class="add-model-group-input" id="freqInput" min="0.01" max="1000" placeholder="请输入频率" />
</div>
<div class="add-model-group-row">
<label class="add-model-group-label">优先级 (1-99)</label>
<input type="number" class="add-model-group-input" id="priorityInput" min="1" max="99" placeholder="请输入优先级" />
</div>
<div class="add-model-group-row">
<label class="add-model-group-label">亲和性</label>
<div style="position:relative;">
<input type="text" class="add-model-group-input" id="cpuAffInput" readonly placeholder="请选择" style="background:#fff;cursor:pointer;" />
<div id="cpuAffDropdown" style="display:none;position:absolute;z-index:10;top:38px;left:0;width:100%;background:#fff;border:1px solid #ddd;border-radius:4px;box-shadow:0 2px 8px rgba(0,0,0,0.08);max-height:150px;overflow:auto;">
${cpuOptions.map(opt => `
<label style='display:flex;align-items:center;padding:6px 12px;cursor:pointer;'>
<input type="checkbox" value="${opt}" style="margin-right:8px;" />${opt}
</label>
`).join('')}
</div>
</div>
</div>
<div class="add-model-group-buttons">
<button class="add-model-group-btn add-model-group-btn-secondary" id="cancelAddModelGroup">取消</button>
<button class="add-model-group-btn add-model-group-btn-primary" id="confirmAddModelGroup">确认添加</button>
</div>
</div>
`;
document.body.appendChild(dialog);
// 亲和性多选下拉逻辑
const cpuAffInput = dialog.querySelector('#cpuAffInput');
const cpuAffDropdown = dialog.querySelector('#cpuAffDropdown');
cpuAffInput.addEventListener('click', () => {
cpuAffDropdown.style.display = cpuAffDropdown.style.display === 'block' ? 'none' : 'block';
});
document.addEventListener('mousedown', function handler(e) {
if (!dialog.contains(e.target)) {
cpuAffDropdown.style.display = 'none';
document.removeEventListener('mousedown', handler);
}
});
cpuAffDropdown.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
checkbox.addEventListener('change', () => {
const selected = Array.from(cpuAffDropdown.querySelectorAll('input[type="checkbox"]:checked')).map(cb => cb.value);
cpuAffInput.value = selected.join(',');
});
});
dialog.querySelector('#cancelAddModelGroup').onclick = () => document.body.removeChild(dialog);
dialog.querySelector('#confirmAddModelGroup').onclick = async () => {
const name = dialog.querySelector('#newModelGroupName').value.trim();
const freq = parseFloat(dialog.querySelector('#freqInput').value);
const priority = parseInt(dialog.querySelector('#priorityInput').value);
const cpuAffSel = cpuAffInput.value;
if (!name) { alert('请输入模型组名称'); return; }
if (isNaN(freq) || freq <= 0 || freq > 1000) { alert('频率必须为0~1000之间的正浮点数'); return; }
if (isNaN(priority) || priority < 1 || priority > 99) { alert('优先级必须为1-99之间的整数'); return; }
if (!cpuAffSel) { alert('请选择亲和性'); return; }
// 组装数据
const postData = {
ConfID: this.selectedConfiguration.ConfID,
GroupName: name,
Freq: freq,
Priority: priority,
CPUAff: cpuAffSel
};
try {
const resp = await fetch(`/api/configurations/${this.selectedConfiguration.ConfID}/model-groups`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(postData)
});
if (!resp.ok) {
const data = await resp.json();
throw new Error(data.error || '添加失败');
}
document.body.removeChild(dialog);
this.showEditor(this.selectedConfiguration, this.selectedPlane, false);
} catch (err) {
alert('添加失败:' + err.message);
}
};
}
/**
* @method showAddServiceDialog
* @description 显示添加服务对话框
*/
async showAddServiceDialog() {
const dialog = document.createElement('div');
dialog.className = 'add-service-dialog';
dialog.innerHTML = `
<style>
.add-service-dialog {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.4);
display: flex; align-items: center; justify-content: center;
z-index: 1001;
}
.add-service-content {
background: #fff;
border-radius: 8px;
padding: 24px 32px;
min-width: 320px;
box-shadow: 0 2px 16px rgba(0,0,0,0.15);
}
.add-service-title {
font-size: 18px;
font-weight: 500;
margin-bottom: 18px;
color: #333;
}
.add-service-row {
margin-bottom: 16px;
}
.add-service-label {
display: block;
margin-bottom: 6px;
color: #555;
font-size: 14px;
}
.add-service-select {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
.add-service-buttons {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 10px;
}
.add-service-btn {
padding: 7px 18px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
}
.add-service-btn-primary {
background: #5c6bc0;
color: #fff;
}
.add-service-btn-secondary {
background: #e8eaf6;
color: #5c6bc0;
}
</style>
<div class="add-service-content">
<div class="add-service-title">添加服务</div>
<div class="add-service-row">
<label class="add-service-label">服务</label>
<select class="add-service-select" id="serviceSelect">
<option value="">加载中...</option>
</select>
</div>
<div class="add-service-row">
<label class="add-service-label">版本</label>
<select class="add-service-select" id="versionSelect" disabled>
<option value="">请先选择服务</option>
</select>
</div>
<div class="add-service-buttons">
<button class="add-service-btn add-service-btn-secondary" id="cancelAddService">取消</button>
<button class="add-service-btn add-service-btn-primary" id="confirmAddService">确认添加</button>
</div>
</div>
`;
document.body.appendChild(dialog);
// 获取服务列表
const serviceSelect = dialog.querySelector('#serviceSelect');
const versionSelect = dialog.querySelector('#versionSelect');
let serviceList = [];
try {
const response = await fetch('/api/services');
if (!response.ok) throw new Error('获取服务列表失败');
serviceList = await response.json();
serviceSelect.innerHTML = '<option value="">请选择服务</option>' +
serviceList.map(service => `<option value="${service.ClassName}">${service.ServiceName_CN || service.ServiceName}</option>`).join('');
} catch (error) {
console.error('获取服务列表失败:', error);
serviceSelect.innerHTML = '<option value="">获取服务列表失败</option>';
}
// 服务选择事件
serviceSelect.onchange = async () => {
const className = serviceSelect.value;
versionSelect.disabled = true;
versionSelect.innerHTML = '<option value="">加载中...</option>';
if (!className) {
versionSelect.innerHTML = '<option value="">请先选择服务</option>';
return;
}
try {
const response = await fetch(`/api/service-versions/${className}`);
if (!response.ok) throw new Error('获取版本列表失败');
const versions = await response.json();
versionSelect.innerHTML = '<option value="">请选择版本</option>' +
versions.map(version => `<option value="${version.Version}">${version.Version}</option>`).join('');
versionSelect.disabled = false;
} catch (error) {
console.error('获取版本列表失败:', error);
versionSelect.innerHTML = '<option value="">获取版本列表失败</option>';
}
};
// 取消按钮事件
dialog.querySelector('#cancelAddService').onclick = () => {
document.body.removeChild(dialog);
};
// 确认按钮事件
dialog.querySelector('#confirmAddService').onclick = async () => {
const className = serviceSelect.value;
const version = versionSelect.value;
if (!className || !version) {
alert('请选择完整的服务信息');
return;
}
// 查找ServiceName
const selectedService = serviceList.find(s => s.ClassName === className);
const serviceName = selectedService ? (selectedService.ServiceName || selectedService.ServiceName_CN || className) : className;
try {
const response = await fetch(`/api/configurations/${this.selectedConfiguration.ConfID}/services`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
ClassName: className,
ServiceVersion: version,
ServiceName: serviceName
})
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || '添加服务失败');
}
document.body.removeChild(dialog);
this.showEditor(this.selectedConfiguration, this.selectedPlane, false);
} catch (error) {
console.error('添加服务失败:', error);
alert('添加服务失败:' + error.message);
}
};
}
}
// 注册自定义元素
customElements.define('configuration-config', ConfigurationConfig);