deving
This commit is contained in:
@@ -1,215 +0,0 @@
|
||||
// static/history.js
|
||||
|
||||
// 页面加载完成后加载历史记录
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadHistory();
|
||||
});
|
||||
|
||||
async function loadHistory() {
|
||||
const fileList = document.getElementById('fileList');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/history', {
|
||||
method: 'POST'
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('获取历史记录失败');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.files && data.files.length > 0) {
|
||||
displayFileList(data.files);
|
||||
} else {
|
||||
fileList.innerHTML = `
|
||||
<div class="no-records">
|
||||
<div style="font-size: 48px; margin-bottom: 20px;">📁</div>
|
||||
<h3>暂无历史记录</h3>
|
||||
<p>还没有上传过STP文件</p>
|
||||
<button class="upload-btn" onclick="location.href='/upload'">
|
||||
上传第一个文件
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
fileList.innerHTML = `
|
||||
<div class="no-records">
|
||||
<div style="font-size: 48px; margin-bottom: 20px;">❌</div>
|
||||
<h3>加载失败</h3>
|
||||
<p>${error.message}</p>
|
||||
<button class="upload-btn" onclick="loadHistory()">
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function displayFileList(files) {
|
||||
const fileList = document.getElementById('fileList');
|
||||
|
||||
fileList.innerHTML = files.map(file => `
|
||||
<div class="file-item" id="file-${file.filename}">
|
||||
<div class="file-header" onclick="toggleFileRecords('${file.filename}')">
|
||||
<div class="file-info">
|
||||
<div class="file-name">📄 ${file.filename}</div>
|
||||
<div class="file-meta">
|
||||
总记录: ${file.record_count} 条 |
|
||||
最后上传: ${formatDateTime(file.last_upload)}
|
||||
</div>
|
||||
</div>
|
||||
<button class="expand-btn" onclick="event.stopPropagation(); toggleFileRecords('${file.filename}')">
|
||||
➕
|
||||
</button>
|
||||
</div>
|
||||
<div class="record-list" id="records-${file.filename}">
|
||||
<div class="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>正在加载记录详情...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function toggleFileRecords(filename) {
|
||||
const fileItem = document.getElementById(`file-${filename}`);
|
||||
const recordList = document.getElementById(`records-${filename}`);
|
||||
const expandBtn = fileItem.querySelector('.expand-btn');
|
||||
|
||||
// 切换展开状态
|
||||
if (fileItem.classList.contains('expanded')) {
|
||||
fileItem.classList.remove('expanded');
|
||||
expandBtn.textContent = '➕';
|
||||
return;
|
||||
}
|
||||
|
||||
// 先设置展开状态
|
||||
fileItem.classList.add('expanded');
|
||||
expandBtn.textContent = '➖';
|
||||
|
||||
// 如果已经加载过数据,直接显示
|
||||
if (recordList.dataset.loaded === 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/history/${encodeURIComponent(filename)}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('获取记录详情失败');
|
||||
}
|
||||
|
||||
const records = await response.json();
|
||||
|
||||
if (records.length > 0) {
|
||||
recordList.innerHTML = records.map(record => `
|
||||
<div class="record-item" onclick="viewRecordDetails('${record.task_id}')">
|
||||
<div class="record-header">
|
||||
<div class="record-time">
|
||||
📅 ${formatDateTime(record.upload_time)}
|
||||
</div>
|
||||
<div class="record-status status-${record.status}">
|
||||
${getStatusText(record.status)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="record-details">
|
||||
<div>文件大小: ${formatFileSize(record.file_size)}</div>
|
||||
<div>任务ID: ${record.task_id}</div>
|
||||
<div>状态: ${record.status}</div>
|
||||
${record.completed_at ? `<div>完成时间: ${formatDateTime(record.completed_at)}</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
recordList.innerHTML = `
|
||||
<div class="no-records">
|
||||
<p>暂无详细记录</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
recordList.dataset.loaded = 'true';
|
||||
|
||||
} catch (error) {
|
||||
recordList.innerHTML = `
|
||||
<div class="no-records">
|
||||
<p>❌ 加载失败: ${error.message}</p>
|
||||
<button class="upload-btn" onclick="toggleFileRecords('${filename}')">
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async function viewRecordDetails(taskId) {
|
||||
// 直接跳转到结果页面,由后端从数据库加载完整数据
|
||||
window.location.href = `/result/${taskId}`;
|
||||
}
|
||||
|
||||
async function reanalyzeTask(taskId) {
|
||||
try {
|
||||
const response = await fetch(`/api/reanalyze/${taskId}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('重新分析任务已启动,请稍后查看结果');
|
||||
// 跳转到分析状态页面
|
||||
window.location.href = `/status/${taskId}`;
|
||||
} else {
|
||||
throw new Error('重新分析失败');
|
||||
}
|
||||
} catch (error) {
|
||||
alert(`重新分析失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusText(status) {
|
||||
const statusMap = {
|
||||
'completed': '已完成',
|
||||
'processing': '处理中',
|
||||
'failed': '失败'
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
}
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
if (!bytes || 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];
|
||||
}
|
||||
|
||||
function formatDateTime(dateString) {
|
||||
if (!dateString) return 'N/A';
|
||||
// 后端已经返回格式化的时间字符串,直接返回
|
||||
return dateString;
|
||||
}
|
||||
|
||||
// 跳转到上传页面
|
||||
async function goToUpload() {
|
||||
try {
|
||||
// 上传页面需要GET请求,直接跳转
|
||||
window.location.href = '/';
|
||||
} catch (error) {
|
||||
console.error('跳转失败:', error);
|
||||
window.location.href = '/';
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到主页
|
||||
async function goToHome() {
|
||||
try {
|
||||
// 主页需要GET请求,直接跳转
|
||||
window.location.href = '/';
|
||||
} catch (error) {
|
||||
console.error('跳转失败:', error);
|
||||
window.location.href = '/';
|
||||
}
|
||||
}
|
||||
@@ -1,657 +0,0 @@
|
||||
// static/script.js
|
||||
let selectedFile = null;
|
||||
const uploadSection = document.getElementById('uploadSection');
|
||||
const uploadArea = document.getElementById('uploadArea');
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const uploadBtn = document.getElementById('uploadBtn');
|
||||
const loading = document.getElementById('loading');
|
||||
const resultsSection = document.getElementById('resultsSection');
|
||||
const errorMessage = document.getElementById('errorMessage');
|
||||
const taskInfo = document.getElementById('taskInfo');
|
||||
const geometryData = document.getElementById('geometryData');
|
||||
const boundingBoxData = document.getElementById('boundingBoxData');
|
||||
const topologyData = document.getElementById('topologyData');
|
||||
const featuresData = document.getElementById('featuresData');
|
||||
const recommendationsData = document.getElementById('recommendationsData');
|
||||
const metricsData = document.getElementById('metricsData');
|
||||
const analysisInfo = document.getElementById('analysisInfo');
|
||||
|
||||
// 页面加载时初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('页面加载完成');
|
||||
showUploadSection();
|
||||
});
|
||||
|
||||
// 显示上传区域
|
||||
function showUploadSection() {
|
||||
uploadSection.style.display = 'block';
|
||||
resultsSection.style.display = 'none';
|
||||
resetUploadArea();
|
||||
}
|
||||
|
||||
// 显示结果区域
|
||||
function showResultsSection() {
|
||||
uploadSection.style.display = 'none';
|
||||
resultsSection.style.display = 'block';
|
||||
}
|
||||
|
||||
// 重置上传区域
|
||||
function resetUploadArea() {
|
||||
selectedFile = null;
|
||||
uploadArea.innerHTML = `
|
||||
<div class="upload-icon">📁</div>
|
||||
<h3>拖放文件到此处或点击选择</h3>
|
||||
<p>最大文件大小: 100MB</p>
|
||||
<input type="file" id="fileInput" class="file-input" accept=".stp,.step">
|
||||
<button class="upload-btn" onclick="document.getElementById('fileInput').click()">
|
||||
选择文件
|
||||
</button>
|
||||
`;
|
||||
uploadBtn.disabled = true;
|
||||
hideError();
|
||||
loading.style.display = 'none';
|
||||
|
||||
// 重新绑定事件
|
||||
const newFileInput = document.getElementById('fileInput');
|
||||
newFileInput.addEventListener('change', (e) => {
|
||||
if (e.target.files.length > 0) {
|
||||
handleFileSelect(e.target.files[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 拖放功能
|
||||
uploadArea.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
uploadArea.classList.add('dragover');
|
||||
});
|
||||
|
||||
uploadArea.addEventListener('dragleave', () => {
|
||||
uploadArea.classList.remove('dragover');
|
||||
});
|
||||
|
||||
uploadArea.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
uploadArea.classList.remove('dragover');
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
handleFileSelect(files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
// 文件选择
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
if (e.target.files.length > 0) {
|
||||
handleFileSelect(e.target.files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
function handleFileSelect(file) {
|
||||
if (!file.name.toLowerCase().endsWith('.stp') && !file.name.toLowerCase().endsWith('.step')) {
|
||||
showError('请选择STP或STEP格式的文件');
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.size > 100 * 1024 * 1024) {
|
||||
showError('文件大小不能超过100MB');
|
||||
return;
|
||||
}
|
||||
|
||||
selectedFile = file;
|
||||
uploadArea.innerHTML = `
|
||||
<div class="upload-icon">✅</div>
|
||||
<h3>已选择文件</h3>
|
||||
<p><strong>${file.name}</strong></p>
|
||||
<p>大小: ${(file.size / 1024 / 1024).toFixed(2)} MB</p>
|
||||
`;
|
||||
uploadBtn.disabled = false;
|
||||
hideError();
|
||||
}
|
||||
|
||||
async function uploadFile() {
|
||||
if (!selectedFile) return;
|
||||
|
||||
loading.style.display = 'block';
|
||||
uploadBtn.disabled = true;
|
||||
hideError();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', selectedFile);
|
||||
|
||||
try {
|
||||
const response = await fetch('/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`上传失败: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('上传结果:', result);
|
||||
|
||||
// 开始轮询任务状态
|
||||
pollTaskStatus(result.task_id);
|
||||
|
||||
} catch (error) {
|
||||
showError('上传失败: ' + error.message);
|
||||
loading.style.display = 'none';
|
||||
uploadBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function pollTaskStatus(taskId) {
|
||||
try {
|
||||
const response = await fetch(`/status/${taskId}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const task = await response.json();
|
||||
|
||||
console.log('任务状态:', task.status);
|
||||
console.log('完整任务数据:', task);
|
||||
|
||||
updateTaskInfo(task);
|
||||
|
||||
if (task.status === 'completed') {
|
||||
loading.style.display = 'none';
|
||||
// 跳转到统一的分析结果页面
|
||||
window.location.href = `/result/${task.task_id}`;
|
||||
} else if (task.status === 'failed') {
|
||||
loading.style.display = 'none';
|
||||
showError('分析失败: ' + (task.error || '未知错误'));
|
||||
uploadBtn.disabled = false;
|
||||
} else {
|
||||
setTimeout(() => pollTaskStatus(taskId), 1000);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('轮询错误:', error);
|
||||
loading.style.display = 'none';
|
||||
showError('查询状态失败: ' + error.message);
|
||||
uploadBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function updateTaskInfo(task) {
|
||||
taskInfo.innerHTML = `
|
||||
<div class="data-item">
|
||||
<div class="data-label">📋 任务ID</div>
|
||||
<div class="data-value">${task.task_id || 'N/A'}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">🔄 状态</div>
|
||||
<div class="data-value">
|
||||
<span class="status-badge status-${task.status}">${getStatusText(task.status)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">📁 文件名</div>
|
||||
<div class="data-value">${task.filename || 'N/A'}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">📏 文件大小</div>
|
||||
<div class="data-value">${task.file_size ? formatFileSize(task.file_size) : 'N/A'}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function displayAllResults(task) {
|
||||
console.log('显示所有结果:', task);
|
||||
|
||||
displayGeometryData(task);
|
||||
displayBoundingBoxData(task);
|
||||
displayTopologyData(task);
|
||||
|
||||
// 添加模具型腔数据显示
|
||||
if (task.cavity_data) {
|
||||
displayCavityData(task.cavity_data);
|
||||
}
|
||||
if (task.key_info) {
|
||||
displayKeyInfo(task.key_info);
|
||||
}
|
||||
|
||||
displayFeaturesData(task);
|
||||
displayRecommendationsData(task);
|
||||
displayMetricsData(task);
|
||||
displayAnalysisInfo(task);
|
||||
}
|
||||
|
||||
function displayGeometryData(task) {
|
||||
if (task.geometry_data) {
|
||||
const geo = task.geometry_data;
|
||||
geometryData.innerHTML = `
|
||||
<div class="data-item">
|
||||
<div class="data-label">📦 体积</div>
|
||||
<div class="data-value">
|
||||
${geo.volume ? formatNumber(geo.volume) : 'N/A'}
|
||||
<span class="data-unit">mm³</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">📐 表面积</div>
|
||||
<div class="data-value">
|
||||
${geo.surface_area ? formatNumber(geo.surface_area) : 'N/A'}
|
||||
<span class="data-unit">mm²</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">📏 体积表面积比</div>
|
||||
<div class="data-value">
|
||||
${geo.volume && geo.surface_area ? (geo.volume / geo.surface_area).toFixed(4) : 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
geometryData.innerHTML = '<div class="data-item"><div class="data-value">无几何数据</div></div>';
|
||||
}
|
||||
}
|
||||
|
||||
function displayBoundingBoxData(task) {
|
||||
if (task.geometry_data && task.geometry_data.bounding_box) {
|
||||
const bbox = task.geometry_data.bounding_box;
|
||||
boundingBoxData.innerHTML = `
|
||||
<div class="data-item coordinate-item">
|
||||
<div class="coordinate-label">📍 最小坐标</div>
|
||||
<div class="coordinate-value">
|
||||
X: ${bbox.min[0].toFixed(2)}<br>
|
||||
Y: ${bbox.min[1].toFixed(2)}<br>
|
||||
Z: ${bbox.min[2].toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-item coordinate-item">
|
||||
<div class="coordinate-label">📍 最大坐标</div>
|
||||
<div class="coordinate-value">
|
||||
X: ${bbox.max[0].toFixed(2)}<br>
|
||||
Y: ${bbox.max[1].toFixed(2)}<br>
|
||||
Z: ${bbox.max[2].toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">📏 尺寸</div>
|
||||
<div class="data-value">
|
||||
${bbox.dimensions[0].toFixed(2)} × ${bbox.dimensions[1].toFixed(2)} × ${bbox.dimensions[2].toFixed(2)}
|
||||
<span class="data-unit">mm</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
boundingBoxData.innerHTML = '<div class="data-item"><div class="data-value">无边界框数据</div></div>';
|
||||
}
|
||||
}
|
||||
|
||||
function displayTopologyData(task) {
|
||||
if (task.geometry_data && task.geometry_data.topology) {
|
||||
const topo = task.geometry_data.topology;
|
||||
topologyData.innerHTML = `
|
||||
<div class="data-item">
|
||||
<div class="data-label">🔺 面数</div>
|
||||
<div class="data-value">${topo.faces || 0}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">📏 边数</div>
|
||||
<div class="data-value">${topo.edges || 0}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">📍 顶点数</div>
|
||||
<div class="data-value">${topo.vertices || 0}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">📊 拓扑复杂度</div>
|
||||
<div class="data-value">${calculateTopologyComplexity(topo)}</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
topologyData.innerHTML = '<div class="data-item"><div class="data-value">无拓扑数据</div></div>';
|
||||
}
|
||||
}
|
||||
|
||||
function displayFeaturesData(task) {
|
||||
if (task.analysis_result && task.analysis_result.detected_features) {
|
||||
const features = task.analysis_result.detected_features;
|
||||
|
||||
if (features.length > 0) {
|
||||
featuresData.innerHTML = features.map(feature => `
|
||||
<div class="feature-item">
|
||||
<div class="feature-header">
|
||||
<div class="feature-type">${getFeatureTypeText(feature.feature_type)}</div>
|
||||
<div class="confidence-badge">置信度: ${(feature.confidence * 100).toFixed(0)}%</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">📍 位置</div>
|
||||
<div class="data-value">${feature.location.map(v => v.toFixed(2)).join(', ')}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">📏 尺寸</div>
|
||||
<div class="data-value">${feature.dimensions.map(v => v.toFixed(2)).join(' × ')} mm</div>
|
||||
</div>
|
||||
${feature.recommendations && feature.recommendations.length > 0 ? `
|
||||
<div class="data-item">
|
||||
<div class="data-label">💡 建议</div>
|
||||
<ul class="recommendation-list">
|
||||
${feature.recommendations.map(rec => `<li>${rec}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
featuresData.innerHTML = '<div class="data-item"><div class="data-value">未检测到明显特征</div></div>';
|
||||
}
|
||||
} else {
|
||||
featuresData.innerHTML = '<div class="data-item"><div class="data-value">无特征数据</div></div>';
|
||||
}
|
||||
}
|
||||
|
||||
function displayRecommendationsData(task) {
|
||||
if (task.analysis_result && task.analysis_result.design_recommendations) {
|
||||
const recommendations = task.analysis_result.design_recommendations;
|
||||
|
||||
if (recommendations.length > 0) {
|
||||
recommendationsData.innerHTML = recommendations.map(rec => `
|
||||
<div class="recommendation-item recommendation-${rec.priority}">
|
||||
<div class="recommendation-header">
|
||||
<div class="feature-type">${getRecommendationTypeText(rec.type)}</div>
|
||||
<div class="priority-badge priority-${rec.priority}">${getPriorityText(rec.priority)}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">📝 描述</div>
|
||||
<div class="data-value">${rec.description}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">📋 原因</div>
|
||||
<div class="data-value">${rec.reason}</div>
|
||||
</div>
|
||||
${Object.keys(rec.parameters).length > 0 ? `
|
||||
<div class="data-item">
|
||||
<div class="data-label">⚙️ 参数</div>
|
||||
<div class="data-value">${formatParameters(rec.parameters)}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
recommendationsData.innerHTML = '<div class="data-item"><div class="data-value">无设计建议</div></div>';
|
||||
}
|
||||
} else {
|
||||
recommendationsData.innerHTML = '<div class="data-item"><div class="data-value">无建议数据</div></div>';
|
||||
}
|
||||
}
|
||||
|
||||
function displayMetricsData(task) {
|
||||
if (task.analysis_result && task.analysis_result.quality_metrics) {
|
||||
const metrics = task.analysis_result.quality_metrics;
|
||||
metricsData.innerHTML = `
|
||||
<div class="metric-item">
|
||||
<div class="metric-label">体积利用率</div>
|
||||
<div class="metric-value ${getMetricClass(metrics.volume_utilization, 0.3, 0.6)}">
|
||||
${(metrics.volume_utilization * 100).toFixed(1)}%
|
||||
</div>
|
||||
<div class="data-value">${getVolumeUtilizationText(metrics.volume_utilization)}</div>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<div class="metric-label">拓扑复杂度</div>
|
||||
<div class="metric-value ${getMetricClass(metrics.topology_complexity, 0.3, 0.7, true)}">
|
||||
${metrics.topology_complexity.toFixed(2)}
|
||||
</div>
|
||||
<div class="data-value">${getComplexityText(metrics.topology_complexity)}</div>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<div class="metric-label">壁厚均匀性</div>
|
||||
<div class="metric-value ${getMetricClass(metrics.wall_uniformity, 0.6, 0.8)}">
|
||||
${(metrics.wall_uniformity * 100).toFixed(1)}%
|
||||
</div>
|
||||
<div class="data-value">${getUniformityText(metrics.wall_uniformity)}</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
metricsData.innerHTML = '<div class="data-item"><div class="data-value">无质量指标数据</div></div>';
|
||||
}
|
||||
}
|
||||
|
||||
function displayAnalysisInfo(task) {
|
||||
let infoHTML = '';
|
||||
|
||||
if (task.geometry_data) {
|
||||
const geo = task.geometry_data;
|
||||
infoHTML += `
|
||||
<div class="data-item">
|
||||
<div class="data-label">🔧 分析方法</div>
|
||||
<div class="data-value">${geo.analysis_method || '未知'}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (task.analysis_result) {
|
||||
const analysis = task.analysis_result;
|
||||
infoHTML += `
|
||||
<div class="data-item">
|
||||
<div class="data-label">✅ 分析状态</div>
|
||||
<div class="data-value">${task.status === 'completed' ? '分析完成' : '分析中'}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">📋 分析摘要</div>
|
||||
<div class="data-value">${analysis.analysis_summary || '无摘要'}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
infoHTML += `
|
||||
<div class="data-item">
|
||||
<div class="data-label">📅 处理时间</div>
|
||||
<div class="data-value">${task.completed_at ? new Date(task.completed_at).toLocaleString() : new Date().toLocaleString()}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
analysisInfo.innerHTML = infoHTML;
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
function getStatusText(status) {
|
||||
const statusMap = {
|
||||
'processing': '处理中',
|
||||
'completed': '已完成',
|
||||
'failed': '失败'
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
}
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
if (!bytes || 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];
|
||||
}
|
||||
|
||||
function formatNumber(num) {
|
||||
if (!num) return 'N/A';
|
||||
if (num >= 1000000) {
|
||||
return (num / 1000000).toFixed(2) + 'M';
|
||||
} else if (num >= 1000) {
|
||||
return (num / 1000).toFixed(2) + 'K';
|
||||
} else {
|
||||
return num.toFixed(2);
|
||||
}
|
||||
}
|
||||
|
||||
function calculateTopologyComplexity(topo) {
|
||||
const totalElements = (topo.faces || 0) + (topo.edges || 0) + (topo.vertices || 0);
|
||||
if (totalElements < 100) return '简单';
|
||||
if (totalElements < 1000) return '中等';
|
||||
return '复杂';
|
||||
}
|
||||
|
||||
function getFeatureTypeText(type) {
|
||||
const typeMap = {
|
||||
'thin_wall': '薄壁区域',
|
||||
'thick_wall': '厚壁区域',
|
||||
'rib_structure': '加强筋结构',
|
||||
'boss_feature': 'BOSS柱',
|
||||
'draft_angle': '拔模角度',
|
||||
'cooling_system': '冷却系统'
|
||||
};
|
||||
return typeMap[type] || type;
|
||||
}
|
||||
|
||||
function getRecommendationTypeText(type) {
|
||||
const typeMap = {
|
||||
'wall_thickness': '壁厚优化',
|
||||
'draft_angle': '拔模角度',
|
||||
'rib_design': '加强筋设计',
|
||||
'boss_design': 'BOSS柱设计',
|
||||
'cooling_system': '冷却系统'
|
||||
};
|
||||
return typeMap[type] || type;
|
||||
}
|
||||
|
||||
function getPriorityText(priority) {
|
||||
const priorityMap = {
|
||||
'high': '高优先级',
|
||||
'medium': '中优先级',
|
||||
'low': '低优先级'
|
||||
};
|
||||
return priorityMap[priority] || priority;
|
||||
}
|
||||
|
||||
function formatParameters(parameters) {
|
||||
const parameterMap = {
|
||||
'min_angle': '最小角度',
|
||||
'preferred_angle': '推荐角度'
|
||||
};
|
||||
|
||||
return Object.entries(parameters).map(([key, value]) => {
|
||||
const displayKey = parameterMap[key] || key;
|
||||
if (typeof value === 'number') {
|
||||
return `${displayKey}: ${value.toFixed(2)}`;
|
||||
}
|
||||
return `${displayKey}: ${value}`;
|
||||
}).join('; ');
|
||||
}
|
||||
|
||||
function getMetricClass(value, goodThreshold, excellentThreshold, reverse = false) {
|
||||
if (reverse) {
|
||||
if (value <= goodThreshold) return 'metric-good';
|
||||
if (value <= excellentThreshold) return 'metric-warning';
|
||||
return 'metric-poor';
|
||||
} else {
|
||||
if (value >= excellentThreshold) return 'metric-good';
|
||||
if (value >= goodThreshold) return 'metric-warning';
|
||||
return 'metric-poor';
|
||||
}
|
||||
}
|
||||
|
||||
function getVolumeUtilizationText(value) {
|
||||
if (value >= 0.6) return '优秀';
|
||||
if (value >= 0.3) return '良好';
|
||||
return '待优化';
|
||||
}
|
||||
|
||||
function getComplexityText(value) {
|
||||
if (value <= 0.3) return '简单';
|
||||
if (value <= 0.7) return '中等';
|
||||
return '复杂';
|
||||
}
|
||||
|
||||
function getUniformityText(value) {
|
||||
if (value >= 0.8) return '均匀';
|
||||
if (value >= 0.6) return '一般';
|
||||
return '不均匀';
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
errorMessage.textContent = message;
|
||||
errorMessage.style.display = 'block';
|
||||
}
|
||||
|
||||
function hideError() {
|
||||
errorMessage.style.display = 'none';
|
||||
}
|
||||
|
||||
// 跳转到历史页面
|
||||
function goToHistory() {
|
||||
// 创建表单进行POST请求跳转,更安全
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/history';
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
|
||||
// 添加显示函数
|
||||
function displayCavityData(cavityData) {
|
||||
const cavityDiv = document.getElementById('cavityData');
|
||||
cavityDiv.innerHTML = `
|
||||
<pre>${JSON.stringify(cavityData, null, 2)}</pre>
|
||||
`;
|
||||
}
|
||||
|
||||
function displayKeyInfo(keyInfo) {
|
||||
const keyInfoDiv = document.getElementById('keyInfoData');
|
||||
|
||||
if (!keyInfo) {
|
||||
keyInfoDiv.innerHTML = '<div class="data-item"><div class="data-value">无关键信息数据</div></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// 从正确的数据结构中提取数据
|
||||
const metadata = keyInfo.metadata || {};
|
||||
const manufacturingInfo = keyInfo.manufacturing_info || {};
|
||||
const moldCavities = keyInfo.mold_cavities || {};
|
||||
const cavityKeyInfo = moldCavities.cavity_key_info || {};
|
||||
const geoChars = cavityKeyInfo.geometric_characteristics || {};
|
||||
|
||||
keyInfoDiv.innerHTML = `
|
||||
<div class="data-item">
|
||||
<div class="data-label">🔧 模具参数</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">收缩率</div>
|
||||
<div class="data-value">${metadata.shrinkage_rate !== undefined ? metadata.shrinkage_rate : 'N/A'}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">拔模角</div>
|
||||
<div class="data-value">${metadata.draft_angle !== undefined ? metadata.draft_angle + '°' : 'N/A'}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">分型线长度</div>
|
||||
<div class="data-value">${manufacturingInfo.parting_line_length || 'N/A'}</div>
|
||||
</div>
|
||||
|
||||
<div class="data-item" style="margin-top: 15px;">
|
||||
<div class="data-label">📐 几何特性</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">产品体积</div>
|
||||
<div class="data-value">${geoChars.product_volume || 'N/A'}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">产品重量</div>
|
||||
<div class="data-value">${geoChars.product_weight || 'N/A'}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">壁厚范围</div>
|
||||
<div class="data-value">${geoChars.wall_thickness_range || 'N/A'}</div>
|
||||
</div>
|
||||
|
||||
<div class="data-item" style="margin-top: 15px;">
|
||||
<div class="data-label">⚙️ 制造要求</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">型腔材料</div>
|
||||
<div class="data-value">${manufacturingInfo.mold_material || 'N/A'}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">硬度</div>
|
||||
<div class="data-value">${manufacturingInfo.mold_hardness || 'N/A'}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">表面光洁度</div>
|
||||
<div class="data-value">${manufacturingInfo.surface_finish || 'N/A'}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">预估周期</div>
|
||||
<div class="data-value">${manufacturingInfo.estimated_cycle_time || 'N/A'}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
+222
-69
@@ -1,102 +1,255 @@
|
||||
/* static/style.css */
|
||||
/**
|
||||
* STP模具几何分析中心 - 统一设计系统
|
||||
* 优化版本:现代化UI设计,响应式布局
|
||||
*/
|
||||
|
||||
/* 设计系统变量 */
|
||||
:root {
|
||||
/* 颜色系统 */
|
||||
--primary-color: #2563eb;
|
||||
--primary-hover: #1d4ed8;
|
||||
--secondary-color: #64748b;
|
||||
--success-color: #10b981;
|
||||
--warning-color: #f59e0b;
|
||||
--error-color: #ef4444;
|
||||
--info-color: #3b82f6;
|
||||
|
||||
/* 中性色 */
|
||||
--gray-50: #f8fafc;
|
||||
--gray-100: #f1f5f9;
|
||||
--gray-200: #e2e8f0;
|
||||
--gray-300: #cbd5e1;
|
||||
--gray-400: #94a3b8;
|
||||
--gray-500: #64748b;
|
||||
--gray-600: #475569;
|
||||
--gray-700: #334155;
|
||||
--gray-800: #1e293b;
|
||||
--gray-900: #0f172a;
|
||||
|
||||
/* 间距系统 */
|
||||
--space-xs: 0.25rem;
|
||||
--space-sm: 0.5rem;
|
||||
--space-md: 1rem;
|
||||
--space-lg: 1.5rem;
|
||||
--space-xl: 2rem;
|
||||
--space-2xl: 3rem;
|
||||
|
||||
/* 字体系统 */
|
||||
--font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
--font-size-xs: 0.75rem;
|
||||
--font-size-sm: 0.875rem;
|
||||
--font-size-base: 1rem;
|
||||
--font-size-lg: 1.125rem;
|
||||
--font-size-xl: 1.25rem;
|
||||
--font-size-2xl: 1.5rem;
|
||||
--font-size-3xl: 1.875rem;
|
||||
|
||||
/* 阴影系统 */
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
|
||||
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1);
|
||||
|
||||
/* 圆角系统 */
|
||||
--radius-sm: 0.375rem;
|
||||
--radius-md: 0.5rem;
|
||||
--radius-lg: 0.75rem;
|
||||
--radius-xl: 1rem;
|
||||
--radius-full: 9999px;
|
||||
}
|
||||
|
||||
/* 基础重置 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #1f2933 0%, #111827 100%);
|
||||
min-height: 100vh;
|
||||
padding: 16px;
|
||||
color: #111827;
|
||||
font-family: var(--font-family);
|
||||
background: linear-gradient(135deg, var(--gray-900) 0%, var(--gray-800) 100%);
|
||||
min-height: 100vh;
|
||||
padding: var(--space-md);
|
||||
color: var(--gray-800);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 旧版容器保留,兼容可能的使用 */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
/* 应用外壳 */
|
||||
.app-shell {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
min-height: calc(100vh - 2 * var(--space-md));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #2c3e50, #34495e);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
/* 通知系统 */
|
||||
.notification-container {
|
||||
position: fixed;
|
||||
top: var(--space-md);
|
||||
right: var(--space-md);
|
||||
z-index: 1000;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 10px;
|
||||
.notification {
|
||||
background: white;
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-md);
|
||||
margin-bottom: var(--space-sm);
|
||||
box-shadow: var(--shadow-lg);
|
||||
border-left: 4px solid var(--info-color);
|
||||
animation: slideIn 0.3s ease-out;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header p {
|
||||
opacity: 0.9;
|
||||
font-size: 1.1em;
|
||||
.notification-success {
|
||||
border-left-color: var(--success-color);
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
.notification-error {
|
||||
border-left-color: var(--error-color);
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
border: 3px dashed #3498db;
|
||||
border-radius: 10px;
|
||||
padding: 60px 40px;
|
||||
margin: 20px 0;
|
||||
background: #f8f9fa;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
.notification-warning {
|
||||
border-left-color: var(--warning-color);
|
||||
}
|
||||
|
||||
.upload-area:hover {
|
||||
border-color: #2980b9;
|
||||
background: #e8f4fc;
|
||||
.notification-message {
|
||||
flex: 1;
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
.upload-area.dragover {
|
||||
border-color: #27ae60;
|
||||
background: #d5f4e6;
|
||||
.notification-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: var(--font-size-lg);
|
||||
color: var(--gray-400);
|
||||
cursor: pointer;
|
||||
padding: var(--space-xs);
|
||||
margin-left: var(--space-sm);
|
||||
border-radius: var(--radius-sm);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 4em;
|
||||
color: #3498db;
|
||||
margin-bottom: 20px;
|
||||
.notification-close:hover {
|
||||
background: var(--gray-100);
|
||||
color: var(--gray-600);
|
||||
}
|
||||
|
||||
.file-input {
|
||||
display: none;
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
background: linear-gradient(135deg, #3498db, #2980b9);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 15px 40px;
|
||||
font-size: 1.1em;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
margin: 10px;
|
||||
/* 应用头部 */
|
||||
.app-header {
|
||||
background: white;
|
||||
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
|
||||
padding: var(--space-2xl);
|
||||
box-shadow: var(--shadow-md);
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.upload-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 20px rgba(52, 152, 219, 0.3);
|
||||
.app-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-lg);
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.upload-btn:disabled {
|
||||
background: #bdc3c7;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
.logo {
|
||||
font-size: var(--font-size-3xl);
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--info-color));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.app-title h1 {
|
||||
font-size: var(--font-size-3xl);
|
||||
color: var(--gray-900);
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.app-subtitle {
|
||||
color: var(--gray-600);
|
||||
font-size: var(--font-size-base);
|
||||
}
|
||||
|
||||
.app-nav {
|
||||
display: flex;
|
||||
gap: var(--space-md);
|
||||
border-top: 1px solid var(--gray-200);
|
||||
padding-top: var(--space-lg);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
text-decoration: none;
|
||||
color: var(--gray-600);
|
||||
border-radius: var(--radius-md);
|
||||
transition: all 0.2s;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: var(--primary-color);
|
||||
background: var(--gray-50);
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
color: var(--primary-color);
|
||||
background: var(--gray-50);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
/* 主要内容区域 */
|
||||
.app-main {
|
||||
flex: 1;
|
||||
background: white;
|
||||
border-radius: 0 0 var(--radius-xl) var(--radius-xl);
|
||||
padding: var(--space-2xl);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
/* 应用底部 */
|
||||
.app-footer {
|
||||
background: var(--gray-800);
|
||||
color: white;
|
||||
padding: var(--space-lg);
|
||||
border-radius: var(--radius-md);
|
||||
margin-top: var(--space-lg);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.footer-left, .footer-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.status-success {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.status-error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
.results-section {
|
||||
|
||||
+183
-60
@@ -1,9 +1,18 @@
|
||||
// static/vue-app.js
|
||||
// 使用 CDN 引入的 Vue3 + Vue Router,构建单页应用
|
||||
/**
|
||||
* STP模具几何分析中心 - Vue3单页应用
|
||||
* 优化版本:统一前端实现,增强用户体验
|
||||
*/
|
||||
|
||||
const { createApp, ref, computed, onMounted } = Vue;
|
||||
const { createApp, ref, computed, onMounted, reactive } = Vue;
|
||||
const { createRouter, createWebHistory, useRoute, useRouter } = VueRouter;
|
||||
|
||||
// 全局状态管理
|
||||
const appState = reactive({
|
||||
health: null,
|
||||
loading: false,
|
||||
notifications: []
|
||||
});
|
||||
|
||||
// 通用工具函数
|
||||
function formatFileSize(bytes) {
|
||||
if (!bytes || bytes === 0) return "0 Bytes";
|
||||
@@ -23,7 +32,7 @@ function formatNumber(num) {
|
||||
function formatDateTime(dateString) {
|
||||
if (!dateString) return "N/A";
|
||||
try {
|
||||
return new Date(dateString).toLocaleString();
|
||||
return new Date(dateString).toLocaleString('zh-CN');
|
||||
} catch {
|
||||
return dateString;
|
||||
}
|
||||
@@ -39,12 +48,49 @@ function statusText(status) {
|
||||
return map[status] || status || "未知";
|
||||
}
|
||||
|
||||
// 状态样式映射
|
||||
function getStatusClass(status) {
|
||||
const classMap = {
|
||||
pending: "status-pending",
|
||||
processing: "status-processing",
|
||||
completed: "status-completed",
|
||||
failed: "status-failed"
|
||||
};
|
||||
return classMap[status] || "status-pending";
|
||||
}
|
||||
|
||||
// 添加通知
|
||||
function addNotification(message, type = 'info') {
|
||||
const notification = {
|
||||
id: Date.now(),
|
||||
message,
|
||||
type,
|
||||
timestamp: new Date()
|
||||
};
|
||||
appState.notifications.push(notification);
|
||||
|
||||
// 自动移除通知
|
||||
setTimeout(() => {
|
||||
const index = appState.notifications.findIndex(n => n.id === notification.id);
|
||||
if (index > -1) {
|
||||
appState.notifications.splice(index, 1);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// 错误处理
|
||||
function handleApiError(error, context = '') {
|
||||
console.error(`API错误 [${context}]:`, error);
|
||||
const message = error.message || '请求失败,请稍后重试';
|
||||
addNotification(message, 'error');
|
||||
return message;
|
||||
}
|
||||
|
||||
// 顶部导航 + 布局
|
||||
const App = {
|
||||
setup() {
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const health = ref(null);
|
||||
|
||||
const isActive = (pathPrefix) =>
|
||||
computed(() => route.path === pathPrefix || route.path.startsWith(pathPrefix));
|
||||
@@ -53,30 +99,66 @@ const App = {
|
||||
try {
|
||||
const res = await fetch("/health", { method: "POST" });
|
||||
if (res.ok) {
|
||||
health.value = await res.json();
|
||||
appState.health = await res.json();
|
||||
}
|
||||
} catch {
|
||||
health.value = null;
|
||||
} catch (error) {
|
||||
appState.health = null;
|
||||
handleApiError(error, '健康检查');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(loadHealth);
|
||||
const dismissNotification = (id) => {
|
||||
const index = appState.notifications.findIndex(n => n.id === id);
|
||||
if (index > -1) {
|
||||
appState.notifications.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
return { route, router, health, isActive };
|
||||
onMounted(() => {
|
||||
loadHealth();
|
||||
// 定时检查健康状态(每30秒)
|
||||
setInterval(loadHealth, 30000);
|
||||
});
|
||||
|
||||
return {
|
||||
route,
|
||||
router,
|
||||
appState,
|
||||
isActive,
|
||||
dismissNotification,
|
||||
getStatusClass,
|
||||
statusText
|
||||
};
|
||||
},
|
||||
template: `
|
||||
<div class="app-shell">
|
||||
<!-- 通知区域 -->
|
||||
<div class="notification-container" v-if="appState.notifications.length > 0">
|
||||
<div
|
||||
v-for="notification in appState.notifications"
|
||||
:key="notification.id"
|
||||
:class="['notification', 'notification-' + notification.type]"
|
||||
>
|
||||
<span class="notification-message">{{ notification.message }}</span>
|
||||
<button class="notification-close" @click="dismissNotification(notification.id)">×</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<header class="app-header">
|
||||
<div class="app-title">
|
||||
<span class="logo">🔧</span>
|
||||
<div>
|
||||
<h1>STP 模具几何分析中心</h1>
|
||||
<p>上传模型 → 自动分析几何 / 型腔 / 工艺 → 聚合展示关键指标</p>
|
||||
<p class="app-subtitle">专业模具几何分析,支持型腔自动生成与工艺参数计算</p>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="app-nav">
|
||||
<router-link :class="['nav-link', { active: isActive('/').value }]" to="/">仪表盘</router-link>
|
||||
<router-link :class="['nav-link', { active: isActive('/history').value }]" to="/history">历史记录</router-link>
|
||||
<router-link :class="['nav-link', { active: isActive('/').value }]" to="/">
|
||||
📊 仪表盘
|
||||
</router-link>
|
||||
<router-link :class="['nav-link', { active: isActive('/history').value }]" to="/history">
|
||||
📋 历史记录
|
||||
</router-link>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
@@ -86,11 +168,15 @@ const App = {
|
||||
|
||||
<footer class="app-footer">
|
||||
<div class="footer-left">
|
||||
<span>PythonOCC: <strong>{{ health?.pythonocc ? '可用' : '未知' }}</strong></span>
|
||||
<span v-if="health"> | 当前任务数: {{ health.total_tasks }}</span>
|
||||
<span>PythonOCC: <strong :class="appState.health?.pythonocc ? 'status-success' : 'status-error'">
|
||||
{{ appState.health?.pythonocc ? '可用' : '未知' }}
|
||||
</strong></span>
|
||||
<span v-if="appState.health"> | 当前任务数: {{ appState.health.total_tasks }}</span>
|
||||
</div>
|
||||
<div class="footer-right">
|
||||
<span>后端版本: 3.0.0</span>
|
||||
<span>版本: 3.0.0</span>
|
||||
<span> | </span>
|
||||
<span>模具几何分析系统</span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
@@ -102,53 +188,77 @@ const DashboardView = {
|
||||
setup() {
|
||||
const router = useRouter();
|
||||
|
||||
const selectedFile = ref(null);
|
||||
const uploading = ref(false);
|
||||
const error = ref("");
|
||||
const currentTask = ref(null);
|
||||
const polling = ref(false);
|
||||
const historySummary = ref(null);
|
||||
const state = reactive({
|
||||
selectedFile: null,
|
||||
uploading: false,
|
||||
error: "",
|
||||
currentTask: null,
|
||||
polling: false,
|
||||
historySummary: null,
|
||||
dragOver: false
|
||||
});
|
||||
|
||||
const totalFiles = computed(() => historySummary.value?.total_files || 0);
|
||||
const totalFiles = computed(() => state.historySummary?.total_files || 0);
|
||||
const totalRecords = computed(() =>
|
||||
(historySummary.value?.files || []).reduce(
|
||||
(state.historySummary?.files || []).reduce(
|
||||
(sum, f) => sum + (f.record_count || 0),
|
||||
0
|
||||
)
|
||||
);
|
||||
const latestFile = computed(() =>
|
||||
(historySummary.value?.files || [])[0] || null
|
||||
(state.historySummary?.files || [])[0] || null
|
||||
);
|
||||
|
||||
const handleFileChange = (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
validateAndSelectFile(file);
|
||||
};
|
||||
|
||||
if (
|
||||
!file.name.toLowerCase().endsWith(".stp") &&
|
||||
!file.name.toLowerCase().endsWith(".step")
|
||||
) {
|
||||
error.value = "请选择 STP 或 STEP 格式文件";
|
||||
selectedFile.value = null;
|
||||
const validateAndSelectFile = (file) => {
|
||||
if (!file.name.toLowerCase().endsWith(".stp") &&
|
||||
!file.name.toLowerCase().endsWith(".step")) {
|
||||
state.error = "请选择 STP 或 STEP 格式文件";
|
||||
state.selectedFile = null;
|
||||
return;
|
||||
}
|
||||
if (file.size > 100 * 1024 * 1024) {
|
||||
error.value = "文件大小不能超过 100MB";
|
||||
selectedFile.value = null;
|
||||
state.error = "文件大小不能超过 100MB";
|
||||
state.selectedFile = null;
|
||||
return;
|
||||
}
|
||||
|
||||
error.value = "";
|
||||
selectedFile.value = file;
|
||||
state.error = "";
|
||||
state.selectedFile = file;
|
||||
addNotification(`已选择文件: ${file.name}`, 'success');
|
||||
};
|
||||
|
||||
const handleDragOver = (event) => {
|
||||
event.preventDefault();
|
||||
state.dragOver = true;
|
||||
};
|
||||
|
||||
const handleDragLeave = (event) => {
|
||||
event.preventDefault();
|
||||
state.dragOver = false;
|
||||
};
|
||||
|
||||
const handleDrop = (event) => {
|
||||
event.preventDefault();
|
||||
state.dragOver = false;
|
||||
const files = event.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
validateAndSelectFile(files[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const uploadFile = async () => {
|
||||
if (!selectedFile.value) return;
|
||||
uploading.value = true;
|
||||
error.value = "";
|
||||
if (!state.selectedFile) return;
|
||||
state.uploading = true;
|
||||
state.error = "";
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", selectedFile.value);
|
||||
formData.append("file", state.selectedFile);
|
||||
|
||||
try {
|
||||
const res = await fetch("/upload", {
|
||||
@@ -159,22 +269,24 @@ const DashboardView = {
|
||||
throw new Error(`上传失败: ${res.status} ${res.statusText}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
currentTask.value = {
|
||||
state.currentTask = {
|
||||
task_id: data.task_id,
|
||||
status: "processing",
|
||||
filename: data.file_info?.filename,
|
||||
file_size: data.file_info?.size,
|
||||
};
|
||||
addNotification(`文件上传成功,开始分析...`, 'success');
|
||||
startPolling(data.task_id);
|
||||
} catch (e) {
|
||||
error.value = e.message || "上传失败";
|
||||
const errorMsg = handleApiError(e, '文件上传');
|
||||
state.error = errorMsg;
|
||||
} finally {
|
||||
uploading.value = false;
|
||||
state.uploading = false;
|
||||
}
|
||||
};
|
||||
|
||||
const startPolling = async (taskId) => {
|
||||
polling.value = true;
|
||||
state.polling = true;
|
||||
const poll = async () => {
|
||||
try {
|
||||
const res = await fetch(`/status/${taskId}`, { method: "POST" });
|
||||
@@ -182,21 +294,26 @@ const DashboardView = {
|
||||
throw new Error("查询任务状态失败");
|
||||
}
|
||||
const task = await res.json();
|
||||
currentTask.value = task;
|
||||
state.currentTask = task;
|
||||
|
||||
if (task.status === "completed") {
|
||||
polling.value = false;
|
||||
// 跳转到结果详情页
|
||||
router.push(`/result/${task.task_id}`);
|
||||
state.polling = false;
|
||||
addNotification(`分析完成,正在跳转到结果页面...`, 'success');
|
||||
setTimeout(() => {
|
||||
router.push(`/result/${task.task_id}`);
|
||||
}, 1000);
|
||||
} else if (task.status === "failed") {
|
||||
polling.value = false;
|
||||
error.value = `分析失败: ${task.error || "未知错误"}`;
|
||||
state.polling = false;
|
||||
const errorMsg = task.error || "未知错误";
|
||||
state.error = `分析失败: ${errorMsg}`;
|
||||
addNotification(`分析失败: ${errorMsg}`, 'error');
|
||||
} else {
|
||||
setTimeout(poll, 1000);
|
||||
}
|
||||
} catch (e) {
|
||||
polling.value = false;
|
||||
error.value = e.message || "轮询失败";
|
||||
state.polling = false;
|
||||
const errorMsg = handleApiError(e, '任务轮询');
|
||||
state.error = errorMsg;
|
||||
}
|
||||
};
|
||||
poll();
|
||||
@@ -206,32 +323,38 @@ const DashboardView = {
|
||||
try {
|
||||
const res = await fetch("/api/history", { method: "POST" });
|
||||
if (res.ok) {
|
||||
historySummary.value = await res.json();
|
||||
state.historySummary = await res.json();
|
||||
}
|
||||
} catch {
|
||||
historySummary.value = null;
|
||||
} catch (e) {
|
||||
state.historySummary = null;
|
||||
handleApiError(e, '加载历史记录');
|
||||
}
|
||||
};
|
||||
|
||||
const clearFile = () => {
|
||||
state.selectedFile = null;
|
||||
state.error = "";
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadHistorySummary();
|
||||
});
|
||||
|
||||
return {
|
||||
selectedFile,
|
||||
uploading,
|
||||
error,
|
||||
currentTask,
|
||||
polling,
|
||||
historySummary,
|
||||
state,
|
||||
totalFiles,
|
||||
totalRecords,
|
||||
latestFile,
|
||||
handleFileChange,
|
||||
handleDragOver,
|
||||
handleDragLeave,
|
||||
handleDrop,
|
||||
uploadFile,
|
||||
clearFile,
|
||||
formatFileSize,
|
||||
statusText,
|
||||
formatDateTime,
|
||||
getStatusClass
|
||||
};
|
||||
},
|
||||
template: `
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<!-- templates/history.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>历史记录 - STP 模具几何分析中心</title>
|
||||
<meta name="description" content="查看所有已处理的STP文件历史记录和分析任务">
|
||||
<meta name="keywords" content="STP历史记录,分析任务,文件管理">
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<link rel="preconnect" href="https://unpkg.com">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
+12
-2
@@ -1,11 +1,13 @@
|
||||
<!-- templates/index.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>STP 模具几何分析中心</title>
|
||||
<title>STP 模具几何分析中心 - 仪表盘</title>
|
||||
<meta name="description" content="专业的STP/STEP文件几何分析工具,支持模具型腔自动生成和工艺参数计算">
|
||||
<meta name="keywords" content="STP,STEP,模具分析,几何分析,型腔设计">
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<link rel="preconnect" href="https://unpkg.com">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
@@ -14,5 +16,13 @@
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||
<script src="https://unpkg.com/vue-router@4/dist/vue-router.global.prod.js"></script>
|
||||
<script src="/static/vue-app.js"></script>
|
||||
|
||||
<!-- 预加载关键资源 -->
|
||||
<script>
|
||||
// 页面加载优化
|
||||
window.addEventListener('load', function() {
|
||||
console.log('STP模具分析中心已加载完成');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,11 +1,13 @@
|
||||
<!-- templates/result.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>分析结果 - STP 模具几何分析中心</title>
|
||||
<meta name="description" content="查看详细的STP文件几何分析结果,包括模具型腔参数和工艺建议">
|
||||
<meta name="keywords" content="STP分析结果,模具参数,几何分析报告">
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<link rel="preconnect" href="https://unpkg.com">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
Reference in New Issue
Block a user