869 lines
28 KiB
JavaScript
869 lines
28 KiB
JavaScript
/**
|
||
* 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: `
|
||
<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 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>
|
||
</nav>
|
||
</header>
|
||
|
||
<main class="app-main">
|
||
<router-view />
|
||
</main>
|
||
|
||
<footer class="app-footer">
|
||
<div class="footer-left">
|
||
<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> | </span>
|
||
<span>模具几何分析系统</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
|
||
});
|
||
|
||
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;
|
||
const poll = async () => {
|
||
try {
|
||
const res = await fetch(`/status/${taskId}`, { method: "POST" });
|
||
if (!res.ok) {
|
||
throw new Error("查询任务状态失败");
|
||
}
|
||
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 {
|
||
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: `
|
||
<div class="dashboard">
|
||
<section class="grid-2">
|
||
<div class="card upload-card">
|
||
<h2>📁 上传 STP / STEP 文件</h2>
|
||
<p class="card-subtitle">上传后系统会自动解析几何、生成模具型腔并计算关键工艺参数。</p>
|
||
|
||
<div class="upload-panel">
|
||
<label class="upload-dropzone">
|
||
<input type="file" accept=".stp,.step" @change="handleFileChange" hidden />
|
||
<div class="upload-icon">📂</div>
|
||
<div v-if="!selectedFile">
|
||
<h3>点击选择或拖拽文件到此处</h3>
|
||
<p>最大 100MB,支持 .stp / .step</p>
|
||
</div>
|
||
<div v-else>
|
||
<h3>已选择文件:</h3>
|
||
<p><strong>{{ selectedFile.name }}</strong></p>
|
||
<p>大小:{{ formatFileSize(selectedFile.size) }}</p>
|
||
</div>
|
||
</label>
|
||
|
||
<button
|
||
class="upload-btn"
|
||
:disabled="!selectedFile || uploading"
|
||
@click="uploadFile"
|
||
>
|
||
<span v-if="!uploading">开始分析</span>
|
||
<span v-else>正在上传...</span>
|
||
</button>
|
||
|
||
<p v-if="error" class="error-text">{{ error }}</p>
|
||
</div>
|
||
|
||
<div v-if="currentTask" class="card current-task">
|
||
<h3>当前任务</h3>
|
||
<div class="info-row">
|
||
<span>任务 ID:</span><span>{{ currentTask.task_id }}</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span>文件名:</span><span>{{ currentTask.filename || 'N/A' }}</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span>文件大小:</span><span>{{ currentTask.file_size ? formatFileSize(currentTask.file_size) : 'N/A' }}</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span>状态:</span>
|
||
<span class="status-pill" :class="'status-' + currentTask.status">
|
||
{{ statusText(currentTask.status) }}
|
||
</span>
|
||
</div>
|
||
<div v-if="polling" class="small-hint">正在后台分析,请稍候,完成后会跳转到结果页。</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card summary-card">
|
||
<h2>📊 项目总览</h2>
|
||
<div class="summary-grid">
|
||
<div class="summary-item">
|
||
<div class="summary-label">已分析文件数</div>
|
||
<div class="summary-value">{{ totalFiles }}</div>
|
||
</div>
|
||
<div class="summary-item">
|
||
<div class="summary-label">总处理记录数</div>
|
||
<div class="summary-value">{{ totalRecords }}</div>
|
||
</div>
|
||
<div class="summary-item" v-if="latestFile">
|
||
<div class="summary-label">最近上传文件</div>
|
||
<div class="summary-value text-left">
|
||
<div class="filename">{{ latestFile.filename }}</div>
|
||
<div class="meta">
|
||
最近上传:{{ formatDateTime(latestFile.last_upload) }}<br />
|
||
历史记录:{{ latestFile.record_count }} 条
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-else class="summary-empty">
|
||
还没有历史记录,先上传一个 STP 文件试试吧。
|
||
</div>
|
||
</div>
|
||
|
||
<router-link class="link-btn" to="/history">查看详细历史记录 →</router-link>
|
||
</div>
|
||
</section>
|
||
</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>📋 历史记录</h2>
|
||
<p class="card-subtitle">按文件名聚合展示所有上传与分析任务。</p>
|
||
|
||
<div v-if="loading" class="center-block">
|
||
<div class="spinner"></div>
|
||
<p>正在加载历史记录...</p>
|
||
</div>
|
||
|
||
<p v-if="error" class="error-text">{{ error }}</p>
|
||
|
||
<div v-if="!loading && files.length === 0 && !error" class="center-block">
|
||
<div style="font-size: 40px; margin-bottom: 12px;">📁</div>
|
||
<p>暂无历史记录,先在仪表盘上传一个文件吧。</p>
|
||
</div>
|
||
|
||
<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>
|
||
<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="expand-btn">
|
||
{{ expanded[file.filename] ? '收起' : '展开' }}
|
||
</button>
|
||
</div>
|
||
|
||
<div class="record-list" v-if="expanded[file.filename]">
|
||
<div v-if="!file.records" class="center-block">
|
||
<div class="spinner small"></div>
|
||
<p>正在加载记录...</p>
|
||
</div>
|
||
<div v-else-if="file.records.length === 0" class="center-block">
|
||
暂无详细记录
|
||
</div>
|
||
<div
|
||
v-else
|
||
v-for="record in file.records"
|
||
:key="record.task_id"
|
||
class="record-item"
|
||
@click="openResult(record.task_id)"
|
||
>
|
||
<div class="record-header">
|
||
<span>📅 {{ formatDateTime(record.upload_time) }}</span>
|
||
<span class="status-pill" :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>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</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>📊 分析结果详情</h2>
|
||
|
||
<div v-if="loading" class="center-block">
|
||
<div class="spinner"></div>
|
||
<p>正在加载任务 {{ taskId }} 的分析结果...</p>
|
||
</div>
|
||
|
||
<p v-if="error" class="error-text">{{ error }}</p>
|
||
|
||
<div v-if="task && !loading" class="result-layout">
|
||
<section class="card task-card">
|
||
<h3>📝 任务信息</h3>
|
||
<div class="info-grid">
|
||
<div class="info-item">
|
||
<span class="info-label">任务 ID</span>
|
||
<span class="info-value">{{ task.task_id || 'N/A' }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="info-label">文件名</span>
|
||
<span class="info-value">{{ task.filename || 'N/A' }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="info-label">文件大小</span>
|
||
<span class="info-value">{{ task.file_size ? formatFileSize(task.file_size) : 'N/A' }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="info-label">状态</span>
|
||
<span class="info-value">
|
||
<span class="status-pill" :class="'status-' + task.status">
|
||
{{ statusText(task.status) }}
|
||
</span>
|
||
</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="info-label">上传时间</span>
|
||
<span class="info-value">{{ task.upload_time ? formatDateTime(task.upload_time) : 'N/A' }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="info-label">完成时间</span>
|
||
<span class="info-value">{{ task.completed_at ? formatDateTime(task.completed_at) : 'N/A' }}</span>
|
||
</div>
|
||
</div>
|
||
<div v-if="task.error" class="error-box">
|
||
<strong>错误信息:</strong>{{ task.error }}
|
||
</div>
|
||
</section>
|
||
|
||
<section class="grid-3">
|
||
<div class="card">
|
||
<h3>📐 几何属性</h3>
|
||
<div v-if="geometry" class="data-grid">
|
||
<div class="data-item">
|
||
<span class="data-label">体积</span>
|
||
<span class="data-value">
|
||
{{ geometry.volume ? formatNumber(geometry.volume) + ' mm³' : 'N/A' }}
|
||
</span>
|
||
</div>
|
||
<div class="data-item">
|
||
<span class="data-label">表面积</span>
|
||
<span class="data-value">
|
||
{{ geometry.surface_area ? formatNumber(geometry.surface_area) + ' mm²' : 'N/A' }}
|
||
</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="placeholder">无几何数据</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>📦 边界框</h3>
|
||
<div v-if="geometry?.bounding_box" class="data-grid">
|
||
<div class="data-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">
|
||
<span class="data-label">最小坐标</span>
|
||
<span class="data-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">
|
||
<span class="data-label">最大坐标</span>
|
||
<span class="data-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="placeholder">无边界框数据</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>🔺 拓扑结构</h3>
|
||
<div v-if="geometry?.topology" class="data-grid">
|
||
<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="placeholder">无拓扑数据</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="grid-2">
|
||
<div class="card">
|
||
<h3>🔧 模具 / 工艺关键信息</h3>
|
||
<div v-if="keyInfo" class="data-grid">
|
||
<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="placeholder">无型腔 / 工艺数据</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>🧱 网格摘要</h3>
|
||
<div v-if="meshSummary" class="data-grid">
|
||
<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="placeholder">
|
||
当前任务未生成网格摘要,或网格生成过程中出现问题。
|
||
</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");
|
||
|