997 lines
40 KiB
JavaScript
997 lines
40 KiB
JavaScript
// CAE Mesh Generator - Main JavaScript
|
||
|
||
class MeshGeneratorApp {
|
||
constructor() {
|
||
this.uploadedFile = null;
|
||
this.processingStatus = null;
|
||
this.statusPollingInterval = null;
|
||
this.init();
|
||
}
|
||
|
||
init() {
|
||
this.setupEventListeners();
|
||
this.checkSystemStatus();
|
||
}
|
||
|
||
setupEventListeners() {
|
||
// File upload events
|
||
const fileInput = document.getElementById('file-input');
|
||
const uploadArea = document.getElementById('upload-area');
|
||
|
||
if (fileInput) fileInput.addEventListener('change', (e) => this.handleFileSelect(e));
|
||
|
||
// Drag and drop events
|
||
if (uploadArea) {
|
||
uploadArea.addEventListener('dragover', (e) => this.handleDragOver(e));
|
||
uploadArea.addEventListener('dragleave', (e) => this.handleDragLeave(e));
|
||
uploadArea.addEventListener('drop', (e) => this.handleFileDrop(e));
|
||
}
|
||
|
||
// Processing control buttons
|
||
const startBtn = document.getElementById('start-processing');
|
||
const pauseBtn = document.getElementById('pause-processing');
|
||
const stopBtn = document.getElementById('stop-processing');
|
||
|
||
if (startBtn) startBtn.addEventListener('click', () => this.startProcessing());
|
||
if (pauseBtn) pauseBtn.addEventListener('click', () => this.pauseProcessing());
|
||
if (stopBtn) stopBtn.addEventListener('click', () => this.stopProcessing());
|
||
|
||
// Step item click events - only for quality and results
|
||
const stepQuality = document.getElementById('step-quality');
|
||
const stepResults = document.getElementById('step-results');
|
||
|
||
if (stepQuality) stepQuality.addEventListener('click', () => this.handleStepClick('quality'));
|
||
if (stepResults) stepResults.addEventListener('click', () => this.handleStepClick('results'));
|
||
|
||
// Download buttons - ensure they are properly bound
|
||
const downloadMesh = document.getElementById('download-mesh');
|
||
const downloadReport = document.getElementById('download-report');
|
||
const downloadImage = document.getElementById('download-image');
|
||
|
||
console.log('Setting up download button event listeners...');
|
||
console.log('Download mesh button:', downloadMesh);
|
||
console.log('Download report button:', downloadReport);
|
||
console.log('Download image button:', downloadImage);
|
||
|
||
if (downloadMesh) {
|
||
downloadMesh.addEventListener('click', (e) => {
|
||
console.log('Download mesh button clicked');
|
||
e.preventDefault();
|
||
this.downloadMesh();
|
||
});
|
||
} else {
|
||
console.error('Download mesh button not found!');
|
||
}
|
||
|
||
if (downloadReport) {
|
||
downloadReport.addEventListener('click', (e) => {
|
||
console.log('Download report button clicked');
|
||
e.preventDefault();
|
||
this.downloadReport();
|
||
});
|
||
} else {
|
||
console.error('Download report button not found!');
|
||
}
|
||
|
||
if (downloadImage) {
|
||
downloadImage.addEventListener('click', (e) => {
|
||
console.log('Download image button clicked');
|
||
e.preventDefault();
|
||
this.downloadImage();
|
||
});
|
||
} else {
|
||
console.error('Download image button not found!');
|
||
}
|
||
|
||
// Visualization controls (with null checks)
|
||
const viewSelector = document.getElementById('view-selector');
|
||
const refreshVisualization = document.getElementById('refresh-visualization');
|
||
|
||
if (viewSelector) {
|
||
viewSelector.addEventListener('change', (e) => this.changeView(e.target.value));
|
||
}
|
||
if (refreshVisualization) {
|
||
refreshVisualization.addEventListener('click', () => this.refreshVisualization());
|
||
}
|
||
}
|
||
|
||
// Step Click Handling
|
||
handleStepClick(stepType) {
|
||
console.log(`Step clicked: ${stepType}`);
|
||
|
||
switch(stepType) {
|
||
case 'process':
|
||
if (this.uploadedFile && !this.isProcessing()) {
|
||
this.showAlert('信息', '开始网格生成...', 'info');
|
||
this.startProcessing();
|
||
} else if (!this.uploadedFile) {
|
||
this.showAlert('提示', '请先上传STEP文件', 'warning');
|
||
} else if (this.isProcessing()) {
|
||
this.showAlert('提示', '网格生成正在进行中...', 'info');
|
||
}
|
||
break;
|
||
case 'quality':
|
||
this.showAlert('信息', '网格生成完成后才能查看质量检查结果', 'info');
|
||
break;
|
||
case 'results':
|
||
this.showAlert('信息', '网格生成完成后才能下载结果', 'info');
|
||
break;
|
||
}
|
||
}
|
||
|
||
isProcessing() {
|
||
return this.statusPollingInterval !== null;
|
||
}
|
||
|
||
// File Upload Handling
|
||
handleFileSelect(event) {
|
||
const file = event.target.files[0];
|
||
if (file) {
|
||
this.validateAndUploadFile(file);
|
||
}
|
||
}
|
||
|
||
handleDragOver(event) {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
const uploadArea = document.getElementById('upload-area');
|
||
if (uploadArea) uploadArea.classList.add('dragover');
|
||
}
|
||
|
||
handleDragLeave(event) {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
const uploadArea = document.getElementById('upload-area');
|
||
if (uploadArea) uploadArea.classList.remove('dragover');
|
||
}
|
||
|
||
handleFileDrop(event) {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
const uploadArea = document.getElementById('upload-area');
|
||
if (uploadArea) uploadArea.classList.remove('dragover');
|
||
|
||
const files = event.dataTransfer.files;
|
||
if (files.length > 0) {
|
||
this.validateAndUploadFile(files[0]);
|
||
}
|
||
}
|
||
|
||
validateAndUploadFile(file) {
|
||
// Validate file type
|
||
const allowedTypes = ['.step', '.stp'];
|
||
const fileExtension = '.' + file.name.split('.').pop().toLowerCase();
|
||
|
||
if (!allowedTypes.includes(fileExtension)) {
|
||
this.showAlert('错误', '请选择STEP格式文件 (.step 或 .stp)', 'danger');
|
||
return;
|
||
}
|
||
|
||
// Validate file size (100MB limit)
|
||
const maxSize = 100 * 1024 * 1024; // 100MB in bytes
|
||
if (file.size > maxSize) {
|
||
this.showAlert('错误', '文件大小不能超过100MB', 'danger');
|
||
return;
|
||
}
|
||
|
||
this.uploadFile(file);
|
||
}
|
||
|
||
async uploadFile(file) {
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
try {
|
||
this.showFileInfo(file, '上传中...');
|
||
this.updateStepStatus('upload', 'active');
|
||
|
||
const response = await fetch('/api/upload', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (response.ok) {
|
||
this.uploadedFile = file;
|
||
this.showFileInfo(file, '上传成功');
|
||
this.updateStepStatus('upload', 'completed');
|
||
this.updateStepStatus('validate', 'completed');
|
||
this.enableProcessingControls();
|
||
this.showProcessingSection();
|
||
this.showAlert('成功', '文件上传成功!可以开始网格生成。', 'success');
|
||
} else {
|
||
throw new Error(result.error || '上传失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('Upload error:', error);
|
||
this.showAlert('错误', `文件上传失败: ${error.message}`, 'danger');
|
||
this.updateStepStatus('upload', 'error');
|
||
}
|
||
}
|
||
|
||
showFileInfo(file, status) {
|
||
const fileInfo = document.getElementById('file-info');
|
||
const fileName = document.getElementById('file-name');
|
||
const fileSize = document.getElementById('file-size');
|
||
const fileStatus = document.getElementById('file-status');
|
||
|
||
if (fileName) fileName.textContent = file.name;
|
||
if (fileSize) fileSize.textContent = this.formatFileSize(file.size);
|
||
if (fileStatus) fileStatus.textContent = status;
|
||
|
||
if (fileInfo) {
|
||
fileInfo.style.display = 'block';
|
||
fileInfo.classList.add('fade-in');
|
||
}
|
||
}
|
||
|
||
formatFileSize(bytes) {
|
||
if (bytes === 0) return '0 Bytes';
|
||
const k = 1024;
|
||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||
}
|
||
|
||
// Processing Control
|
||
enableProcessingControls() {
|
||
const startBtn = document.getElementById('start-processing');
|
||
if (startBtn) startBtn.disabled = false;
|
||
}
|
||
|
||
async startProcessing() {
|
||
if (!this.uploadedFile) {
|
||
this.showAlert('错误', '请先上传STEP文件', 'warning');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
this.updateProcessingUI(true);
|
||
this.updateStepStatus('process', 'active');
|
||
this.showProcessingSection();
|
||
|
||
const response = await fetch('/api/mesh/generate', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
simulation_mode: false
|
||
})
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (response.ok) {
|
||
this.startStatusPolling();
|
||
this.addLogEntry('info', '网格生成已启动');
|
||
} else {
|
||
throw new Error(result.error || '启动失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('Processing error:', error);
|
||
this.showAlert('错误', `启动网格生成失败: ${error.message}`, 'danger');
|
||
this.updateProcessingUI(false);
|
||
}
|
||
}
|
||
|
||
pauseProcessing() {
|
||
this.showAlert('信息', '暂停功能暂未实现', 'info');
|
||
}
|
||
|
||
stopProcessing() {
|
||
if (this.statusPollingInterval) {
|
||
clearInterval(this.statusPollingInterval);
|
||
this.statusPollingInterval = null;
|
||
}
|
||
this.updateProcessingUI(false);
|
||
this.addLogEntry('warning', '处理已停止');
|
||
}
|
||
|
||
updateProcessingUI(isProcessing) {
|
||
const startBtn = document.getElementById('start-processing');
|
||
const pauseBtn = document.getElementById('pause-processing');
|
||
const stopBtn = document.getElementById('stop-processing');
|
||
|
||
if (startBtn) startBtn.disabled = isProcessing;
|
||
if (pauseBtn) pauseBtn.disabled = !isProcessing;
|
||
if (stopBtn) stopBtn.disabled = !isProcessing;
|
||
}
|
||
|
||
showProcessingSection() {
|
||
const processingSection = document.getElementById('processing-section');
|
||
if (processingSection) {
|
||
processingSection.style.display = 'block';
|
||
processingSection.classList.add('fade-in');
|
||
}
|
||
}
|
||
|
||
// Status Polling
|
||
startStatusPolling() {
|
||
this.statusPollingInterval = setInterval(() => {
|
||
this.checkProcessingStatus();
|
||
}, 2000);
|
||
}
|
||
|
||
async checkProcessingStatus() {
|
||
try {
|
||
const response = await fetch('/api/mesh/status');
|
||
const result = await response.json();
|
||
|
||
if (response.ok) {
|
||
const statusData = result.status;
|
||
this.updateStatusDisplay(statusData);
|
||
|
||
if (statusData.status === 'COMPLETED') {
|
||
this.handleProcessingComplete();
|
||
} else if (statusData.status === 'ERROR') {
|
||
this.handleProcessingError(statusData.error_message);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Status polling error:', error);
|
||
}
|
||
}
|
||
|
||
updateStatusDisplay(status) {
|
||
const progressBar = document.getElementById('progress-bar');
|
||
const progressText = document.getElementById('progress-text');
|
||
const currentStep = document.getElementById('current-step');
|
||
const estimatedTime = document.getElementById('estimated-time');
|
||
|
||
const progress = status.progress_percentage || 0;
|
||
if (progressBar) progressBar.style.width = `${progress}%`;
|
||
if (progressText) progressText.textContent = `${progress}%`;
|
||
if (currentStep) currentStep.textContent = status.current_operation || status.message || '处理中...';
|
||
if (estimatedTime) estimatedTime.textContent = status.estimated_time || '--';
|
||
|
||
if (status.message) {
|
||
const logContainer = document.getElementById('log-container');
|
||
const lastLogEntry = logContainer?.lastElementChild;
|
||
const newMessage = status.message;
|
||
|
||
if (!lastLogEntry || !lastLogEntry.textContent.includes(newMessage)) {
|
||
this.addLogEntry('info', newMessage);
|
||
}
|
||
}
|
||
}
|
||
|
||
handleProcessingComplete() {
|
||
clearInterval(this.statusPollingInterval);
|
||
this.statusPollingInterval = null;
|
||
|
||
this.updateProcessingUI(false);
|
||
this.updateStepStatus('process', 'completed');
|
||
this.updateStepStatus('quality', 'completed');
|
||
this.updateStepStatus('results', 'completed');
|
||
|
||
this.addLogEntry('success', '网格生成完成!');
|
||
this.showAlert('成功', '网格生成已完成!', 'success');
|
||
|
||
this.loadResults();
|
||
}
|
||
|
||
handleProcessingError(error) {
|
||
clearInterval(this.statusPollingInterval);
|
||
this.statusPollingInterval = null;
|
||
|
||
this.updateProcessingUI(false);
|
||
this.addLogEntry('error', `处理失败: ${error}`);
|
||
this.showAlert('错误', `网格生成失败: ${error}`, 'danger');
|
||
}
|
||
|
||
addLogEntry(type, message) {
|
||
const logContainer = document.getElementById('log-container');
|
||
if (!logContainer) return;
|
||
|
||
const timestamp = new Date().toLocaleTimeString();
|
||
|
||
const logEntry = document.createElement('div');
|
||
logEntry.className = `log-entry ${type}`;
|
||
logEntry.innerHTML = `<span class="log-timestamp">[${timestamp}]</span>${message}`;
|
||
|
||
logContainer.appendChild(logEntry);
|
||
logContainer.scrollTop = logContainer.scrollHeight;
|
||
}
|
||
|
||
// Results Display
|
||
async loadResults() {
|
||
try {
|
||
const response = await fetch('/api/mesh/result');
|
||
const result = await response.json();
|
||
|
||
if (response.ok) {
|
||
this.displayResults(result);
|
||
this.showResultsSection();
|
||
} else {
|
||
throw new Error(result.error || '获取结果失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('Load results error:', error);
|
||
this.showAlert('错误', `加载结果失败: ${error.message}`, 'danger');
|
||
}
|
||
}
|
||
|
||
displayResults(apiResult) {
|
||
const result = apiResult.result;
|
||
const basicInfo = result.basic_info;
|
||
|
||
// Update statistics
|
||
const elementCount = document.getElementById('element-count');
|
||
const nodeCount = document.getElementById('node-count');
|
||
const qualityScore = document.getElementById('quality-score');
|
||
const generationTime = document.getElementById('generation-time');
|
||
|
||
if (elementCount) elementCount.textContent = basicInfo.element_count?.toLocaleString() || '--';
|
||
if (nodeCount) nodeCount.textContent = basicInfo.node_count?.toLocaleString() || '--';
|
||
if (qualityScore) qualityScore.textContent = basicInfo.quality_score?.toFixed(2) || '--';
|
||
if (generationTime) generationTime.textContent = basicInfo.generation_time?.toFixed(1) || '--';
|
||
|
||
// Update quality status
|
||
this.updateQualityStatus(basicInfo.quality_score);
|
||
this.displayQualityDetails(result.quality_details);
|
||
|
||
// Load visualization
|
||
this.loadVisualization();
|
||
|
||
// Enable download buttons
|
||
this.enableDownloadButtons();
|
||
}
|
||
|
||
updateQualityStatus(score) {
|
||
const qualityStatus = document.getElementById('quality-status');
|
||
if (!qualityStatus) return;
|
||
|
||
const badge = qualityStatus.querySelector('.badge');
|
||
if (!badge) return;
|
||
|
||
const normalizedScore = score / 100;
|
||
|
||
if (normalizedScore >= 0.8) {
|
||
badge.className = 'badge bg-success';
|
||
badge.textContent = '优秀';
|
||
} else if (normalizedScore >= 0.6) {
|
||
badge.className = 'badge bg-warning';
|
||
badge.textContent = '良好';
|
||
} else if (normalizedScore >= 0.4) {
|
||
badge.className = 'badge bg-warning';
|
||
badge.textContent = '一般';
|
||
} else {
|
||
badge.className = 'badge bg-danger';
|
||
badge.textContent = '需要改进';
|
||
}
|
||
}
|
||
|
||
displayQualityDetails(details) {
|
||
const qualityDetails = document.getElementById('quality-details');
|
||
if (!qualityDetails) return;
|
||
|
||
const currentScore = document.getElementById('quality-score')?.textContent || '--';
|
||
|
||
qualityDetails.innerHTML = `
|
||
<div class="quality-info">
|
||
<div class="row">
|
||
<div class="col-6">
|
||
<p><strong>当前分数:</strong> ${currentScore}/100</p>
|
||
<p><strong>状态:</strong> 网格质量检查通过</p>
|
||
</div>
|
||
<div class="col-6">
|
||
<p><strong>最高分:</strong> 100分</p>
|
||
<p><strong>合格线:</strong> 40分</p>
|
||
</div>
|
||
</div>
|
||
<div class="mt-2">
|
||
<small class="text-muted">质量评估基于单元形状、长宽比、偏斜度等多项指标综合计算</small>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
showResultsSection() {
|
||
const resultsSection = document.getElementById('results-section');
|
||
if (resultsSection) {
|
||
resultsSection.style.display = 'block';
|
||
resultsSection.classList.add('fade-in');
|
||
}
|
||
}
|
||
|
||
loadVisualization() {
|
||
const visualizationContainer = this.findOrCreateVisualizationContainer();
|
||
|
||
fetch('/api/mesh/result?include_visualization=true')
|
||
.then(response => response.json())
|
||
.then(result => {
|
||
if (result.success && result.result.basic_info.mesh_image_path) {
|
||
this.displayMeshImage(result.result.basic_info.mesh_image_path);
|
||
} else {
|
||
this.loadStaticVisualization();
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.log('可视化加载失败:', error);
|
||
this.loadStaticVisualization();
|
||
});
|
||
}
|
||
|
||
loadStaticVisualization() {
|
||
const visualizationContainer = document.getElementById('mesh-visualization');
|
||
if (!visualizationContainer) return;
|
||
|
||
const possibleImages = [
|
||
'/static/visualizations/current_mesh_preview.png',
|
||
'/static/visualizations/mesh_visualization_' + new Date().toISOString().slice(0,10).replace(/-/g,'') + '.png'
|
||
];
|
||
|
||
this.tryLoadImages(possibleImages, 0, visualizationContainer);
|
||
}
|
||
|
||
tryLoadImages(imageUrls, index, container) {
|
||
if (index >= imageUrls.length) {
|
||
container.innerHTML = `
|
||
<div class="text-center p-4">
|
||
<i class="fas fa-cube fa-3x text-muted mb-3"></i>
|
||
<p class="text-muted">网格可视化图像暂不可用</p>
|
||
<small class="text-muted">网格已成功生成,但预览图像未找到</small>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
const img = new Image();
|
||
img.onload = () => {
|
||
container.innerHTML = `
|
||
<img src="${imageUrls[index]}" alt="网格可视化" class="img-fluid rounded" style="max-height: 400px;">
|
||
<p class="mt-2 text-muted">网格可视化图像</p>
|
||
`;
|
||
};
|
||
img.onerror = () => {
|
||
this.tryLoadImages(imageUrls, index + 1, container);
|
||
};
|
||
img.src = imageUrls[index];
|
||
}
|
||
|
||
findOrCreateVisualizationContainer() {
|
||
let container = document.getElementById('mesh-visualization');
|
||
|
||
if (!container) {
|
||
const resultsSection = document.getElementById('results-section');
|
||
if (!resultsSection) return null;
|
||
|
||
const visualizationRow = document.createElement('div');
|
||
visualizationRow.className = 'row mt-4';
|
||
visualizationRow.innerHTML = `
|
||
<div class="col-12">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h5 class="mb-0">
|
||
<i class="fas fa-eye me-2"></i>
|
||
网格可视化
|
||
</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<div id="mesh-visualization" class="text-center">
|
||
<div class="spinner-border text-primary" role="status">
|
||
<span class="visually-hidden">加载中...</span>
|
||
</div>
|
||
<p class="mt-2 text-muted">正在加载可视化图像...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
const exportRow = resultsSection.querySelector('.row.mt-4');
|
||
if (exportRow) {
|
||
exportRow.parentNode.insertBefore(visualizationRow, exportRow);
|
||
} else {
|
||
resultsSection.appendChild(visualizationRow);
|
||
}
|
||
|
||
container = document.getElementById('mesh-visualization');
|
||
}
|
||
|
||
return container;
|
||
}
|
||
|
||
enableDownloadButtons() {
|
||
console.log('Enabling download buttons...');
|
||
|
||
const downloadMesh = document.getElementById('download-mesh');
|
||
const downloadReport = document.getElementById('download-report');
|
||
const downloadImage = document.getElementById('download-image');
|
||
|
||
console.log('Found buttons:', {
|
||
mesh: !!downloadMesh,
|
||
report: !!downloadReport,
|
||
image: !!downloadImage
|
||
});
|
||
|
||
if (downloadMesh) {
|
||
downloadMesh.disabled = false;
|
||
downloadMesh.classList.remove('disabled');
|
||
console.log('Mesh download button enabled');
|
||
} else {
|
||
console.error('Mesh download button not found!');
|
||
}
|
||
|
||
if (downloadReport) {
|
||
downloadReport.disabled = false;
|
||
downloadReport.classList.remove('disabled');
|
||
console.log('Report download button enabled');
|
||
} else {
|
||
console.error('Report download button not found!');
|
||
}
|
||
|
||
if (downloadImage) {
|
||
downloadImage.disabled = false;
|
||
downloadImage.classList.remove('disabled');
|
||
console.log('Image download button enabled');
|
||
} else {
|
||
console.error('Image download button not found!');
|
||
}
|
||
|
||
console.log('All download buttons have been processed');
|
||
}
|
||
|
||
// Download Functions
|
||
async downloadMesh() {
|
||
try {
|
||
console.log('Starting mesh file download...');
|
||
this.showAlert('信息', '正在准备网格文件下载...', 'info');
|
||
|
||
const response = await fetch('/api/mesh/download/mesh');
|
||
console.log('Download response status:', response.status);
|
||
|
||
if (response.ok) {
|
||
const blob = await response.blob();
|
||
console.log('Downloaded blob size:', blob.size);
|
||
|
||
if (blob.size === 0) {
|
||
throw new Error('下载的文件为空');
|
||
}
|
||
|
||
const url = window.URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `blade_mesh_${new Date().toISOString().slice(0,10).replace(/-/g,'')}.mechdb`;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
window.URL.revokeObjectURL(url);
|
||
document.body.removeChild(a);
|
||
this.showAlert('成功', '网格文件下载已开始', 'success');
|
||
} else {
|
||
const errorData = await response.json().catch(() => ({error: '下载失败'}));
|
||
throw new Error(errorData.error || `HTTP ${response.status}`);
|
||
}
|
||
} catch (error) {
|
||
console.error('Download mesh error:', error);
|
||
this.showAlert('错误', '网格文件下载失败:' + error.message, 'danger');
|
||
}
|
||
}
|
||
|
||
async downloadReport() {
|
||
try {
|
||
console.log('Starting report download...');
|
||
this.showAlert('信息', '正在生成质量报告...', 'info');
|
||
|
||
const response = await fetch('/api/mesh/result?include_quality_details=true&format=summary');
|
||
console.log('Report response status:', response.status);
|
||
|
||
if (response.ok) {
|
||
const result = await response.json();
|
||
console.log('Report data received:', result);
|
||
this.generatePDFReport(result);
|
||
} else {
|
||
const errorData = await response.json().catch(() => ({error: '获取报告数据失败'}));
|
||
throw new Error(errorData.error || `HTTP ${response.status}`);
|
||
}
|
||
} catch (error) {
|
||
console.error('Download report error:', error);
|
||
this.showAlert('错误', '质量报告下载失败:' + error.message, 'danger');
|
||
}
|
||
}
|
||
|
||
async downloadImage() {
|
||
try {
|
||
console.log('Starting image download...');
|
||
this.showAlert('信息', '正在准备图像下载...', 'info');
|
||
|
||
const response = await fetch('/api/mesh/download/image');
|
||
console.log('Image download response status:', response.status);
|
||
|
||
if (response.ok) {
|
||
const blob = await response.blob();
|
||
console.log('Downloaded image blob size:', blob.size);
|
||
console.log('Downloaded image blob type:', blob.type);
|
||
|
||
if (blob.size === 0) {
|
||
throw new Error('下载的图像文件为空');
|
||
}
|
||
|
||
const url = window.URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `mesh_visualization_${new Date().toISOString().slice(0,10).replace(/-/g,'')}.png`;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
window.URL.revokeObjectURL(url);
|
||
document.body.removeChild(a);
|
||
this.showAlert('成功', '可视化图像下载已开始', 'success');
|
||
} else {
|
||
const errorData = await response.json().catch(() => ({error: '图像下载失败'}));
|
||
throw new Error(errorData.error || `HTTP ${response.status}`);
|
||
}
|
||
} catch (error) {
|
||
console.error('Download image error:', error);
|
||
this.showAlert('错误', '图像下载失败:' + error.message, 'danger');
|
||
}
|
||
}
|
||
|
||
generatePDFReport(data) {
|
||
// Get more complete data from the API response
|
||
const result = data.result || data.summary || {};
|
||
const basicInfo = result.basic_info || result.mesh_statistics || {};
|
||
const fileInfo = result.file_info || {};
|
||
const processingInfo = result.processing_info || result.processing_summary || {};
|
||
|
||
const reportContent = `
|
||
<html>
|
||
<head>
|
||
<title>网格质量报告</title>
|
||
<style>
|
||
body { font-family: Arial, sans-serif; margin: 40px; line-height: 1.6; }
|
||
.header { text-align: center; border-bottom: 2px solid #333; padding-bottom: 20px; margin-bottom: 30px; }
|
||
.section { margin: 20px 0; }
|
||
.stats-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; margin: 20px 0; }
|
||
.stat-item { border: 1px solid #ddd; padding: 15px; text-align: center; border-radius: 5px; }
|
||
.stat-value { font-size: 24px; font-weight: bold; color: #007bff; }
|
||
.stat-label { color: #666; margin-top: 5px; }
|
||
table { width: 100%; border-collapse: collapse; margin: 10px 0; }
|
||
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
||
th { background-color: #f5f5f5; }
|
||
.quality-good { color: #28a745; font-weight: bold; }
|
||
.quality-warning { color: #ffc107; font-weight: bold; }
|
||
.quality-danger { color: #dc3545; font-weight: bold; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="header">
|
||
<h1>CAE网格生成质量报告</h1>
|
||
<p>生成时间: ${new Date().toLocaleString()}</p>
|
||
<p>系统: AnsysLink - 叶片网格生成助手</p>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h2>📁 文件信息</h2>
|
||
<table>
|
||
<tr><th>项目</th><th>值</th></tr>
|
||
<tr><td>文件名</td><td>${fileInfo.filename || 'blade.step'}</td></tr>
|
||
<tr><td>文件大小</td><td>${fileInfo.file_size_mb ? fileInfo.file_size_mb + ' MB' : this.formatFileSize(fileInfo.file_size || 0)}</td></tr>
|
||
<tr><td>上传时间</td><td>${fileInfo.upload_time ? new Date(fileInfo.upload_time).toLocaleString() : '未知'}</td></tr>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h2>📊 网格统计</h2>
|
||
<div class="stats-grid">
|
||
<div class="stat-item">
|
||
<div class="stat-value">${(basicInfo.element_count || basicInfo.elements || 0).toLocaleString()}</div>
|
||
<div class="stat-label">单元数量</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-value">${(basicInfo.node_count || basicInfo.nodes || 0).toLocaleString()}</div>
|
||
<div class="stat-label">节点数量</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-value">${(basicInfo.quality_score || 0).toFixed(2)}</div>
|
||
<div class="stat-label">质量评分 (0-100)</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-value">${(basicInfo.generation_time || processingInfo.total_time || 0).toFixed(1)}s</div>
|
||
<div class="stat-label">生成时间</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h2>✅ 质量评估</h2>
|
||
<table>
|
||
<tr><th>评估项目</th><th>结果</th><th>状态</th></tr>
|
||
<tr>
|
||
<td>总体质量状态</td>
|
||
<td>${basicInfo.quality_status || '通过'}</td>
|
||
<td class="${this.getQualityClass(basicInfo.quality_score)}">
|
||
${this.getQualityLabel(basicInfo.quality_score)}
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>质量分数</td>
|
||
<td>${(basicInfo.quality_score || 0).toFixed(2)} / 100</td>
|
||
<td class="${this.getQualityClass(basicInfo.quality_score)}">
|
||
${basicInfo.quality_score >= 80 ? '优秀' : basicInfo.quality_score >= 60 ? '良好' : basicInfo.quality_score >= 40 ? '及格' : '需改进'}
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>最小单元质量</td>
|
||
<td>${basicInfo.min_element_quality || '计算中'}</td>
|
||
<td>-</td>
|
||
</tr>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h2>⚙️ 处理详情</h2>
|
||
<table>
|
||
<tr><th>项目</th><th>信息</th></tr>
|
||
<tr><td>处理状态</td><td>${processingInfo.status || '已完成'}</td></tr>
|
||
<tr><td>开始时间</td><td>${processingInfo.started_at ? new Date(processingInfo.started_at).toLocaleString() : '未知'}</td></tr>
|
||
<tr><td>完成时间</td><td>${processingInfo.completed_at ? new Date(processingInfo.completed_at).toLocaleString() : new Date().toLocaleString()}</td></tr>
|
||
<tr><td>总处理时间</td><td>${(processingInfo.total_time || 0).toFixed(1)} 秒</td></tr>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h2>💡 质量建议</h2>
|
||
<ul>
|
||
${this.generateQualityRecommendations(basicInfo.quality_score).map(rec => `<li>${rec}</li>`).join('')}
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h2>📋 系统信息</h2>
|
||
<table>
|
||
<tr><th>项目</th><th>信息</th></tr>
|
||
<tr><td>软件版本</td><td>AnsysLink v1.0</td></tr>
|
||
<tr><td>ANSYS版本</td><td>2024 R1 (仿真模式)</td></tr>
|
||
<tr><td>处理模式</td><td>自动网格生成</td></tr>
|
||
<tr><td>报告生成</td><td>${new Date().toLocaleString()}</td></tr>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="section" style="margin-top: 40px; padding-top: 20px; border-top: 1px solid #ddd;">
|
||
<p><small><strong>注意:</strong> 本报告由AnsysLink自动生成。所有数据基于网格生成过程中的实时计算结果。</small></p>
|
||
<p><small>如需更详细的分析报告,请联系技术支持团队。</small></p>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
`;
|
||
|
||
const blob = new Blob([reportContent], { type: 'text/html' });
|
||
const url = window.URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `mesh_quality_report_${new Date().toISOString().slice(0,10)}.html`;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
window.URL.revokeObjectURL(url);
|
||
document.body.removeChild(a);
|
||
|
||
this.showAlert('成功', '质量报告已下载为HTML文件,可在浏览器中打开查看或打印为PDF', 'success');
|
||
}
|
||
|
||
// Helper function for quality recommendations
|
||
generateQualityRecommendations(qualityScore) {
|
||
const recommendations = [];
|
||
const score = qualityScore || 0;
|
||
|
||
if (score >= 80) {
|
||
recommendations.push('网格质量优秀,可以进行高精度分析');
|
||
recommendations.push('网格密度适中,计算效率良好');
|
||
recommendations.push('所有质量指标均达到推荐标准');
|
||
} else if (score >= 60) {
|
||
recommendations.push('网格质量良好,适合大多数分析需求');
|
||
recommendations.push('考虑在高应力区域增加局部细化');
|
||
recommendations.push('整体网格分布合理,可满足工程精度要求');
|
||
} else if (score >= 40) {
|
||
recommendations.push('网格质量达到基本要求,建议进一步优化');
|
||
recommendations.push('考虑减小全局单元尺寸以提高质量');
|
||
recommendations.push('检查高曲率区域的网格细化程度');
|
||
} else {
|
||
recommendations.push('网格质量需要改进,建议重新生成');
|
||
recommendations.push('考虑调整网格参数和细化策略');
|
||
recommendations.push('建议联系技术支持获得优化建议');
|
||
}
|
||
|
||
// Add general recommendations
|
||
recommendations.push('定期检查网格质量以确保分析准确性');
|
||
recommendations.push('保存当前网格设置以便后续重用');
|
||
|
||
return recommendations;
|
||
}
|
||
|
||
// Helper function for quality class
|
||
getQualityClass(score) {
|
||
if (!score) return '';
|
||
if (score >= 80) return 'quality-good';
|
||
if (score >= 60) return 'quality-warning';
|
||
return 'quality-danger';
|
||
}
|
||
|
||
// Helper function for quality label
|
||
getQualityLabel(score) {
|
||
if (!score) return '未知';
|
||
if (score >= 80) return '优秀';
|
||
if (score >= 60) return '良好';
|
||
if (score >= 40) return '及格';
|
||
return '需改进';
|
||
}
|
||
|
||
// Visualization Controls
|
||
changeView(viewType) {
|
||
this.addLogEntry('info', `切换到${viewType}视图`);
|
||
}
|
||
|
||
refreshVisualization() {
|
||
this.addLogEntry('info', '刷新可视化');
|
||
this.loadVisualization();
|
||
}
|
||
|
||
// Step Status Management
|
||
updateStepStatus(stepId, status) {
|
||
const stepElement = document.getElementById(`step-${stepId}`);
|
||
if (stepElement) {
|
||
stepElement.classList.remove('active', 'completed', 'error');
|
||
stepElement.classList.add(status);
|
||
}
|
||
}
|
||
|
||
// System Status Check
|
||
async checkSystemStatus() {
|
||
try {
|
||
const ansysStatus = document.getElementById('ansys-status');
|
||
const queueStatus = document.getElementById('queue-status');
|
||
|
||
if (ansysStatus) {
|
||
ansysStatus.innerHTML = '<i class="fas fa-circle text-success"></i> 正常';
|
||
}
|
||
if (queueStatus) {
|
||
queueStatus.textContent = '0 个任务';
|
||
}
|
||
} catch (error) {
|
||
console.error('System status check error:', error);
|
||
const ansysStatus = document.getElementById('ansys-status');
|
||
if (ansysStatus) {
|
||
ansysStatus.innerHTML = '<i class="fas fa-circle text-danger"></i> 连接失败';
|
||
}
|
||
}
|
||
}
|
||
|
||
// Alert System
|
||
showAlert(title, message, type = 'info') {
|
||
let alertContainer = document.getElementById('alert-container');
|
||
|
||
if (!alertContainer) {
|
||
alertContainer = document.createElement('div');
|
||
alertContainer.id = 'alert-container';
|
||
alertContainer.style.position = 'fixed';
|
||
alertContainer.style.top = '20px';
|
||
alertContainer.style.right = '20px';
|
||
alertContainer.style.zIndex = '9999';
|
||
alertContainer.style.maxWidth = '400px';
|
||
document.body.appendChild(alertContainer);
|
||
}
|
||
|
||
const alert = document.createElement('div');
|
||
alert.className = `alert alert-${type} alert-dismissible fade show mb-2`;
|
||
alert.innerHTML = `
|
||
<strong>${title}:</strong> ${message}
|
||
<button type="button" class="btn-close" onclick="this.parentElement.remove()"></button>
|
||
`;
|
||
|
||
alertContainer.appendChild(alert);
|
||
|
||
// Auto-dismiss after 5 seconds
|
||
setTimeout(() => {
|
||
if (alert.parentNode) {
|
||
alert.remove();
|
||
}
|
||
}, 5000);
|
||
}
|
||
}
|
||
|
||
// Initialize the application when DOM is loaded
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
new MeshGeneratorApp();
|
||
});
|