XNSim/XNSimHtml/components/service-development.js

1512 lines
65 KiB
JavaScript
Raw Permalink 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 ServiceDevelopment extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.services = [];
this.currentServices = [];
this.currentServiceVersions = [];
this.currentVersion = null;
this.currentView = 'services'; // 'services', 'versions', 'versionForm'
this.currentService = null;
this.isEditMode = false;
// 初始化DOM结构
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
padding: 20px;
color: #333;
box-sizing: border-box;
height: calc(100vh - 40px); /* 限制总高度 */
overflow: hidden; /* 防止整体滚动 */
}
*, *:before, *:after {
box-sizing: inherit;
}
.page-title {
color: #5c6bc0;
margin-bottom: 20px;
font-weight: 500;
font-size: 24px;
}
.subtitle {
font-size: 16px;
color: #666;
font-weight: normal;
margin-left: 10px;
}
.service-dev-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
max-height: calc(100vh - 200px); /* 调整容器高度,留出足够空间 */
overflow-y: auto;
padding-right: 10px;
padding-top: 10px; /* 添加上边距 */
width: 100%;
/* 添加以下属性确保每行高度一致 */
grid-auto-rows: 1fr;
/* 添加底部padding确保滚动时最后一行完全可见 */
padding-bottom: 20px;
margin-bottom: 50px; /* 添加底部边距,确保滚动条箭头完全可见 */
}
.service-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;
/* 添加以下属性,确保卡片大小一致 */
box-sizing: border-box;
height: 100%;
width: 100%;
/* 添加顶部margin防止悬停时上边界消失 */
margin-top: 5px;
/* 添加底部padding防止内容靠近下边界 */
padding-bottom: 25px;
/* 添加底部margin确保下边界可见 */
margin-bottom: 5px;
}
.service-card:hover {
/* 减小上移距离防止超出边界 */
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15);
}
.service-title {
font-size: 20px;
font-weight: 600;
margin-bottom: 10px;
color: #333;
}
.service-subtitle {
font-size: 16px;
color: #666;
}
.version-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 20px;
transition: transform 0.3s ease;
cursor: pointer;
height: 100%;
display: flex;
flex-direction: column;
/* 添加以下属性确保与service-card一致 */
box-sizing: border-box;
width: 100%;
margin-top: 5px;
margin-bottom: 5px;
padding-bottom: 25px;
}
.version-card:hover {
transform: translateY(-3px);
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.12);
}
.version-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.version-title {
font-size: 18px;
font-weight: 600;
color: #333;
}
.version-number {
font-size: 15px;
font-weight: 500;
color: #5c6bc0;
padding: 3px 8px;
background: #eef0ff;
border-radius: 4px;
}
.version-details {
display: grid;
grid-template-columns: 1fr;
gap: 6px;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid #eee;
}
.version-detail {
font-size: 14px;
color: #666;
}
.label {
font-weight: 500;
color: #555;
margin-right: 5px;
}
.version-description {
font-size: 14px;
color: #666;
line-height: 1.5;
flex-grow: 1;
}
.back-button {
grid-column: 1 / -1;
cursor: pointer;
color: #667eea;
font-weight: 500;
margin-bottom: 15px;
}
.back-button:hover {
text-decoration: underline;
}
.add-version-button {
background-color: #667eea;
color: white;
border: none;
border-radius: 4px;
padding: 8px 16px;
font-size: 14px;
cursor: pointer;
margin-bottom: 20px;
}
.add-version-button:hover {
background-color: #5c6bc0;
}
.versions-grid {
display: grid;
grid-column: 1 / -1;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
grid-auto-rows: 1fr; /* 确保每行高度一致 */
width: 100%; /* 确保宽度占满整个容器 */
padding-bottom: 20px; /* 底部边距 */
margin-bottom: 30px; /* 增加底部边距,确保滚动条完全可见 */
}
.services-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
width: 100%;
padding-bottom: 20px;
margin-bottom: 30px; /* 增加底部边距,确保滚动条完全可见 */
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #333;
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.form-group textarea {
min-height: 100px;
resize: vertical;
}
.form-actions {
margin-top: 20px;
text-align: right;
}
.save-button {
background-color: #667eea;
color: white;
border: none;
border-radius: 4px;
padding: 10px 20px;
font-size: 14px;
cursor: pointer;
}
.save-button:hover {
background-color: #5c6bc0;
}
.version-form {
grid-column: 1 / -1;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 20px;
width: 100%;
}
.loading {
text-align: center;
padding: 40px;
color: #666;
grid-column: 1 / -1;
}
.error-state {
text-align: center;
padding: 40px;
color: #e74c3c;
grid-column: 1 / -1;
}
.retry-btn {
background: #667eea;
color: white;
border: none;
border-radius: 4px;
padding: 8px 16px;
cursor: pointer;
font-size: 14px;
margin-top: 15px;
}
.empty-state {
text-align: center;
padding: 40px;
color: #666;
grid-column: 1 / -1;
margin-top: 20px;
background-color: #f9fafb;
border-radius: 8px;
border: 1px dashed #e2e8f0;
}
/* 新建版本卡片样式 */
.new-version-card {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
background-color: #f8fafc;
border: 1px solid #cbd5e1;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 20px;
transition: all 0.3s ease;
/* 添加以下属性确保与其他卡片大小一致 */
box-sizing: border-box;
height: 100%;
width: 100%;
margin-top: 5px;
margin-bottom: 5px;
padding-bottom: 25px;
min-height: 180px;
}
.new-version-card:hover {
background-color: #f1f5f9;
border-color: #94a3b8;
transform: translateY(-3px);
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.12);
}
.new-version-icon {
display: flex;
justify-content: center;
align-items: center;
width: 50px;
height: 50px;
background-color: #eef2ff;
border-radius: 50%;
margin-bottom: 12px;
}
.new-version-icon svg {
width: 25px;
height: 25px;
fill: #6366f1;
}
.new-version-title {
font-size: 16px;
font-weight: 600;
color: #4f46e5;
margin-bottom: 5px;
}
.new-version-description {
font-size: 13px;
color: #64748b;
line-height: 1.4;
margin-bottom: 0;
}
/* 新建服务卡片样式 */
.new-service-card {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
background-color: #f8fafc;
border: 1px solid #cbd5e1;
padding: 30px 20px;
transition: all 0.3s ease;
/* 确保内容垂直居中,不会太靠近底部 */
min-height: 180px;
/* 添加以下属性确保与其他卡片大小一致 */
box-sizing: border-box;
height: 100%;
width: 100%;
/* 添加顶部margin防止悬停时上边界消失 */
margin-top: 5px;
/* 添加底部margin确保下边界可见 */
margin-bottom: 5px;
}
.new-service-card:hover {
background-color: #eef2ff;
border-color: #818cf8;
/* 修改transform值减少上移距离防止超出边界 */
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(79, 70, 229, 0.15);
}
</style>
<div class="service-development">
<h2 class="page-title">服务列表</h2>
<div class="service-dev-container">
<div class="loading">正在加载数据...</div>
</div>
</div>
`;
// 开始初始化数据
this.init();
}
async init() {
try {
const response = await fetch('/api/services');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
this.services = await response.json();
this.renderServices();
} catch (error) {
console.error('初始化服务开发页面失败:', error);
this.renderError('加载服务列表失败,请稍后重试。');
}
}
async fetchServiceVersions(className, serviceName) {
try {
const response = await fetch(`/api/service-versions/${encodeURIComponent(className)}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
this.currentServiceVersions = await response.json();
this.currentService = { className, name: serviceName };
this.renderServiceVersions();
} catch (error) {
console.error(`获取服务${className}的版本数据失败:`, error);
this.renderError(error.message);
}
}
async saveServiceVersion(versionData) {
try {
const response = await fetch('/api/service-versions', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(versionData)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
return result;
} catch (error) {
console.error('保存服务版本失败:', error);
throw error;
}
}
renderServices() {
this.currentView = 'services';
// 更新标题
const title = this.shadowRoot.querySelector('.page-title');
if (title) {
title.textContent = '服务列表';
}
const container = this.shadowRoot.querySelector('.service-dev-container');
if (!container) {
console.error('找不到容器元素');
return;
}
// 清空容器并重置样式
container.innerHTML = '';
container.style = ''; // 清除所有行内样式
// 重新应用基本样式
Object.assign(container.style, {
display: 'block', // 改为块级元素
width: '100%',
maxHeight: 'calc(100vh - 200px)', // 调整高度,留出滚动条箭头空间
overflowY: 'auto',
paddingRight: '10px',
paddingTop: '10px',
paddingBottom: '20px',
marginBottom: '50px' // 添加底部边距,确保滚动条箭头可见
});
// 创建服务网格
const servicesGrid = document.createElement('div');
servicesGrid.className = 'services-grid';
// 确保使用网格布局
Object.assign(servicesGrid.style, {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))',
gap: '20px',
width: '100%',
paddingBottom: '20px'
});
// 添加新建服务卡片
const newServiceCard = document.createElement('div');
newServiceCard.className = 'service-card new-service-card';
newServiceCard.innerHTML = `
<div style="display: flex; justify-content: center; align-items: center; width: 50px; height: 50px; background-color: #eef2ff; border-radius: 50%; margin-bottom: 12px;">
<svg viewBox="0 0 24 24" width="25" height="25" fill="#6366f1">
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</svg>
</div>
<div class="service-title" style="text-align: center; color: #4f46e5;">添加新服务</div>
<div class="service-subtitle" style="text-align: center;">创建一个新的服务定义</div>
`;
newServiceCard.addEventListener('click', () => this.showServiceCreationDialog());
servicesGrid.appendChild(newServiceCard);
if (this.services.length === 0) {
const emptyState = document.createElement('div');
emptyState.className = 'empty-state';
emptyState.style.gridColumn = '1 / -1';
emptyState.textContent = '未找到服务数据';
servicesGrid.appendChild(emptyState);
} else {
// 添加从后端获取的服务卡片
this.services.forEach(service => {
const card = document.createElement('div');
card.className = 'service-card';
card.innerHTML = `
<div class="service-title">${service.ServiceName}</div>
<div class="service-subtitle">中文名称: ${service.ServiceName_CN || '无'}</div>
<div class="service-detail"><span class="label">类名:</span> ${service.ClassName}</div>
<div class="service-detail"><span class="label">描述:</span> ${service.Description || '无描述'}</div>
`;
card.addEventListener('click', () => this.fetchServiceVersions(service.ClassName, service.ServiceName));
servicesGrid.appendChild(card);
});
}
// 将服务网格添加到容器
container.appendChild(servicesGrid);
}
renderServiceVersions() {
this.currentView = 'versions';
// 更新标题为服务名称
const title = this.shadowRoot.querySelector('.page-title');
title.textContent = this.currentService.name;
const container = this.shadowRoot.querySelector('.service-dev-container');
// 清空容器并重置样式
container.innerHTML = '';
container.style = ''; // 清除所有行内样式
// 重新应用基本样式
Object.assign(container.style, {
display: 'block', // 改为块级元素
width: '100%',
maxHeight: 'calc(100vh - 200px)', // 调整高度,留出滚动条箭头空间
overflowY: 'auto',
paddingRight: '10px',
paddingTop: '10px',
paddingBottom: '20px',
marginBottom: '50px' // 添加底部边距,确保滚动条箭头可见
});
// 添加返回按钮(作为独立元素)
const backButton = document.createElement('div');
backButton.className = 'back-button';
backButton.innerHTML = `<span>← 返回服务列表</span>`;
backButton.addEventListener('click', () => this.renderServices());
container.appendChild(backButton);
// 创建版本卡片容器
const versionsGrid = document.createElement('div');
versionsGrid.className = 'versions-grid';
// 确保使用网格布局
Object.assign(versionsGrid.style, {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))',
gap: '20px',
width: '100%',
paddingBottom: '20px',
marginBottom: '30px' // 增加底部边距,确保滚动条箭头可见
});
// 添加新建服务版本卡片(始终是第一个)
const newVersionCard = document.createElement('div');
newVersionCard.className = 'version-card new-version-card';
newVersionCard.innerHTML = `
<div class="new-version-icon">
<svg viewBox="0 0 24 24">
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</svg>
</div>
<div class="new-version-title">新建服务版本</div>
<div class="new-version-description">为${this.currentService.name}创建新的版本</div>
`;
newVersionCard.addEventListener('click', () => this.createNewVersion());
versionsGrid.appendChild(newVersionCard);
if (this.currentServiceVersions.length === 0) {
const emptyState = document.createElement('div');
emptyState.className = 'empty-state';
emptyState.style.gridColumn = '1 / -1';
emptyState.textContent = '该服务没有版本信息,点击"新建服务版本"创建第一个版本';
versionsGrid.appendChild(emptyState);
} else {
// 对版本进行排序 - 按版本号从高到低排序
const sortedVersions = [...this.currentServiceVersions].sort((a, b) => {
return this.compareVersions(b.Version, a.Version);
});
// 添加从后端获取的版本卡片
sortedVersions.forEach(version => {
const card = document.createElement('div');
card.className = 'version-card';
// 获取显示名称 - 优先使用Name字段如果没有则使用当前服务名称
const displayName = version.Name || version.ServiceName || this.currentService.name;
card.innerHTML = `
<div class="version-header">
<div class="version-title">${displayName}</div>
<div class="version-number">v${version.Version || '未知版本'}</div>
</div>
<div class="version-details">
<div class="version-detail"><span class="label">作者:</span> ${version.Author || '未知'}</div>
<div class="version-detail"><span class="label">创建时间:</span> ${version.CreatTime || '未知'}</div>
<div class="version-detail"><span class="label">修改时间:</span> ${version.ChangeTime || '未知'}</div>
</div>
<div class="version-description">${version.Description || '无描述'}</div>
`;
card.addEventListener('click', () => this.showVersionEditor(version));
versionsGrid.appendChild(card);
});
}
// 将版本网格添加到容器
container.appendChild(versionsGrid);
}
// 比较版本号的方法
compareVersions(versionA, versionB) {
if (!versionA) return -1;
if (!versionB) return 1;
const partsA = versionA.toString().split('.').map(Number);
const partsB = versionB.toString().split('.').map(Number);
// 确保两个数组长度相同,以便比较
const maxLength = Math.max(partsA.length, partsB.length);
for (let i = 0; i < maxLength; i++) {
// 处理长度不同的情况用0填充
const partA = i < partsA.length ? partsA[i] : 0;
const partB = i < partsB.length ? partsB[i] : 0;
if (partA > partB) return 1;
if (partA < partB) return -1;
}
return 0; // 版本号相同
}
renderError(message) {
// 更新当前视图状态
this.currentView = 'error';
const container = this.shadowRoot.querySelector('.service-dev-container');
if (!container) {
console.error('找不到容器元素');
return;
}
// 清空容器并重置样式
container.innerHTML = '';
container.style = ''; // 清除所有行内样式
// 重新应用基本样式
Object.assign(container.style, {
display: 'block',
width: '100%',
maxHeight: 'calc(100vh - 200px)', // 调整高度,留出滚动条箭头空间
overflowY: 'auto',
paddingRight: '10px',
paddingTop: '10px',
paddingBottom: '20px',
marginBottom: '50px' // 添加底部边距,确保滚动条箭头可见
});
// 创建错误容器
const errorContainer = document.createElement('div');
errorContainer.className = 'error-state';
errorContainer.innerHTML = `
<p>加载数据失败: ${message}</p>
<button class="retry-btn">重试</button>
`;
// 添加到容器
container.appendChild(errorContainer);
const retryBtn = errorContainer.querySelector('.retry-btn');
if (retryBtn) {
retryBtn.addEventListener('click', () => {
if (this.currentView === 'services') {
this.init();
} else if (this.currentView === 'versions' && this.currentService) {
this.fetchServiceVersions(this.currentService.className, this.currentService.name);
} else {
this.init();
}
});
}
}
// 组件被重新激活时调用
reactivate() {
}
showVersionEditor(versionData) {
this.currentView = 'versionEditor';
this.currentVersion = versionData;
this.isEditMode = versionData && versionData.Version ? true : false;
const container = this.shadowRoot.querySelector('.service-dev-container');
if (!container) {
console.error('找不到容器元素');
return;
}
// 更新标题
const title = this.shadowRoot.querySelector('.page-title');
if (title) {
title.textContent = this.isEditMode
? `编辑版本: ${versionData.Name || this.currentService.name} (v${versionData.Version})`
: `${this.currentService.name} 创建新版本`;
}
container.innerHTML = '';
// 添加工具栏按钮
const toolbarDiv = document.createElement('div');
toolbarDiv.className = 'editor-toolbar';
toolbarDiv.style.cssText = 'display: flex; justify-content: space-between; margin-bottom: 20px; grid-column: 1 / -1; width: 100%;';
// 添加返回按钮
const backButton = document.createElement('div');
backButton.className = 'back-button';
backButton.innerHTML = `<span>← 返回版本列表</span>`;
backButton.style.cssText = 'margin: 0; cursor: pointer; color: #667eea; font-weight: 500;';
backButton.addEventListener('click', () => {
this.fetchServiceVersions(this.currentService.className, this.currentService.name);
});
// 添加按钮组
const actionsDiv = document.createElement('div');
actionsDiv.className = 'toolbar-actions';
actionsDiv.style.cssText = 'display: flex; gap: 10px;';
// 创建各种按钮
const buttonConfigs = [
{
text: '刷新',
color: '#38a169',
action: async () => {
try {
// 保存当前版本ID用于刷新后重新定位
const versionId = this.currentVersion.Version;
// 先刷新版本列表
await this.fetchServiceVersions(this.currentService.className, this.currentService.name);
// 然后找到当前编辑的版本并重新打开
if (versionId) {
const refreshedVersion = this.currentServiceVersions.find(v => v.Version === versionId);
if (refreshedVersion) {
this.showVersionEditor(refreshedVersion);
} else {
alert('未能找到当前版本,可能已被删除');
// 如果找不到当前版本,回到版本列表
this.renderServiceVersions();
}
}
} catch (error) {
console.error('刷新版本数据失败:', error);
alert(`刷新失败: ${error.message}`);
}
}
},
{
text: '保存',
color: '#3182ce',
action: () => {
try {
// 获取表单元素
const form = this.shadowRoot.querySelector('#versionEditorForm');
if (form) {
// 触发提交事件
form.dispatchEvent(new Event('submit'));
} else {
console.error('找不到表单元素');
alert('保存失败: 找不到表单元素');
}
} catch (error) {
console.error('保存时发生错误:', error);
alert(`保存失败: ${error.message}`);
}
}
},
{ text: '生成代码', color: '#805ad5', action: () => alert('生成代码功能即将上线') },
{ text: '编辑代码', color: '#d69e2e', action: () => alert('编辑代码功能即将上线') },
{ text: '服务编译', color: '#dd6b20', action: () => alert('服务编译功能即将上线') },
{ text: '服务提交', color: '#e53e3e', action: () => alert('服务提交功能即将上线') }
];
buttonConfigs.forEach(config => {
const button = document.createElement('button');
button.className = 'toolbar-button';
button.textContent = config.text;
button.style.cssText = `background-color: ${config.color}; color: white; border: none; border-radius: 4px; padding: 8px 16px; cursor: pointer; font-weight: 500;`;
button.addEventListener('click', config.action);
actionsDiv.appendChild(button);
});
toolbarDiv.appendChild(backButton);
toolbarDiv.appendChild(actionsDiv);
container.appendChild(toolbarDiv);
// 创建表单容器 - 使用网格布局
const formContainer = document.createElement('div');
formContainer.className = 'form-container';
formContainer.style.cssText = 'display: grid; grid-template-columns: 1fr 1fr; gap: 20px; grid-column: 1 / -1; width: 100%;';
// 创建表单
const form = document.createElement('form');
form.className = 'version-form';
form.id = 'versionEditorForm';
form.style.cssText = 'background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); padding: 20px; grid-column: 1 / -1;';
// 创建两列布局的基本信息部分
const basicInfoSection = document.createElement('div');
basicInfoSection.className = 'form-section basic-info';
basicInfoSection.style.cssText = 'display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px;';
// 左列 - 基本信息
const leftColumn = document.createElement('div');
leftColumn.className = 'form-column';
leftColumn.innerHTML = `
<div class="form-group">
<label for="className">类名 (ClassName)*</label>
<input type="text" id="className" name="ClassName" value="${this.currentVersion.ClassName || this.currentService.className}" readonly title="服务类名,不可修改" required>
</div>
<div class="form-group">
<label for="name">名称 (Name)*</label>
<input type="text" id="name" name="Name" value="${this.currentVersion.Name || this.currentService.name}" readonly title="服务名称,不可修改" required>
</div>
<div class="form-group">
<label for="version">版本 (Version)*</label>
<input type="text" id="version" name="Version" value="${this.currentVersion.Version || '1.0.0'}" pattern="[0-9.]+" title="版本号只能包含数字和小数点例如1.0.0" required>
</div>
<div class="form-group">
<label for="author">作者 (Author)*</label>
<input type="text" id="author" name="Author" value="${this.currentVersion.Author || ''}" title="服务版本的作者姓名" required>
</div>
<div class="form-group">
<label for="changeTime">修改时间 (ChangeTime)*</label>
<div style="display: flex; gap: 8px;">
<input type="text" id="changeTime" name="ChangeTime" value="${this.currentVersion.ChangeTime || this.getCurrentDateTime()}" title="最后修改时间" style="flex: 1;" required>
<button type="button" id="dateTimePickerButton" style="background-color: #667eea; color: white; border: none; border-radius: 4px; width: 40px; display: flex; justify-content: center; align-items: center;">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
<line x1="16" y1="2" x2="16" y2="6"></line>
<line x1="8" y1="2" x2="8" y2="6"></line>
<line x1="3" y1="10" x2="21" y2="10"></line>
</svg>
</button>
</div>
</div>
`;
// 右列 - 描述和其他信息
const rightColumn = document.createElement('div');
rightColumn.className = 'form-column';
rightColumn.innerHTML = `
<div class="form-group" style="height: 100%;">
<label for="description">描述 (Description)</label>
<textarea id="description" name="Description" style="height: calc(100% - 30px);" title="服务版本的详细描述,包括主要功能和改进">${this.currentVersion.Description || ''}</textarea>
</div>
`;
basicInfoSection.appendChild(leftColumn);
basicInfoSection.appendChild(rightColumn);
// 代码路径
const codePathSection = document.createElement('div');
codePathSection.className = 'form-section code-path';
codePathSection.style.cssText = 'margin-top: 20px; border-top: 1px solid #e2e8f0; padding-top: 20px;';
const codePathGroup = document.createElement('div');
codePathGroup.className = 'form-group';
codePathGroup.innerHTML = `
<label for="codePath">代码路径 (CodePath)</label>
<input type="text" id="codePath" name="CodePath" value="${this.currentVersion.CodePath || ''}" title="服务代码文件的路径">
`;
codePathSection.appendChild(codePathGroup);
// 表单操作按钮
const formActions = document.createElement('div');
formActions.className = 'form-actions';
formActions.style.cssText = 'margin-top: 30px; text-align: right; border-top: 1px solid #e2e8f0; padding-top: 20px;';
formActions.innerHTML = `
<button type="submit" class="save-button">${this.isEditMode ? '保存修改' : '创建版本'}</button>
`;
// 组装表单
form.appendChild(basicInfoSection);
form.appendChild(codePathSection);
form.appendChild(formActions);
formContainer.appendChild(form);
container.appendChild(formContainer);
// 为日期时间按钮添加事件监听器
setTimeout(() => {
const dateTimePickerButton = this.shadowRoot.querySelector('#dateTimePickerButton');
const changeTimeInput = this.shadowRoot.querySelector('#changeTime');
if (dateTimePickerButton && changeTimeInput) {
dateTimePickerButton.addEventListener('click', () => {
this.showDateTimeDialog(changeTimeInput);
});
}
}, 0);
// 添加表单提交事件
form.addEventListener('submit', async (e) => {
e.preventDefault();
try {
// 显示加载提示
const saveButton = form.querySelector('.save-button');
const originalButtonText = saveButton.textContent;
saveButton.textContent = '保存中...';
saveButton.disabled = true;
// 验证版本号格式
const versionInput = form.querySelector('#version');
if (versionInput && !/^[0-9.]+$/.test(versionInput.value)) {
throw new Error('版本号只能包含数字和小数点');
}
// 构建版本数据
const versionData = {
ClassName: form.querySelector('#className').value,
Name: form.querySelector('#name').value,
Version: form.querySelector('#version').value,
Author: form.querySelector('#author').value,
Description: form.querySelector('#description').value,
ChangeTime: form.querySelector('#changeTime').value,
CodePath: form.querySelector('#codePath').value,
isUpdate: this.isEditMode,
originalVersion: this.isEditMode ? this.currentVersion.Version : null
};
// 验证必填字段
const requiredFields = ['ClassName', 'Name', 'Version', 'Author', 'ChangeTime'];
for (const field of requiredFields) {
if (!versionData[field]) {
throw new Error(`${field}是必填字段`);
}
}
// 保存版本数据
const result = await this.saveServiceVersion(versionData);
// 显示保存成功提示
alert(this.isEditMode ? '版本更新成功!' : '新版本创建成功!');
// 刷新版本列表
this.fetchServiceVersions(this.currentService.className, this.currentService.name);
} catch (error) {
alert(`保存失败: ${error.message}`);
} finally {
// 恢复按钮状态
const saveButton = form.querySelector('.save-button');
if (saveButton) {
saveButton.textContent = this.isEditMode ? '保存修改' : '创建版本';
saveButton.disabled = false;
}
}
});
}
/**
* 显示日期时间选择对话框
* @param {HTMLInputElement} inputElement - 输入元素
*/
showDateTimeDialog(inputElement) {
// 获取当前系统时间
const now = new Date();
// 解析当前日期和时间
let currentDate = now;
let currentTime = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
// 如果输入框有值,尝试解析
if (inputElement.value && inputElement.value.trim() !== '') {
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]);
const parsedDate = new Date(year, month, day);
// 验证日期是否有效
if (!isNaN(parsedDate.getTime())) {
currentDate = parsedDate;
}
}
}
if (parts.length >= 2) {
// 验证时间格式
const timeRegex = /^([01]\d|2[0-3]):([0-5]\d):([0-5]\d)$/;
if (timeRegex.test(parts[1])) {
currentTime = parts[1];
}
}
} catch (error) {
console.error('解析日期时间失败:', error);
// 使用系统当前时间(已在初始化时设置)
}
}
// 创建模态框
const modal = document.createElement('div');
modal.className = 'modal';
modal.id = 'dateTimeModal';
modal.style.cssText = 'position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.4); display: flex; justify-content: center; align-items: center;';
// 获取年、月、日
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>';
}
// 解析当前时间为时、分、秒
let hours = '00', minutes = '00', seconds = '00';
if (currentTime) {
const timeParts = currentTime.split(':');
if (timeParts.length >= 1) hours = timeParts[0].padStart(2, '0');
if (timeParts.length >= 2) minutes = timeParts[1].padStart(2, '0');
if (timeParts.length >= 3) seconds = timeParts[2].padStart(2, '0');
}
modal.innerHTML = `
<div class="modal-content" style="background-color: white; margin: auto; padding: 20px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); max-width: 400px; width: 100%;">
<div class="modal-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<div class="modal-title" style="font-size: 18px; font-weight: 600; color: #2d3748;">选择日期和时间</div>
<span class="close" style="font-size: 24px; color: #a0aec0; cursor: pointer;">&times;</span>
</div>
<div class="modal-body">
<div class="calendar-container" style="width: 100%;">
<div class="calendar-header" style="display: flex; justify-content: space-between; margin-bottom: 10px;">
<select id="calendarMonth" style="padding: 5px; border: 1px solid #e2e8f0; border-radius: 4px; flex: 1; margin-right: 10px;">${monthOptions}</select>
<select id="calendarYear" style="padding: 5px; border: 1px solid #e2e8f0; border-radius: 4px; flex: 1;">${yearOptions}</select>
</div>
<table class="calendar-table" style="width: 100%; border-collapse: collapse;">
<thead>
${calendarRows}
</thead>
</table>
</div>
<div class="time-container" style="margin-top: 15px;">
<label style="display: block; margin-bottom: 5px; color: #4a5568;">时间:</label>
<div style="display: flex; gap: 5px;">
<div style="flex: 1;">
<label for="hourInput" style="display: block; text-align: center; font-size: 12px; color: #718096;">时</label>
<input type="number" id="hourInput" min="0" max="23" value="${hours}" style="width: 100%; padding: 8px; border: 1px solid #e2e8f0; border-radius: 4px; text-align: center;">
</div>
<div style="display: flex; align-items: center; padding-top: 15px;">:</div>
<div style="flex: 1;">
<label for="minuteInput" style="display: block; text-align: center; font-size: 12px; color: #718096;">分</label>
<input type="number" id="minuteInput" min="0" max="59" value="${minutes}" style="width: 100%; padding: 8px; border: 1px solid #e2e8f0; border-radius: 4px; text-align: center;">
</div>
<div style="display: flex; align-items: center; padding-top: 15px;">:</div>
<div style="flex: 1;">
<label for="secondInput" style="display: block; text-align: center; font-size: 12px; color: #718096;">秒</label>
<input type="number" id="secondInput" min="0" max="59" value="${seconds}" style="width: 100%; padding: 8px; border: 1px solid #e2e8f0; border-radius: 4px; text-align: center;">
</div>
</div>
</div>
</div>
<div class="modal-footer" style="margin-top: 20px; text-align: right;">
<button id="cancelDateTime" style="background-color: #e2e8f0; color: #4a5568; border: none; border-radius: 4px; padding: 8px 16px; margin-right: 10px; cursor: pointer;">取消</button>
<button id="confirmDateTime" style="background-color: #4c6ef5; color: white; border: none; border-radius: 4px; padding: 8px 16px; cursor: pointer;">确定</button>
</div>
</div>
`;
// 添加样式
const style = document.createElement('style');
style.textContent = `
.calendar-day {
cursor: pointer;
padding: 8px;
text-align: center;
border: 1px solid #e2e8f0;
}
.calendar-day:hover {
background-color: #ebf4ff;
}
.calendar-day.selected {
background-color: #4c6ef5;
color: white;
}
.calendar-table th {
padding: 8px;
text-align: center;
border: 1px solid #e2e8f0;
color: #4a5568;
}
`;
// 获取shadowRoot
const shadowRoot = this.shadowRoot;
// 添加到DOM
shadowRoot.appendChild(style);
shadowRoot.appendChild(modal);
// 事件处理
const closeBtn = modal.querySelector('.close');
const cancelBtn = modal.querySelector('#cancelDateTime');
const confirmBtn = modal.querySelector('#confirmDateTime');
const calendarMonth = modal.querySelector('#calendarMonth');
const calendarYear = modal.querySelector('#calendarYear');
const calendarDays = modal.querySelectorAll('.calendar-day');
const hourInput = modal.querySelector('#hourInput');
const minuteInput = modal.querySelector('#minuteInput');
const secondInput = modal.querySelector('#secondInput');
// 验证时间输入
const validateTimeInput = (input, min, max) => {
let value = parseInt(input.value);
if (isNaN(value)) value = 0;
if (value < min) value = min;
if (value > max) value = max;
input.value = value.toString().padStart(2, '0');
};
// 添加输入验证
hourInput.addEventListener('change', () => validateTimeInput(hourInput, 0, 23));
minuteInput.addEventListener('change', () => validateTimeInput(minuteInput, 0, 59));
secondInput.addEventListener('change', () => validateTimeInput(secondInput, 0, 59));
// 选择日期事件
calendarDays.forEach(cell => {
cell.addEventListener('click', (e) => {
// 移除所有选中状态
calendarDays.forEach(day => day.classList.remove('selected'));
// 添加新选中状态
e.target.classList.add('selected');
});
});
// 月份和年份变化时重新渲染日历
const updateCalendar = () => {
const selectedYear = parseInt(calendarYear.value);
const selectedMonth = parseInt(calendarMonth.value);
// 关闭当前对话框
closeModal();
// 更新日期参数并重新显示对话框
currentDate = new Date(selectedYear, selectedMonth, 1);
this.showDateTimeDialog(inputElement);
};
calendarMonth.addEventListener('change', updateCalendar);
calendarYear.addEventListener('change', updateCalendar);
const closeModal = () => {
if (modal.parentNode === shadowRoot) {
shadowRoot.removeChild(modal);
}
if (style.parentNode === shadowRoot) {
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 hours = hourInput.value.padStart(2, '0');
const minutes = minuteInput.value.padStart(2, '0');
const seconds = secondInput.value.padStart(2, '0');
// 格式化日期
const formattedDate = `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
// 格式化时间
const formattedTime = `${hours}:${minutes}:${seconds}`;
// 更新输入框值为标准格式
inputElement.value = `${formattedDate} ${formattedTime}`;
closeModal();
});
}
// 创建新版本的方法
createNewVersion() {
// 创建一个新的版本对象,设置默认值
const newVersion = {
ClassName: this.currentService.className,
Name: this.currentService.name,
Version: this.getNextVersionNumber(),
Author: '',
Description: '',
CreatTime: this.getCurrentDateTime(),
ChangeTime: this.getCurrentDateTime(),
CodePath: ''
};
// 打开编辑界面
this.showVersionEditor(newVersion);
}
// 获取下一个版本号
getNextVersionNumber() {
if (!this.currentServiceVersions || this.currentServiceVersions.length === 0) {
return "1.0.0.0";
}
// 找出最高版本号
let highestVersion = null;
this.currentServiceVersions.forEach(version => {
if (!version.Version) return;
if (!highestVersion || this.compareVersions(version.Version, highestVersion) > 0) {
highestVersion = version.Version;
}
});
if (!highestVersion) return "1.0.0.0";
// 解析版本号
const versionParts = highestVersion.toString().split('.').map(Number);
// 确保版本号有4位
while (versionParts.length < 4) {
versionParts.push(0);
}
// 增加最后一位
versionParts[versionParts.length - 1]++;
// 处理进位
for (let i = versionParts.length - 1; i > 0; i--) {
if (versionParts[i] > 9) {
versionParts[i] = 0;
versionParts[i - 1]++;
}
}
return versionParts.join('.');
}
// 获取当前日期时间格式化字符串
getCurrentDateTime() {
const now = new Date();
// 格式化日期YYYY-MM-DD
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const date = `${year}-${month}-${day}`;
// 格式化时间HH:MM:SS
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
const time = `${hours}:${minutes}:${seconds}`;
return `${date} ${time}`;
}
// 显示服务创建对话框
showServiceCreationDialog() {
// 创建模态框
const modal = document.createElement('div');
modal.className = 'modal';
modal.id = 'serviceCreationModal';
modal.style.cssText = 'position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.4); display: flex; justify-content: center; align-items: center;';
modal.innerHTML = `
<div class="modal-content" style="background-color: white; margin: auto; padding: 20px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); max-width: 500px; width: 100%;">
<div class="modal-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<div class="modal-title" style="font-size: 20px; font-weight: 600; color: #2d3748;">添加新服务</div>
<span class="close" style="font-size: 24px; color: #a0aec0; cursor: pointer;">&times;</span>
</div>
<div class="modal-body">
<form id="serviceCreationForm">
<div class="form-group" style="margin-bottom: 15px;">
<label for="serviceClassName" style="display: block; margin-bottom: 5px; font-weight: 500; color: #4a5568;">服务类名 (ClassName)*</label>
<input type="text" id="serviceClassName" required style="width: 100%; padding: 10px; border: 1px solid #e2e8f0; border-radius: 4px; font-size: 14px;">
<small style="display: block; margin-top: 5px; color: #718096; font-size: 12px;">必须以XN开头且是唯一的类名如: XNUserService</small>
</div>
<div class="form-group" style="margin-bottom: 15px;">
<label for="serviceName" style="display: block; margin-bottom: 5px; font-weight: 500; color: #4a5568;">服务名称 (ServiceName)*</label>
<input type="text" id="serviceName" required style="width: 100%; padding: 10px; border: 1px solid #e2e8f0; border-radius: 4px; font-size: 14px;">
<small style="display: block; margin-top: 5px; color: #718096; font-size: 12px;">服务的英文名称,建议使用英文</small>
</div>
<div class="form-group" style="margin-bottom: 15px;">
<label for="serviceNameCN" style="display: block; margin-bottom: 5px; font-weight: 500; color: #4a5568;">中文名称 (ServiceName_CN)*</label>
<input type="text" id="serviceNameCN" required style="width: 100%; padding: 10px; border: 1px solid #e2e8f0; border-radius: 4px; font-size: 14px;">
<small style="display: block; margin-top: 5px; color: #718096; font-size: 12px;">服务的中文显示名称</small>
</div>
<div class="form-group" style="margin-bottom: 15px;">
<label for="serviceDescription" style="display: block; margin-bottom: 5px; font-weight: 500; color: #4a5568;">服务描述 (Description)</label>
<textarea id="serviceDescription" style="width: 100%; padding: 10px; border: 1px solid #e2e8f0; border-radius: 4px; font-size: 14px; min-height: 100px; resize: vertical;"></textarea>
</div>
</form>
</div>
<div class="modal-footer" style="margin-top: 20px; text-align: right;">
<button id="cancelService" style="background-color: #e2e8f0; color: #4a5568; border: none; border-radius: 4px; padding: 10px 16px; margin-right: 10px; cursor: pointer; font-weight: 500;">取消</button>
<button id="confirmService" style="background-color: #4c6ef5; color: white; border: none; border-radius: 4px; padding: 10px 16px; cursor: pointer; font-weight: 500;">创建服务</button>
</div>
</div>
`;
// 获取shadowRoot
const shadowRoot = this.shadowRoot;
// 添加到DOM
shadowRoot.appendChild(modal);
// 获取DOM元素
const closeBtn = modal.querySelector('.close');
const cancelBtn = modal.querySelector('#cancelService');
const confirmBtn = modal.querySelector('#confirmService');
const form = modal.querySelector('#serviceCreationForm');
// 关闭对话框
const closeModal = () => {
if (modal.parentNode === shadowRoot) {
shadowRoot.removeChild(modal);
}
};
// 关闭按钮事件
closeBtn.addEventListener('click', closeModal);
cancelBtn.addEventListener('click', closeModal);
// 确认按钮事件
confirmBtn.addEventListener('click', async () => {
// 表单验证
const serviceClassName = modal.querySelector('#serviceClassName').value.trim();
const serviceName = modal.querySelector('#serviceName').value.trim();
const serviceNameCN = modal.querySelector('#serviceNameCN').value.trim();
const serviceDescription = modal.querySelector('#serviceDescription').value.trim();
if (!serviceClassName || !serviceName || !serviceNameCN) {
alert('服务类名、服务名称和中文名称是必填项!');
return;
}
// 验证类名格式 - 必须以XN开头
const classNameRegex = /^XN[A-Za-z0-9]*$/;
if (!classNameRegex.test(serviceClassName)) {
alert('服务类名必须以XN开头并且只能包含字母和数字');
return;
}
try {
// 禁用确认按钮,显示加载状态
confirmBtn.disabled = true;
confirmBtn.textContent = '创建中...';
// 构建服务数据
const serviceData = {
ClassName: serviceClassName,
ServiceName: serviceName,
ServiceName_CN: serviceNameCN,
Description: serviceDescription
};
// 调用API创建服务
await this.createService(serviceData);
// 关闭对话框
closeModal();
// 刷新服务列表
this.init();
// 显示成功提示
alert('服务创建成功!');
} catch (error) {
alert(`创建服务失败: ${error.message}`);
// 恢复按钮状态
confirmBtn.disabled = false;
confirmBtn.textContent = '创建服务';
}
});
}
// 创建服务的API
async createService(serviceData) {
try {
// 确保所有必要字段都存在
const requiredData = {
ClassName: serviceData.ClassName,
ServiceName: serviceData.ServiceName,
ServiceName_CN: serviceData.ServiceName_CN,
Description: serviceData.Description || ''
};
const response = await fetch('/api/services', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requiredData)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `HTTP错误! 状态码: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('创建服务失败:', error);
throw error;
}
}
}
customElements.define('service-development', ServiceDevelopment);