Files
geMoldInsight/static/vue-app.js
T
2026-03-03 23:57:04 +08:00

978 lines
33 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* STP模具几何分析中心 - Vue3单页应用
* 版本: 4.0.0 - 暖白色主题 + 增强动画效果
*/
const { createApp, ref, computed, onMounted, reactive, watch, nextTick } = Vue;
const { createRouter, createWebHistory, useRoute, useRouter } = VueRouter;
// 全局状态管理
const appState = reactive({
health: null,
loading: false,
notifications: [],
theme: 'warm'
});
// 通用工具函数
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";
}
let notificationId = 0;
function addNotification(message, type = 'info') {
const id = ++notificationId;
const notification = {
id,
message,
type,
timestamp: new Date(),
visible: true
};
appState.notifications.push(notification);
setTimeout(() => {
const index = appState.notifications.findIndex(n => n.id === id);
if (index > -1) {
appState.notifications[index].visible = false;
setTimeout(() => {
const idx = appState.notifications.findIndex(n => n.id === id);
if (idx > -1) {
appState.notifications.splice(idx, 1);
}
}, 300);
}
}, 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[index].visible = false;
setTimeout(() => {
const idx = appState.notifications.findIndex(n => n.id === id);
if (idx > -1) {
appState.notifications.splice(idx, 1);
}
}, 300);
}
};
onMounted(() => {
loadHealth();
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">
<TransitionGroup name="notification">
<div
v-for="notification in appState.notifications"
:key="notification.id"
:class="['notification', 'notification-' + notification.type, { 'notification-hidden': !notification.visible }]"
>
<span class="notification-icon">
{{ notification.type === 'success' ? '✓' : notification.type === 'error' ? '✕' : notification.type === 'warning' ? '⚠' : 'ℹ' }}
</span>
<span class="notification-message">{{ notification.message }}</span>
<button class="notification-close" @click="dismissNotification(notification.id)">×</button>
</div>
</TransitionGroup>
</div>
<header class="app-header">
<div class="app-title">
<span class="logo">🔧</span>
<div>
<h1>STP 模具几何分析中心</h1>
<p class="app-subtitle">专业模具几何分析,支持型腔自动生成与工艺参数计算</p>
</div>
</div>
<nav class="app-nav">
<router-link :class="['nav-link', { active: isActive('/').value }]" to="/">
<span class="nav-icon">📊</span>
<span>仪表盘</span>
</router-link>
<router-link :class="['nav-link', { active: isActive('/history').value }]" to="/history">
<span class="nav-icon">📋</span>
<span>历史记录</span>
</router-link>
</nav>
</header>
<main class="app-main">
<router-view v-slot="{ Component }">
<transition name="fade-slide" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</main>
<footer class="app-footer">
<div class="footer-left">
<span class="footer-item">
<span class="footer-label">PythonOCC:</span>
<strong :class="appState.health?.pythonocc ? 'status-success' : 'status-error'">
{{ appState.health?.pythonocc ? '可用' : '未知' }}
</strong>
</span>
<span v-if="appState.health" class="footer-divider">|</span>
<span v-if="appState.health" class="footer-item">
<span class="footer-label">当前任务数:</span>
<strong>{{ appState.health.total_tasks }}</strong>
</span>
</div>
<div class="footer-right">
<span class="footer-item">版本: <strong>4.0.0</strong></span>
<span class="footer-divider">|</span>
<span class="footer-item">模具几何分析系统</span>
</div>
</footer>
</div>
`,
};
// 仪表盘视图:上传 + 总览
const DashboardView = {
setup() {
const router = useRouter();
const state = reactive({
selectedFile: null,
uploading: false,
error: "",
currentTask: null,
polling: false,
historySummary: null,
dragOver: false,
progress: 0
});
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 = "";
state.progress = 0;
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;
state.progress = 10;
let pollCount = 0;
const maxPolls = 300;
const poll = async () => {
try {
pollCount++;
state.progress = Math.min(90, 10 + pollCount * 0.5);
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;
state.progress = 100;
addNotification(`分析完成,正在跳转到结果页面...`, 'success');
setTimeout(() => {
router.push(`/result/${task.task_id}`);
}, 1000);
} else if (task.status === "failed") {
state.polling = false;
state.progress = 0;
const errorMsg = task.error || "未知错误";
state.error = `分析失败: ${errorMsg}`;
addNotification(`分析失败: ${errorMsg}`, 'error');
} else if (pollCount >= maxPolls) {
state.polling = false;
state.progress = 0;
state.error = "任务处理超时,请稍后查看结果";
addNotification("任务处理超时,请稍后查看结果", 'warning');
} else {
setTimeout(poll, 1000);
}
} catch (e) {
state.polling = false;
state.progress = 0;
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: `
<div class="dashboard">
<!-- 统计卡片 -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon">📁</div>
<div class="stat-value">{{ totalFiles }}</div>
<div class="stat-label">已分析文件</div>
</div>
<div class="stat-card">
<div class="stat-icon">📊</div>
<div class="stat-value">{{ totalRecords }}</div>
<div class="stat-label">处理记录</div>
</div>
<div class="stat-card">
<div class="stat-icon">⚡</div>
<div class="stat-value">{{ state.polling ? '分析中' : '就绪' }}</div>
<div class="stat-label">系统状态</div>
</div>
<div class="stat-card">
<div class="stat-icon">🎯</div>
<div class="stat-value">{{ latestFile ? '1' : '0' }}</div>
<div class="stat-label">最近文件</div>
</div>
</div>
<div class="grid-2">
<!-- 上传区域 -->
<div class="card upload-card">
<h2>
<span class="card-icon">📤</span>
上传 STP / STEP 文件
</h2>
<p class="card-subtitle">上传后系统会自动解析几何、生成模具型腔并计算关键工艺参数。</p>
<div class="upload-section">
<label
class="upload-area"
:class="{ 'drag-over': state.dragOver }"
@dragover.prevent="handleDragOver"
@dragleave.prevent="handleDragLeave"
@drop.prevent="handleDrop"
>
<input type="file" accept=".stp,.step" @change="handleFileChange" hidden />
<div class="upload-icon">📂</div>
<div v-if="!state.selectedFile" class="upload-content">
<h3>点击选择或拖拽文件到此处</h3>
<p>最大 100MB,支持 .stp / .step</p>
</div>
<div v-else class="upload-selected">
<h3>已选择文件</h3>
<p class="filename"><strong>{{ state.selectedFile.name }}</strong></p>
<p class="filesize">大小:{{ formatFileSize(state.selectedFile.size) }}</p>
<button class="btn btn-secondary clear-btn" @click.stop="clearFile">清除选择</button>
</div>
</label>
<button
class="btn btn-primary upload-btn"
:disabled="!state.selectedFile || state.uploading"
@click="uploadFile"
>
<span v-if="!state.uploading">🚀 开始分析</span>
<span v-else>⏳ 正在上传...</span>
</button>
<p v-if="state.error" class="error-message">{{ state.error }}</p>
</div>
<!-- 当前进度 -->
<div v-if="state.currentTask" class="current-task-card">
<h4>📋 当前任务</h4>
<div class="task-info">
<div class="data-item">
<span class="data-label">任务 ID</span>
<span class="data-value">{{ state.currentTask.task_id }}</span>
</div>
<div class="data-item">
<span class="data-label">文件名</span>
<span class="data-value">{{ state.currentTask.filename || 'N/A' }}</span>
</div>
<div class="data-item">
<span class="data-label">文件大小</span>
<span class="data-value">{{ state.currentTask.file_size ? formatFileSize(state.currentTask.file_size) : 'N/A' }}</span>
</div>
<div class="data-item">
<span class="data-label">状态</span>
<span class="status-badge" :class="getStatusClass(state.currentTask.status)">
{{ statusText(state.currentTask.status) }}
</span>
</div>
</div>
<div v-if="state.polling" class="progress-container">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: state.progress + '%' }"></div>
</div>
<p class="progress-text">正在后台分析,请稍候...</p>
</div>
</div>
</div>
<!-- 项目总览 -->
<div class="card summary-card">
<h2>
<span class="card-icon">📊</span>
项目总览
</h2>
<div v-if="latestFile" class="latest-file">
<h4>最近上传文件</h4>
<div class="file-preview">
<span class="file-icon">📄</span>
<div class="file-details">
<div class="file-name">{{ latestFile.filename }}</div>
<div class="file-meta">
<span>📅 {{ formatDateTime(latestFile.last_upload) }}</span>
<span>📊 {{ latestFile.record_count }} 条记录</span>
</div>
</div>
</div>
</div>
<div v-else class="empty-state">
<div class="empty-state-icon">📁</div>
<h3>暂无历史记录</h3>
<p>上传一个 STP 文件开始分析吧</p>
</div>
<router-link class="btn btn-secondary link-btn" to="/history">
查看详细历史记录 →
</router-link>
</div>
</div>
</div>
`,
};
// 历史记录视图
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: `
<div class="history-view">
<h2>
<span class="card-icon">📋</span>
历史记录
</h2>
<p class="card-subtitle">按文件名聚合展示所有上传与分析任务。</p>
<div v-if="loading" class="loading">
<div class="spinner"></div>
<p class="loading-text">正在加载历史记录...</p>
</div>
<p v-if="error" class="error-message">{{ error }}</p>
<div v-if="!loading && files.length === 0 && !error" class="empty-state">
<div class="empty-state-icon">📁</div>
<h3>暂无历史记录</h3>
<p>先在仪表盘上传一个文件吧。</p>
</div>
<TransitionGroup name="list" tag="div" class="file-list" v-if="files.length">
<div
v-for="file in files"
:key="file.filename"
class="file-card"
:class="{ expanded: expanded[file.filename] }"
>
<div class="file-header" @click="toggleExpand(file.filename)">
<div class="file-info">
<div class="file-name">📄 {{ file.filename }}</div>
<div class="file-meta">
总记录:{{ file.record_count }} 条
<span v-if="file.last_upload"> | 最近上传:{{ formatDateTime(file.last_upload) }}</span>
</div>
</div>
<button class="btn btn-secondary expand-btn">
{{ expanded[file.filename] ? '收起' : '展开' }}
</button>
</div>
<transition name="expand">
<div class="record-list" v-if="expanded[file.filename]">
<div v-if="!file.records" class="loading">
<div class="spinner"></div>
<p class="loading-text">正在加载记录...</p>
</div>
<div v-else-if="file.records.length === 0" class="empty-state">
<p>暂无详细记录</p>
</div>
<TransitionGroup name="list" tag="div" v-else>
<div
v-for="record in file.records"
:key="record.task_id"
class="record-item"
@click="openResult(record.task_id)"
>
<div class="record-header">
<span class="record-date">📅 {{ formatDateTime(record.upload_time) }}</span>
<span class="status-badge" :class="'status-' + record.status">
{{ statusText(record.status) }}
</span>
</div>
<div class="record-meta">
<span>任务 ID:{{ record.task_id }}</span>
<span>文件大小:{{ formatFileSize(record.file_size) }}</span>
<span v-if="record.completed_at">完成时间:{{ formatDateTime(record.completed_at) }}</span>
</div>
</div>
</TransitionGroup>
</div>
</transition>
</div>
</TransitionGroup>
</div>
`,
};
// 结果详情视图
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: `
<div class="result-view">
<h2>
<span class="card-icon">📊</span>
分析结果详情
</h2>
<div v-if="loading" class="loading">
<div class="spinner"></div>
<p class="loading-text">正在加载任务 {{ taskId }} 的分析结果...</p>
</div>
<p v-if="error" class="error-message">{{ error }}</p>
<div v-if="task && !loading" class="result-layout">
<!-- 任务信息卡片 -->
<section class="result-card task-card">
<h3>📝 任务信息</h3>
<div class="task-info">
<div class="data-item">
<span class="data-label">任务 ID</span>
<span class="data-value">{{ task.task_id || 'N/A' }}</span>
</div>
<div class="data-item">
<span class="data-label">文件名</span>
<span class="data-value">{{ task.filename || 'N/A' }}</span>
</div>
<div class="data-item">
<span class="data-label">文件大小</span>
<span class="data-value">{{ task.file_size ? formatFileSize(task.file_size) : 'N/A' }}</span>
</div>
<div class="data-item">
<span class="data-label">状态</span>
<span class="status-badge" :class="'status-' + task.status">
{{ statusText(task.status) }}
</span>
</div>
<div class="data-item">
<span class="data-label">上传时间</span>
<span class="data-value">{{ task.upload_time ? formatDateTime(task.upload_time) : 'N/A' }}</span>
</div>
<div class="data-item">
<span class="data-label">完成时间</span>
<span class="data-value">{{ task.completed_at ? formatDateTime(task.completed_at) : 'N/A' }}</span>
</div>
</div>
<div v-if="task.error" class="error-message">
<strong>错误信息:</strong>{{ task.error }}
</div>
</section>
<!-- 几何属性 -->
<section class="grid-3">
<div class="result-card">
<h3>📐 几何属性</h3>
<div v-if="geometry" class="geometry-data">
<div class="data-item">
<span class="data-label">体积</span>
<span class="data-value">
{{ geometry.volume ? formatNumber(geometry.volume) : 'N/A' }}
<span class="data-unit">mm³</span>
</span>
</div>
<div class="data-item">
<span class="data-label">表面积</span>
<span class="data-value">
{{ geometry.surface_area ? formatNumber(geometry.surface_area) : 'N/A' }}
<span class="data-unit">mm²</span>
</span>
</div>
<div class="data-item">
<span class="data-label">体积/面积比</span>
<span class="data-value">
{{
geometry.volume && geometry.surface_area
? (geometry.volume / geometry.surface_area).toFixed(4)
: 'N/A'
}}
</span>
</div>
</div>
<div v-else class="empty-state">
<p>无几何数据</p>
</div>
</div>
<div class="result-card">
<h3>📦 边界框</h3>
<div v-if="geometry?.bounding_box" class="bounding-box-data">
<div class="data-item coordinate-item">
<span class="data-label">尺寸 (mm)</span>
<span class="data-value">
{{ geometry.bounding_box.dimensions[0].toFixed(2) }} ×
{{ geometry.bounding_box.dimensions[1].toFixed(2) }} ×
{{ geometry.bounding_box.dimensions[2].toFixed(2) }}
</span>
</div>
<div class="data-item coordinate-item">
<span class="data-label">最小坐标</span>
<span class="data-value coordinate-value">
X: {{ geometry.bounding_box.min[0].toFixed(2) }},
Y: {{ geometry.bounding_box.min[1].toFixed(2) }},
Z: {{ geometry.bounding_box.min[2].toFixed(2) }}
</span>
</div>
<div class="data-item coordinate-item">
<span class="data-label">最大坐标</span>
<span class="data-value coordinate-value">
X: {{ geometry.bounding_box.max[0].toFixed(2) }},
Y: {{ geometry.bounding_box.max[1].toFixed(2) }},
Z: {{ geometry.bounding_box.max[2].toFixed(2) }}
</span>
</div>
</div>
<div v-else class="empty-state">
<p>无边界框数据</p>
</div>
</div>
<div class="result-card">
<h3>🔺 拓扑结构</h3>
<div v-if="geometry?.topology" class="topology-data">
<div class="data-item">
<span class="data-label">面数</span>
<span class="data-value">{{ geometry.topology.faces || 0 }}</span>
</div>
<div class="data-item">
<span class="data-label">边数</span>
<span class="data-value">{{ geometry.topology.edges || 0 }}</span>
</div>
<div class="data-item">
<span class="data-label">顶点数</span>
<span class="data-value">{{ geometry.topology.vertices || 0 }}</span>
</div>
</div>
<div v-else class="empty-state">
<p>无拓扑数据</p>
</div>
</div>
</section>
<!-- 模具工艺信息 -->
<section class="grid-2">
<div class="result-card">
<h3>🔧 模具 / 工艺关键信息</h3>
<div v-if="keyInfo" class="metrics-data">
<div class="data-item">
<span class="data-label">收缩率</span>
<span class="data-value">{{ keyInfo.metadata?.shrinkage_rate ?? 'N/A' }}</span>
</div>
<div class="data-item">
<span class="data-label">拔模角</span>
<span class="data-value">
{{ keyInfo.metadata?.draft_angle !== undefined ? keyInfo.metadata.draft_angle + '°' : 'N/A' }}
</span>
</div>
<div class="data-item">
<span class="data-label">预估夹紧力</span>
<span class="data-value">{{ keyInfo.manufacturing_info?.estimated_clamping_force || 'N/A' }}</span>
</div>
<div class="data-item">
<span class="data-label">模具尺寸 (mm)</span>
<span class="data-value">
{{
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'
}}
</span>
</div>
<div class="data-item">
<span class="data-label">产品重量</span>
<span class="data-value">
{{ keyInfo.mold_cavities?.cavity_key_info?.geometric_characteristics?.product_weight || 'N/A' }}
</span>
</div>
<div class="data-item">
<span class="data-label">壁厚范围</span>
<span class="data-value">
{{ keyInfo.mold_cavities?.cavity_key_info?.geometric_characteristics?.wall_thickness_range || 'N/A' }}
</span>
</div>
<div class="data-item">
<span class="data-label">预估成型周期</span>
<span class="data-value">{{ keyInfo.manufacturing_info?.estimated_cycle_time || 'N/A' }}</span>
</div>
</div>
<div v-else class="empty-state">
<p>无型腔 / 工艺数据</p>
</div>
</div>
<div class="result-card">
<h3>🧱 网格摘要</h3>
<div v-if="meshSummary" class="metrics-data">
<div class="data-item">
<span class="data-label">网格质量等级</span>
<span class="data-value">{{ meshSummary.quality }}</span>
</div>
<div class="data-item">
<span class="data-label">顶点数</span>
<span class="data-value">{{ meshSummary.vertex_count }}</span>
</div>
<div class="data-item">
<span class="data-label">面数</span>
<span class="data-value">{{ meshSummary.face_count }}</span>
</div>
<div class="data-item">
<span class="data-label">点云采样点数</span>
<span class="data-value">{{ meshSummary.point_count ?? 'N/A' }}</span>
</div>
</div>
<div v-else class="empty-state">
<p>当前任务未生成网格摘要</p>
</div>
</div>
</section>
</div>
</div>
`,
};
// 路由配置
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");