/**
* STP模具几何分析中心 - Vue3单页应用
* 优化版本:统一前端实现,增强用户体验
*/
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";
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 === null || num === undefined) return "N/A";
if (num >= 1_000_000) return (num / 1_000_000).toFixed(2) + "M";
if (num >= 1_000) return (num / 1_000).toFixed(2) + "K";
return num.toFixed ? num.toFixed(2) : String(num);
}
function formatDateTime(dateString) {
if (!dateString) return "N/A";
try {
return new Date(dateString).toLocaleString('zh-CN');
} catch {
return dateString;
}
}
function statusText(status) {
const map = {
pending: "排队中",
processing: "处理中",
completed: "已完成",
failed: "失败",
};
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 isActive = (pathPrefix) =>
computed(() => route.path === pathPrefix || route.path.startsWith(pathPrefix));
const loadHealth = async () => {
try {
const res = await fetch("/health", { method: "POST" });
if (res.ok) {
appState.health = await res.json();
}
} catch (error) {
appState.health = null;
handleApiError(error, '健康检查');
}
};
const dismissNotification = (id) => {
const index = appState.notifications.findIndex(n => n.id === id);
if (index > -1) {
appState.notifications.splice(index, 1);
}
};
onMounted(() => {
loadHealth();
// 定时检查健康状态(每30秒)
setInterval(loadHealth, 30000);
});
return {
route,
router,
appState,
isActive,
dismissNotification,
getStatusClass,
statusText
};
},
template: `
{{ notification.message }}
`,
};
// 仪表盘视图:上传 + 总览
const DashboardView = {
setup() {
const router = useRouter();
const state = reactive({
selectedFile: null,
uploading: false,
error: "",
currentTask: null,
polling: false,
historySummary: null,
dragOver: false
});
const totalFiles = computed(() => state.historySummary?.total_files || 0);
const totalRecords = computed(() =>
(state.historySummary?.files || []).reduce(
(sum, f) => sum + (f.record_count || 0),
0
)
);
const latestFile = computed(() =>
(state.historySummary?.files || [])[0] || null
);
const handleFileChange = (event) => {
const file = event.target.files[0];
if (!file) return;
validateAndSelectFile(file);
};
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) {
state.error = "文件大小不能超过 100MB";
state.selectedFile = null;
return;
}
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 (!state.selectedFile) return;
state.uploading = true;
state.error = "";
const formData = new FormData();
formData.append("file", state.selectedFile);
try {
const res = await fetch("/upload", {
method: "POST",
body: formData,
});
if (!res.ok) {
throw new Error(`上传失败: ${res.status} ${res.statusText}`);
}
const data = await res.json();
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) {
const errorMsg = handleApiError(e, '文件上传');
state.error = errorMsg;
} finally {
state.uploading = false;
}
};
const startPolling = async (taskId) => {
state.polling = true;
let pollCount = 0;
const maxPolls = 300; // 最多轮询5分钟(300秒)
const poll = async () => {
try {
pollCount++;
const res = await fetch(`/status/${taskId}`, { method: "POST" });
if (res.status === 404) {
// 任务不存在,停止轮询
state.polling = false;
state.error = "任务不存在或已被删除";
addNotification("任务不存在或已被删除", 'error');
return;
}
if (!res.ok) {
throw new Error(`查询任务状态失败: ${res.status} ${res.statusText}`);
}
const task = await res.json();
state.currentTask = task;
if (task.status === "completed") {
state.polling = false;
addNotification(`分析完成,正在跳转到结果页面...`, 'success');
setTimeout(() => {
router.push(`/result/${task.task_id}`);
}, 1000);
} else if (task.status === "failed") {
state.polling = false;
const errorMsg = task.error || "未知错误";
state.error = `分析失败: ${errorMsg}`;
addNotification(`分析失败: ${errorMsg}`, 'error');
} else if (pollCount >= maxPolls) {
// 超时停止轮询
state.polling = false;
state.error = "任务处理超时,请稍后查看结果";
addNotification("任务处理超时,请稍后查看结果", 'warning');
} else {
setTimeout(poll, 1000);
}
} catch (e) {
state.polling = false;
const errorMsg = handleApiError(e, '任务轮询');
state.error = errorMsg;
}
};
poll();
};
const loadHistorySummary = async () => {
try {
const res = await fetch("/api/history", { method: "POST" });
if (res.ok) {
state.historySummary = await res.json();
}
} catch (e) {
state.historySummary = null;
handleApiError(e, '加载历史记录');
}
};
const clearFile = () => {
state.selectedFile = null;
state.error = "";
};
onMounted(() => {
loadHistorySummary();
});
return {
state,
totalFiles,
totalRecords,
latestFile,
handleFileChange,
handleDragOver,
handleDragLeave,
handleDrop,
uploadFile,
clearFile,
formatFileSize,
statusText,
formatDateTime,
getStatusClass
};
},
template: `
📁 上传 STP / STEP 文件
上传后系统会自动解析几何、生成模具型腔并计算关键工艺参数。
{{ state.error }}
当前任务
任务 ID:{{ state.currentTask.task_id }}
文件名:{{ state.currentTask.filename || 'N/A' }}
文件大小:{{ state.currentTask.file_size ? formatFileSize(state.currentTask.file_size) : 'N/A' }}
状态:
{{ statusText(state.currentTask.status) }}
正在后台分析,请稍候,完成后会跳转到结果页。
📊 项目总览
总处理记录数
{{ totalRecords }}
最近上传文件
{{ latestFile.filename }}
最近上传:{{ formatDateTime(latestFile.last_upload) }}
历史记录:{{ latestFile.record_count }} 条
还没有历史记录,先上传一个 STP 文件试试吧。
查看详细历史记录 →
`,
};
// 历史记录视图
const HistoryView = {
setup() {
const router = useRouter();
const files = ref([]);
const loading = ref(true);
const error = ref("");
const expanded = ref({});
const loadHistory = async () => {
loading.value = true;
error.value = "";
try {
const res = await fetch("/api/history", { method: "POST" });
if (!res.ok) throw new Error("获取历史记录失败");
const data = await res.json();
files.value = data.files || [];
} catch (e) {
error.value = e.message || "加载失败";
} finally {
loading.value = false;
}
};
const toggleExpand = async (filename) => {
expanded.value[filename] = !expanded.value[filename];
// 首次展开时加载该文件的所有记录
if (expanded.value[filename]) {
const file = files.value.find((f) => f.filename === filename);
if (!file.records) {
try {
const res = await fetch(
`/api/history/${encodeURIComponent(filename)}`,
{ method: "POST" }
);
if (!res.ok) throw new Error("加载记录失败");
file.records = await res.json();
} catch (e) {
error.value = e.message || "加载记录失败";
}
}
}
};
const openResult = (taskId) => {
router.push(`/result/${taskId}`);
};
onMounted(loadHistory);
return {
files,
loading,
error,
expanded,
toggleExpand,
openResult,
formatFileSize,
formatDateTime,
statusText,
};
},
template: `
📋 历史记录
按文件名聚合展示所有上传与分析任务。
{{ error }}
暂无详细记录
任务 ID:{{ record.task_id }}
文件大小:{{ formatFileSize(record.file_size) }}
完成时间:{{ formatDateTime(record.completed_at) }}
`,
};
// 结果详情视图
const ResultView = {
setup() {
const route = useRoute();
const task = ref(null);
const loading = ref(true);
const error = ref("");
const taskId = computed(() => route.params.taskId);
const loadTask = async () => {
loading.value = true;
error.value = "";
try {
const res = await fetch(`/status/${taskId.value}`, { method: "POST" });
if (!res.ok) throw new Error("获取任务数据失败");
const data = await res.json();
task.value = data;
} catch (e) {
error.value = e.message || "加载失败";
} finally {
loading.value = false;
}
};
const geometry = computed(() => task.value?.geometry_data || null);
const keyInfo = computed(() => task.value?.key_info || null);
const meshSummary = computed(() => task.value?.mesh_summary || null);
onMounted(loadTask);
return {
task,
loading,
error,
taskId,
geometry,
keyInfo,
meshSummary,
formatNumber,
formatFileSize,
formatDateTime,
statusText,
};
},
template: `
📊 分析结果详情
正在加载任务 {{ taskId }} 的分析结果...
{{ error }}
📝 任务信息
任务 ID
{{ task.task_id || 'N/A' }}
文件名
{{ task.filename || 'N/A' }}
文件大小
{{ task.file_size ? formatFileSize(task.file_size) : 'N/A' }}
状态
{{ statusText(task.status) }}
上传时间
{{ task.upload_time ? formatDateTime(task.upload_time) : 'N/A' }}
完成时间
{{ task.completed_at ? formatDateTime(task.completed_at) : 'N/A' }}
错误信息:{{ task.error }}
📐 几何属性
体积
{{ geometry.volume ? formatNumber(geometry.volume) + ' mm³' : 'N/A' }}
表面积
{{ geometry.surface_area ? formatNumber(geometry.surface_area) + ' mm²' : 'N/A' }}
体积/面积比
{{
geometry.volume && geometry.surface_area
? (geometry.volume / geometry.surface_area).toFixed(4)
: 'N/A'
}}
无几何数据
📦 边界框
尺寸 (mm)
{{ geometry.bounding_box.dimensions[0].toFixed(2) }} ×
{{ geometry.bounding_box.dimensions[1].toFixed(2) }} ×
{{ geometry.bounding_box.dimensions[2].toFixed(2) }}
最小坐标
X: {{ geometry.bounding_box.min[0].toFixed(2) }},
Y: {{ geometry.bounding_box.min[1].toFixed(2) }},
Z: {{ geometry.bounding_box.min[2].toFixed(2) }}
最大坐标
X: {{ geometry.bounding_box.max[0].toFixed(2) }},
Y: {{ geometry.bounding_box.max[1].toFixed(2) }},
Z: {{ geometry.bounding_box.max[2].toFixed(2) }}
无边界框数据
🔺 拓扑结构
面数
{{ geometry.topology.faces || 0 }}
边数
{{ geometry.topology.edges || 0 }}
顶点数
{{ geometry.topology.vertices || 0 }}
无拓扑数据
🔧 模具 / 工艺关键信息
收缩率
{{ keyInfo.metadata?.shrinkage_rate ?? 'N/A' }}
拔模角
{{
keyInfo.metadata?.draft_angle !== undefined
? keyInfo.metadata.draft_angle + '°'
: 'N/A'
}}
预估夹紧力
{{ keyInfo.manufacturing_info?.estimated_clamping_force || 'N/A' }}
模具尺寸 (mm)
{{
keyInfo.manufacturing_info?.estimated_mold_size
? keyInfo.manufacturing_info.estimated_mold_size.length +
' × ' +
keyInfo.manufacturing_info.estimated_mold_size.width +
' × ' +
keyInfo.manufacturing_info.estimated_mold_size.height
: 'N/A'
}}
产品重量
{{
keyInfo.mold_cavities?.cavity_key_info?.geometric_characteristics
?.product_weight || 'N/A'
}}
壁厚范围
{{
keyInfo.mold_cavities?.cavity_key_info?.geometric_characteristics
?.wall_thickness_range || 'N/A'
}}
预估成型周期
{{ keyInfo.manufacturing_info?.estimated_cycle_time || 'N/A' }}
无型腔 / 工艺数据
🧱 网格摘要
网格质量等级
{{ meshSummary.quality }}
顶点数
{{ meshSummary.vertex_count }}
面数
{{ meshSummary.face_count }}
点云采样点数
{{ meshSummary.point_count ?? 'N/A' }}
当前任务未生成网格摘要,或网格生成过程中出现问题。
`,
};
// 路由配置
const routes = [
{ path: "/", component: DashboardView },
{ path: "/history", component: HistoryView },
{ path: "/result/:taskId", component: ResultView },
];
const router = createRouter({
history: createWebHistory(),
routes,
});
// 挂载应用
createApp(App).use(router).mount("#app");