This commit is contained in:
cjw
2026-02-17 00:42:55 +08:00
parent 34afd80298
commit 06225e0d65
7 changed files with 423 additions and 1005 deletions
-215
View File
@@ -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 = '/';
}
}
-657
View File
@@ -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
View File
@@ -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; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
}
html {
scroll-behavior: smooth;
} }
body { body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-family: var(--font-family);
background: linear-gradient(135deg, #1f2933 0%, #111827 100%); background: linear-gradient(135deg, var(--gray-900) 0%, var(--gray-800) 100%);
min-height: 100vh; min-height: 100vh;
padding: 16px; padding: var(--space-md);
color: #111827; color: var(--gray-800);
line-height: 1.6;
} }
/* 旧版容器保留,兼容可能的使用 */ /* 应用外壳 */
.container { .app-shell {
max-width: 1200px; max-width: 1400px;
margin: 0 auto; margin: 0 auto;
background: white; min-height: calc(100vh - 2 * var(--space-md));
border-radius: 15px; display: flex;
box-shadow: 0 20px 40px rgba(0,0,0,0.1); flex-direction: column;
overflow: hidden;
} }
.header { /* 通知系统 */
background: linear-gradient(135deg, #2c3e50, #34495e); .notification-container {
color: white; position: fixed;
padding: 30px; top: var(--space-md);
text-align: center; right: var(--space-md);
z-index: 1000;
max-width: 400px;
} }
.header h1 { .notification {
font-size: 2.5em; background: white;
margin-bottom: 10px; 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 { .notification-success {
opacity: 0.9; border-left-color: var(--success-color);
font-size: 1.1em;
} }
.upload-section { .notification-error {
padding: 40px; border-left-color: var(--error-color);
text-align: center;
} }
.upload-area { .notification-warning {
border: 3px dashed #3498db; border-left-color: var(--warning-color);
border-radius: 10px;
padding: 60px 40px;
margin: 20px 0;
background: #f8f9fa;
transition: all 0.3s ease;
cursor: pointer;
} }
.upload-area:hover { .notification-message {
border-color: #2980b9; flex: 1;
background: #e8f4fc; color: var(--gray-700);
} }
.upload-area.dragover { .notification-close {
border-color: #27ae60; background: none;
background: #d5f4e6; 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 { .notification-close:hover {
font-size: 4em; background: var(--gray-100);
color: #3498db; color: var(--gray-600);
margin-bottom: 20px;
} }
.file-input { @keyframes slideIn {
display: none; from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
} }
.upload-btn { /* 应用头部 */
background: linear-gradient(135deg, #3498db, #2980b9); .app-header {
color: white; background: white;
border: none; border-radius: var(--radius-xl) var(--radius-xl) 0 0;
padding: 15px 40px; padding: var(--space-2xl);
font-size: 1.1em; box-shadow: var(--shadow-md);
border-radius: 50px; margin-bottom: var(--space-lg);
cursor: pointer;
transition: all 0.3s ease;
margin: 10px;
} }
.upload-btn:hover { .app-title {
transform: translateY(-2px); display: flex;
box-shadow: 0 10px 20px rgba(52, 152, 219, 0.3); align-items: center;
gap: var(--space-lg);
margin-bottom: var(--space-lg);
} }
.upload-btn:disabled { .logo {
background: #bdc3c7; font-size: var(--font-size-3xl);
cursor: not-allowed; background: linear-gradient(135deg, var(--primary-color), var(--info-color));
transform: none; -webkit-background-clip: text;
box-shadow: none; -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 { .results-section {
+183 -60
View File
@@ -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 { createRouter, createWebHistory, useRoute, useRouter } = VueRouter;
// 全局状态管理
const appState = reactive({
health: null,
loading: false,
notifications: []
});
// 通用工具函数 // 通用工具函数
function formatFileSize(bytes) { function formatFileSize(bytes) {
if (!bytes || bytes === 0) return "0 Bytes"; if (!bytes || bytes === 0) return "0 Bytes";
@@ -23,7 +32,7 @@ function formatNumber(num) {
function formatDateTime(dateString) { function formatDateTime(dateString) {
if (!dateString) return "N/A"; if (!dateString) return "N/A";
try { try {
return new Date(dateString).toLocaleString(); return new Date(dateString).toLocaleString('zh-CN');
} catch { } catch {
return dateString; return dateString;
} }
@@ -39,12 +48,49 @@ function statusText(status) {
return map[status] || 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 = { const App = {
setup() { setup() {
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const health = ref(null);
const isActive = (pathPrefix) => const isActive = (pathPrefix) =>
computed(() => route.path === pathPrefix || route.path.startsWith(pathPrefix)); computed(() => route.path === pathPrefix || route.path.startsWith(pathPrefix));
@@ -53,30 +99,66 @@ const App = {
try { try {
const res = await fetch("/health", { method: "POST" }); const res = await fetch("/health", { method: "POST" });
if (res.ok) { if (res.ok) {
health.value = await res.json(); appState.health = await res.json();
} }
} catch { } catch (error) {
health.value = null; 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: ` template: `
<div class="app-shell"> <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"> <header class="app-header">
<div class="app-title"> <div class="app-title">
<span class="logo">🔧</span> <span class="logo">🔧</span>
<div> <div>
<h1>STP 模具几何分析中心</h1> <h1>STP 模具几何分析中心</h1>
<p>上传模型 → 自动分析几何 / 型腔 / 工艺 → 聚合展示关键指标</p> <p class="app-subtitle">专业模具几何分析,支持型腔自动生成与工艺参数计算</p>
</div> </div>
</div> </div>
<nav class="app-nav"> <nav class="app-nav">
<router-link :class="['nav-link', { active: isActive('/').value }]" to="/">仪表盘</router-link> <router-link :class="['nav-link', { active: isActive('/').value }]" to="/">
<router-link :class="['nav-link', { active: isActive('/history').value }]" to="/history">历史记录</router-link> 📊 仪表盘
</router-link>
<router-link :class="['nav-link', { active: isActive('/history').value }]" to="/history">
📋 历史记录
</router-link>
</nav> </nav>
</header> </header>
@@ -86,11 +168,15 @@ const App = {
<footer class="app-footer"> <footer class="app-footer">
<div class="footer-left"> <div class="footer-left">
<span>PythonOCC: <strong>{{ health?.pythonocc ? '可用' : '未知' }}</strong></span> <span>PythonOCC: <strong :class="appState.health?.pythonocc ? 'status-success' : 'status-error'">
<span v-if="health"> | 当前任务数: {{ health.total_tasks }}</span> {{ appState.health?.pythonocc ? '可用' : '未知' }}
</strong></span>
<span v-if="appState.health"> | 当前任务数: {{ appState.health.total_tasks }}</span>
</div> </div>
<div class="footer-right"> <div class="footer-right">
<span>后端版本: 3.0.0</span> <span>版本: 3.0.0</span>
<span> | </span>
<span>模具几何分析系统</span>
</div> </div>
</footer> </footer>
</div> </div>
@@ -102,53 +188,77 @@ const DashboardView = {
setup() { setup() {
const router = useRouter(); const router = useRouter();
const selectedFile = ref(null); const state = reactive({
const uploading = ref(false); selectedFile: null,
const error = ref(""); uploading: false,
const currentTask = ref(null); error: "",
const polling = ref(false); currentTask: null,
const historySummary = ref(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(() => const totalRecords = computed(() =>
(historySummary.value?.files || []).reduce( (state.historySummary?.files || []).reduce(
(sum, f) => sum + (f.record_count || 0), (sum, f) => sum + (f.record_count || 0),
0 0
) )
); );
const latestFile = computed(() => const latestFile = computed(() =>
(historySummary.value?.files || [])[0] || null (state.historySummary?.files || [])[0] || null
); );
const handleFileChange = (event) => { const handleFileChange = (event) => {
const file = event.target.files[0]; const file = event.target.files[0];
if (!file) return; if (!file) return;
validateAndSelectFile(file);
};
if ( const validateAndSelectFile = (file) => {
!file.name.toLowerCase().endsWith(".stp") && if (!file.name.toLowerCase().endsWith(".stp") &&
!file.name.toLowerCase().endsWith(".step") !file.name.toLowerCase().endsWith(".step")) {
) { state.error = "请选择 STP 或 STEP 格式文件";
error.value = "请选择 STP 或 STEP 格式文件"; state.selectedFile = null;
selectedFile.value = null;
return; return;
} }
if (file.size > 100 * 1024 * 1024) { if (file.size > 100 * 1024 * 1024) {
error.value = "文件大小不能超过 100MB"; state.error = "文件大小不能超过 100MB";
selectedFile.value = null; state.selectedFile = null;
return; return;
} }
error.value = ""; state.error = "";
selectedFile.value = file; 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 () => { const uploadFile = async () => {
if (!selectedFile.value) return; if (!state.selectedFile) return;
uploading.value = true; state.uploading = true;
error.value = ""; state.error = "";
const formData = new FormData(); const formData = new FormData();
formData.append("file", selectedFile.value); formData.append("file", state.selectedFile);
try { try {
const res = await fetch("/upload", { const res = await fetch("/upload", {
@@ -159,22 +269,24 @@ const DashboardView = {
throw new Error(`上传失败: ${res.status} ${res.statusText}`); throw new Error(`上传失败: ${res.status} ${res.statusText}`);
} }
const data = await res.json(); const data = await res.json();
currentTask.value = { state.currentTask = {
task_id: data.task_id, task_id: data.task_id,
status: "processing", status: "processing",
filename: data.file_info?.filename, filename: data.file_info?.filename,
file_size: data.file_info?.size, file_size: data.file_info?.size,
}; };
addNotification(`文件上传成功,开始分析...`, 'success');
startPolling(data.task_id); startPolling(data.task_id);
} catch (e) { } catch (e) {
error.value = e.message || "上传失败"; const errorMsg = handleApiError(e, '文件上传');
state.error = errorMsg;
} finally { } finally {
uploading.value = false; state.uploading = false;
} }
}; };
const startPolling = async (taskId) => { const startPolling = async (taskId) => {
polling.value = true; state.polling = true;
const poll = async () => { const poll = async () => {
try { try {
const res = await fetch(`/status/${taskId}`, { method: "POST" }); const res = await fetch(`/status/${taskId}`, { method: "POST" });
@@ -182,21 +294,26 @@ const DashboardView = {
throw new Error("查询任务状态失败"); throw new Error("查询任务状态失败");
} }
const task = await res.json(); const task = await res.json();
currentTask.value = task; state.currentTask = task;
if (task.status === "completed") { if (task.status === "completed") {
polling.value = false; state.polling = false;
// 跳转到结果详情页 addNotification(`分析完成,正在跳转到结果页面...`, 'success');
router.push(`/result/${task.task_id}`); setTimeout(() => {
router.push(`/result/${task.task_id}`);
}, 1000);
} else if (task.status === "failed") { } else if (task.status === "failed") {
polling.value = false; state.polling = false;
error.value = `分析失败: ${task.error || "未知错误"}`; const errorMsg = task.error || "未知错误";
state.error = `分析失败: ${errorMsg}`;
addNotification(`分析失败: ${errorMsg}`, 'error');
} else { } else {
setTimeout(poll, 1000); setTimeout(poll, 1000);
} }
} catch (e) { } catch (e) {
polling.value = false; state.polling = false;
error.value = e.message || "轮询失败"; const errorMsg = handleApiError(e, '任务轮询');
state.error = errorMsg;
} }
}; };
poll(); poll();
@@ -206,32 +323,38 @@ const DashboardView = {
try { try {
const res = await fetch("/api/history", { method: "POST" }); const res = await fetch("/api/history", { method: "POST" });
if (res.ok) { if (res.ok) {
historySummary.value = await res.json(); state.historySummary = await res.json();
} }
} catch { } catch (e) {
historySummary.value = null; state.historySummary = null;
handleApiError(e, '加载历史记录');
} }
}; };
const clearFile = () => {
state.selectedFile = null;
state.error = "";
};
onMounted(() => { onMounted(() => {
loadHistorySummary(); loadHistorySummary();
}); });
return { return {
selectedFile, state,
uploading,
error,
currentTask,
polling,
historySummary,
totalFiles, totalFiles,
totalRecords, totalRecords,
latestFile, latestFile,
handleFileChange, handleFileChange,
handleDragOver,
handleDragLeave,
handleDrop,
uploadFile, uploadFile,
clearFile,
formatFileSize, formatFileSize,
statusText, statusText,
formatDateTime, formatDateTime,
getStatusClass
}; };
}, },
template: ` template: `
+3 -1
View File
@@ -1,11 +1,13 @@
<!-- templates/history.html -->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>历史记录 - STP 模具几何分析中心</title> <title>历史记录 - STP 模具几何分析中心</title>
<meta name="description" content="查看所有已处理的STP文件历史记录和分析任务">
<meta name="keywords" content="STP历史记录,分析任务,文件管理">
<link rel="stylesheet" href="/static/style.css"> <link rel="stylesheet" href="/static/style.css">
<link rel="preconnect" href="https://unpkg.com">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
+12 -2
View File
@@ -1,11 +1,13 @@
<!-- templates/index.html -->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <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="stylesheet" href="/static/style.css">
<link rel="preconnect" href="https://unpkg.com">
</head> </head>
<body> <body>
<div id="app"></div> <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@3/dist/vue.global.prod.js"></script>
<script src="https://unpkg.com/vue-router@4/dist/vue-router.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 src="/static/vue-app.js"></script>
<!-- 预加载关键资源 -->
<script>
// 页面加载优化
window.addEventListener('load', function() {
console.log('STP模具分析中心已加载完成');
});
</script>
</body> </body>
</html> </html>
+3 -1
View File
@@ -1,11 +1,13 @@
<!-- templates/result.html -->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>分析结果 - STP 模具几何分析中心</title> <title>分析结果 - STP 模具几何分析中心</title>
<meta name="description" content="查看详细的STP文件几何分析结果,包括模具型腔参数和工艺建议">
<meta name="keywords" content="STP分析结果,模具参数,几何分析报告">
<link rel="stylesheet" href="/static/style.css"> <link rel="stylesheet" href="/static/style.css">
<link rel="preconnect" href="https://unpkg.com">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>