This commit is contained in:
2026-03-04 00:10:05 +08:00
parent ea67bfa5b2
commit cf811d28a9
2 changed files with 1391 additions and 1800 deletions
+1115 -1434
View File
File diff suppressed because it is too large Load Diff
+273 -363
View File
@@ -1,24 +1,23 @@
/** /**
* STP模具几何分析中心 - Vue3单页应用 * STP模具几何分析中心 - Vue3单页应用
* 版本: 4.0.0 - 暖白色主题 + 增强动画效果 * 版本: 5.0.0 - Modern Minimal UI
* 设计灵感: Linear, Notion, Vercel, Stripe
*/ */
const { createApp, ref, computed, onMounted, reactive, watch, nextTick } = Vue; const { createApp, ref, computed, onMounted, reactive, watch, nextTick } = Vue;
const { createRouter, createWebHistory, useRoute, useRouter } = VueRouter; const { createRouter, createWebHistory, useRoute, useRouter } = VueRouter;
// 全局状态管理
const appState = reactive({ const appState = reactive({
health: null, health: null,
loading: false, loading: false,
notifications: [], notifications: [],
theme: 'warm' theme: 'modern'
}); });
// 通用工具函数
function formatFileSize(bytes) { function formatFileSize(bytes) {
if (!bytes || bytes === 0) return "0 Bytes"; if (!bytes || bytes === 0) return "0 B";
const k = 1024; const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"]; const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k)); const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
} }
@@ -51,12 +50,12 @@ function statusText(status) {
function getStatusClass(status) { function getStatusClass(status) {
const classMap = { const classMap = {
pending: "status-pending", pending: "badge-warning",
processing: "status-processing", processing: "badge-info",
completed: "status-completed", completed: "badge-success",
failed: "status-failed" failed: "badge-error"
}; };
return classMap[status] || "status-pending"; return classMap[status] || "badge-neutral";
} }
let notificationId = 0; let notificationId = 0;
@@ -93,7 +92,6 @@ function handleApiError(error, context = '') {
return message; return message;
} }
// 顶部导航 + 布局
const App = { const App = {
setup() { setup() {
const route = useRoute(); const route = useRoute();
@@ -110,7 +108,6 @@ const App = {
} }
} catch (error) { } catch (error) {
appState.health = null; appState.health = null;
handleApiError(error, '健康检查');
} }
}; };
@@ -143,77 +140,80 @@ const App = {
}; };
}, },
template: ` template: `
<div class="app-shell"> <div class="app-container">
<!-- 通知区域 -->
<div class="notification-container" v-if="appState.notifications.length > 0"> <div class="notification-container" v-if="appState.notifications.length > 0">
<TransitionGroup name="notification"> <TransitionGroup name="notification">
<div <div
v-for="notification in appState.notifications" v-for="notification in appState.notifications"
:key="notification.id" :key="notification.id"
:class="['notification', 'notification-' + notification.type, { 'notification-hidden': !notification.visible }]" :class="['notification', 'notification-' + notification.type]"
> >
<span class="notification-icon"> <div class="notification-icon">
{{ notification.type === 'success' ? '✓' : notification.type === 'error' ? '✕' : notification.type === 'warning' ? '⚠' : 'ℹ' }} {{ notification.type === 'success' ? '✓' : notification.type === 'error' ? '✕' : notification.type === 'warning' ? '!' : 'i' }}
</span> </div>
<span class="notification-message">{{ notification.message }}</span> <div class="notification-content">
<div class="notification-message">{{ notification.message }}</div>
</div>
<button class="notification-close" @click="dismissNotification(notification.id)">×</button> <button class="notification-close" @click="dismissNotification(notification.id)">×</button>
</div> </div>
</TransitionGroup> </TransitionGroup>
</div> </div>
<header class="app-header"> <header class="app-header">
<div class="app-title"> <div class="header-content">
<span class="logo">🔧</span> <div class="logo">
<div> <div class="logo-icon">◈</div>
<h1>STP 模具几何分析中心</h1> <div>
<p class="app-subtitle">专业模具几何分析,支持型腔自动生成与工艺参数计算</p> <div class="logo-text">MoldInsight</div>
<div class="logo-subtitle">STP 模具几何分析</div>
</div>
</div>
<nav class="nav-menu">
<router-link :class="['nav-item', { active: isActive('/').value }]" to="/">
仪表盘
</router-link>
<router-link :class="['nav-item', { active: isActive('/history').value }]" to="/history">
历史记录
</router-link>
</nav>
<div class="user-section">
<div class="status-badge">
<span class="status-dot"></span>
<span>系统运行中</span>
</div>
</div> </div>
</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> </header>
<main class="app-main"> <main class="main-content">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<transition name="fade-slide" mode="out-in"> <transition name="fade" mode="out-in">
<component :is="Component" /> <component :is="Component" />
</transition> </transition>
</router-view> </router-view>
</main> </main>
<footer class="app-footer"> <footer class="app-footer">
<div class="footer-left"> <div class="footer-content">
<span class="footer-item"> <span class="footer-item">
<span class="footer-label">PythonOCC:</span> <span class="footer-label">PythonOCC:</span>
<strong :class="appState.health?.pythonocc ? 'status-success' : 'status-error'"> <strong>{{ appState.health?.pythonocc ? '可用' : '检测中...' }}</strong>
{{ appState.health?.pythonocc ? '可用' : '未知' }}
</strong>
</span> </span>
<span v-if="appState.health" class="footer-divider">|</span> <span class="footer-divider"></span>
<span v-if="appState.health" class="footer-item"> <span v-if="appState.health" class="footer-item">
<span class="footer-label">当前任务数:</span> <span class="footer-label">任务数:</span>
<strong>{{ appState.health.total_tasks }}</strong> <strong>{{ appState.health.total_tasks }}</strong>
</span> </span>
</div> <span class="footer-divider"></span>
<div class="footer-right"> <span class="footer-item">版本 5.0.0</span>
<span class="footer-item">版本: <strong>4.0.0</strong></span>
<span class="footer-divider">|</span>
<span class="footer-item">模具几何分析系统</span>
</div> </div>
</footer> </footer>
</div> </div>
`, `,
}; };
// 仪表盘视图:上传 + 总览
const DashboardView = { const DashboardView = {
setup() { setup() {
const router = useRouter(); const router = useRouter();
@@ -347,7 +347,7 @@ const DashboardView = {
if (task.status === "completed") { if (task.status === "completed") {
state.polling = false; state.polling = false;
state.progress = 100; state.progress = 100;
addNotification(`分析完成,正在跳转到结果页面...`, 'success'); addNotification(`分析完成,正在跳转...`, 'success');
setTimeout(() => { setTimeout(() => {
router.push(`/result/${task.task_id}`); router.push(`/result/${task.task_id}`);
}, 1000); }, 1000);
@@ -361,7 +361,7 @@ const DashboardView = {
state.polling = false; state.polling = false;
state.progress = 0; state.progress = 0;
state.error = "任务处理超时,请稍后查看结果"; state.error = "任务处理超时,请稍后查看结果";
addNotification("任务处理超时,请稍后查看结果", 'warning'); addNotification("任务处理超时", 'warning');
} else { } else {
setTimeout(poll, 1000); setTimeout(poll, 1000);
} }
@@ -383,7 +383,6 @@ const DashboardView = {
} }
} catch (e) { } catch (e) {
state.historySummary = null; state.historySummary = null;
handleApiError(e, '加载历史记录');
} }
}; };
@@ -414,8 +413,7 @@ const DashboardView = {
}; };
}, },
template: ` template: `
<div class="dashboard"> <div>
<!-- 统计卡片 -->
<div class="stats-grid"> <div class="stats-grid">
<div class="stat-card"> <div class="stat-card">
<div class="stat-icon">📁</div> <div class="stat-icon">📁</div>
@@ -429,7 +427,7 @@ const DashboardView = {
</div> </div>
<div class="stat-card"> <div class="stat-card">
<div class="stat-icon">⚡</div> <div class="stat-icon">⚡</div>
<div class="stat-value">{{ state.polling ? '分析中' : '就绪' }}</div> <div class="stat-value">{{ state.polling ? '...' : '就绪' }}</div>
<div class="stat-label">系统状态</div> <div class="stat-label">系统状态</div>
</div> </div>
<div class="stat-card"> <div class="stat-card">
@@ -439,119 +437,105 @@ const DashboardView = {
</div> </div>
</div> </div>
<div class="grid-2"> <div class="content-grid">
<!-- 上传区域 --> <div class="card">
<div class="card upload-card"> <div class="card-header">
<h2> <div class="card-title">
<span class="card-icon">📤</span> <div class="card-title-icon">📤</div>
上传 STP / STEP 文件 上传文件
</h2> </div>
<p class="card-subtitle">上传后系统会自动解析几何、生成模具型腔并计算关键工艺参数。</p> </div>
<div class="card-body">
<div class="upload-section">
<label <label
class="upload-area" class="upload-zone"
:class="{ 'drag-over': state.dragOver }" :class="{ 'drag-over': state.dragOver }"
@dragover.prevent="handleDragOver" @dragover.prevent="handleDragOver"
@dragleave.prevent="handleDragLeave" @dragleave.prevent="handleDragLeave"
@drop.prevent="handleDrop" @drop.prevent="handleDrop"
> >
<input type="file" accept=".stp,.step" @change="handleFileChange" hidden /> <input type="file" accept=".stp,.step" @change="handleFileChange" hidden />
<div class="upload-icon">📂</div> <div class="upload-icon">⬆</div>
<div v-if="!state.selectedFile" class="upload-content"> <div v-if="!state.selectedFile" class="upload-title">点击选择或拖拽文件</div>
<h3>点击选择或拖拽文件到此处</h3> <div v-if="!state.selectedFile" class="upload-hint">支持 .stp / .step 格式,最大 100MB</div>
<p>最大 100MB,支持 .stp / .step</p> <div v-else class="upload-title">{{ state.selectedFile.name }}</div>
</div> <div v-if="state.selectedFile" class="upload-hint">{{ formatFileSize(state.selectedFile.size) }}</div>
<div v-else class="upload-selected"> <div class="upload-formats">
<h3>已选择文件</h3> <span class="format-tag">.stp</span>
<p class="filename"><strong>{{ state.selectedFile.name }}</strong></p> <span class="format-tag">.step</span>
<p class="filesize">大小:{{ formatFileSize(state.selectedFile.size) }}</p>
<button class="btn btn-secondary clear-btn" @click.stop="clearFile">清除选择</button>
</div> </div>
</label> </label>
<button <div class="flex gap-2 mt-4">
class="btn btn-primary upload-btn" <button
:disabled="!state.selectedFile || state.uploading" v-if="state.selectedFile"
@click="uploadFile" class="btn btn-secondary"
> @click="clearFile"
<span v-if="!state.uploading">🚀 开始分析</span> >
<span v-else>⏳ 正在上传...</span> 清除
</button> </button>
<button
class="btn btn-primary btn-full"
:disabled="!state.selectedFile || state.uploading"
@click="uploadFile"
>
<span v-if="state.uploading" class="loading-spinner"></span>
<span v-if="!state.uploading">开始分析</span>
<span v-else>上传中...</span>
</button>
</div>
<p v-if="state.error" class="error-message">{{ state.error }}</p> <p v-if="state.error" class="badge badge-error mt-4">{{ state.error }}</p>
</div>
<!-- 当前进度 --> <div v-if="state.currentTask" class="mt-6">
<div v-if="state.currentTask" class="current-task-card"> <div class="flex items-center justify-between mb-2">
<h4>📋 当前任务</h4> <span class="text-secondary">当前任务</span>
<div class="task-info"> <span :class="['badge', getStatusClass(state.currentTask.status)]">
<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) }} {{ statusText(state.currentTask.status) }}
</span> </span>
</div> </div>
</div>
<div v-if="state.polling" class="progress-container">
<div class="progress-bar"> <div class="progress-bar">
<div class="progress-fill" :style="{ width: state.progress + '%' }"></div> <div class="progress-fill" :style="{ width: state.progress + '%' }"></div>
</div> </div>
<p class="progress-text">正在后台分析,请稍候...</p> <div class="text-muted text-sm mt-2">{{ state.currentTask.filename }}</div>
</div> </div>
</div> </div>
</div> </div>
<!-- 项目总览 --> <div class="card">
<div class="card summary-card"> <div class="card-header">
<h2> <div class="card-title">
<span class="card-icon">📊</span> <div class="card-title-icon">📊</div>
项目总览 项目总览
</h2> </div>
</div>
<div v-if="latestFile" class="latest-file"> <div class="card-body">
<h4>最近上传文件</h4> <div v-if="latestFile" class="file-item">
<div class="file-preview"> <div class="file-icon">📄</div>
<span class="file-icon">📄</span> <div class="file-info">
<div class="file-details">
<div class="file-name">{{ latestFile.filename }}</div> <div class="file-name">{{ latestFile.filename }}</div>
<div class="file-meta"> <div class="file-meta">
<span>📅 {{ formatDateTime(latestFile.last_upload) }}</span> <span>{{ latestFile.record_count }} 条记录</span>
<span>📊 {{ latestFile.record_count }} 条记录</span> <span>{{ formatDateTime(latestFile.last_upload) }}</span>
</div> </div>
</div> </div>
</div> </div>
</div>
<div v-else class="empty-state"> <div v-else class="empty-state">
<div class="empty-state-icon">📁</div> <div class="empty-icon">📁</div>
<h3>暂无历史记录</h3> <div class="empty-title">暂无历史记录</div>
<p>上传一个 STP 文件开始分析吧</p> <div class="empty-description">上传一个 STP 文件开始分析</div>
</div> </div>
<router-link class="btn btn-secondary link-btn" to="/history"> <router-link class="btn btn-secondary btn-full mt-6" to="/history">
查看详细历史记录 → 查看历史记录
</router-link> </router-link>
</div>
</div> </div>
</div> </div>
</div> </div>
`, `,
}; };
// 历史记录视图
const HistoryView = { const HistoryView = {
setup() { setup() {
const router = useRouter(); const router = useRouter();
@@ -613,73 +597,72 @@ const HistoryView = {
}; };
}, },
template: ` template: `
<div class="history-view"> <div>
<h2> <div class="result-header">
<span class="card-icon">📋</span> <div class="result-title">
历史记录 📋 历史记录
</h2> </div>
<p class="card-subtitle">按文件名聚合展示所有上传与分析任务。</p>
<div v-if="loading" class="loading">
<div class="spinner"></div>
<p class="loading-text">正在加载历史记录...</p>
</div> </div>
<p v-if="error" class="error-message">{{ error }}</p> <div v-if="loading" class="flex items-center justify-center gap-4" style="padding: 4rem;">
<div class="loading-spinner"></div>
<span class="text-secondary">加载中...</span>
</div>
<p v-if="error" class="badge badge-error">{{ error }}</p>
<div v-if="!loading && files.length === 0 && !error" class="empty-state"> <div v-if="!loading && files.length === 0 && !error" class="empty-state">
<div class="empty-state-icon">📁</div> <div class="empty-icon">📁</div>
<h3>暂无历史记录</h3> <div class="empty-title">暂无历史记录</div>
<p>先在仪表盘上传一个文件吧。</p> <div class="empty-description">上传文件后这里会显示分析历史</div>
</div> </div>
<TransitionGroup name="list" tag="div" class="file-list" v-if="files.length"> <TransitionGroup name="list" tag="div" class="file-list" v-if="files.length">
<div <div
v-for="file in files" v-for="file in files"
:key="file.filename" :key="file.filename"
class="file-card" class="card"
:class="{ expanded: expanded[file.filename] }" style="margin-bottom: var(--space-4);"
> >
<div class="file-header" @click="toggleExpand(file.filename)"> <div class="file-header" @click="toggleExpand(file.filename)">
<div class="file-info"> <div class="file-info">
<div class="file-name">📄 {{ file.filename }}</div> <div class="file-name">{{ file.filename }}</div>
<div class="file-meta"> <div class="file-meta">
总记录:{{ file.record_count }} 条 {{ file.record_count }} 条记录 · {{ formatDateTime(file.last_upload) }}
<span v-if="file.last_upload"> | 最近上传:{{ formatDateTime(file.last_upload) }}</span>
</div> </div>
</div> </div>
<button class="btn btn-secondary expand-btn"> <button class="btn btn-ghost">
{{ expanded[file.filename] ? '收起' : '展开' }} {{ expanded[file.filename] ? '收起' : '展开' }}
</button> </button>
</div> </div>
<transition name="expand"> <transition name="slide-up">
<div class="record-list" v-if="expanded[file.filename]"> <div v-if="expanded[file.filename]" style="padding: var(--space-4); border-top: 1px solid var(--border-light);">
<div v-if="!file.records" class="loading"> <div v-if="!file.records" class="flex items-center justify-center gap-2">
<div class="spinner"></div> <div class="loading-spinner"></div>
<p class="loading-text">正在加载记录...</p> <span class="text-secondary">加载中...</span>
</div> </div>
<div v-else-if="file.records.length === 0" class="empty-state"> <div v-else-if="file.records.length === 0" class="text-center text-muted">
<p>暂无详细记录</p> 暂无详细记录
</div> </div>
<TransitionGroup name="list" tag="div" v-else> <TransitionGroup name="list" tag="div" v-else class="file-list">
<div <div
v-for="record in file.records" v-for="record in file.records"
:key="record.task_id" :key="record.task_id"
class="record-item" class="file-item"
@click="openResult(record.task_id)" @click="openResult(record.task_id)"
> >
<div class="record-header"> <div class="file-icon">📊</div>
<span class="record-date">📅 {{ formatDateTime(record.upload_time) }}</span> <div class="file-info">
<span class="status-badge" :class="'status-' + record.status"> <div class="file-name">{{ record.task_id.slice(0, 8) }}...</div>
{{ statusText(record.status) }} <div class="file-meta">
</span> <span>{{ formatDateTime(record.upload_time) }}</span>
</div> <span>{{ formatFileSize(record.file_size) }}</span>
<div class="record-meta"> </div>
<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>
<span :class="['badge', record.status === 'completed' ? 'badge-success' : record.status === 'failed' ? 'badge-error' : 'badge-warning']">
{{ statusText(record.status) }}
</span>
</div> </div>
</TransitionGroup> </TransitionGroup>
</div> </div>
@@ -690,7 +673,6 @@ const HistoryView = {
`, `,
}; };
// 结果详情视图
const ResultView = { const ResultView = {
setup() { setup() {
const route = useRoute(); const route = useRoute();
@@ -736,232 +718,159 @@ const ResultView = {
}; };
}, },
template: ` template: `
<div class="result-view"> <div>
<h2> <div class="result-header">
<span class="card-icon">📊</span> <div class="result-title">
分析结果详情 📊 分析结果
</h2> </div>
<router-link class="btn btn-secondary" to="/">返回仪表盘</router-link>
<div v-if="loading" class="loading">
<div class="spinner"></div>
<p class="loading-text">正在加载任务 {{ taskId }} 的分析结果...</p>
</div> </div>
<p v-if="error" class="error-message">{{ error }}</p> <div v-if="loading" class="flex items-center justify-center gap-4" style="padding: 4rem;">
<div class="loading-spinner"></div>
<span class="text-secondary">加载中...</span>
</div>
<div v-if="task && !loading" class="result-layout"> <p v-if="error" class="badge badge-error">{{ error }}</p>
<!-- 任务信息卡片 -->
<section class="result-card task-card"> <div v-if="task && !loading">
<h3>📝 任务信息</h3> <div class="card mb-6">
<div class="task-info"> <div class="card-header">
<div class="data-item"> <div class="card-title">
<span class="data-label">任务 ID</span> <div class="card-title-icon">📝</div>
<span class="data-value">{{ task.task_id || 'N/A' }}</span> 任务信息
</div> </div>
<div class="data-item"> <span :class="['badge', task.status === 'completed' ? 'badge-success' : task.status === 'failed' ? 'badge-error' : 'badge-info']">
<span class="data-label">文件名</span> {{ statusText(task.status) }}
<span class="data-value">{{ task.filename || 'N/A' }}</span> </span>
</div>
<div class="card-body">
<div class="grid-2">
<div class="file-item">
<div class="file-icon">📄</div>
<div class="file-info">
<div class="file-name">{{ task.filename || 'N/A' }}</div>
<div class="file-meta">文件名</div>
</div>
</div>
<div class="file-item">
<div class="file-icon">💾</div>
<div class="file-info">
<div class="file-name">{{ task.file_size ? formatFileSize(task.file_size) : 'N/A' }}</div>
<div class="file-meta">文件大小</div>
</div>
</div>
<div class="file-item">
<div class="file-icon">🆔</div>
<div class="file-info">
<div class="file-name font-mono">{{ task.task_id }}</div>
<div class="file-meta">任务 ID</div>
</div>
</div>
<div class="file-item">
<div class="file-icon">📅</div>
<div class="file-info">
<div class="file-name">{{ task.upload_time ? formatDateTime(task.upload_time) : 'N/A' }}</div>
<div class="file-meta">上传时间</div>
</div>
</div>
</div> </div>
<div class="data-item"> <div v-if="task.error" class="badge badge-error mt-4">
<span class="data-label">文件大小</span> 错误: {{ task.error }}
<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> </div>
<div v-if="task.error" class="error-message"> </div>
<strong>错误信息:</strong>{{ task.error }}
</div>
</section>
<!-- 几何属性 --> <div class="result-grid">
<section class="grid-3">
<div class="result-card"> <div class="result-card">
<h3>📐 几何属性</h3> <div class="result-card-header">
<div v-if="geometry" class="geometry-data"> <div class="result-card-icon" style="background: var(--primary-50); color: var(--primary-600);">📐</div>
<div class="data-item"> <div class="result-card-title">几何属性</div>
<span class="data-label">体积</span> </div>
<span class="data-value"> <div v-if="geometry" class="result-card-value">
{{ geometry.volume ? formatNumber(geometry.volume) : 'N/A' }} <div class="mb-4">
<span class="data-unit">mm³</span> <div class="text-muted text-sm">体积</div>
</span> <div>{{ geometry.volume ? formatNumber(geometry.volume) : 'N/A' }} mm³</div>
</div> </div>
<div class="data-item"> <div class="mb-4">
<span class="data-label">表面积</span> <div class="text-muted text-sm">表面积</div>
<span class="data-value"> <div>{{ geometry.surface_area ? formatNumber(geometry.surface_area) : 'N/A' }} mm²</div>
{{ 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> </div>
<div v-else class="empty-state"> <div v-else class="text-muted">无几何数据</div>
<p>无几何数据</p>
</div>
</div> </div>
<div class="result-card"> <div class="result-card">
<h3>📦 边界框</h3> <div class="result-card-header">
<div v-if="geometry?.bounding_box" class="bounding-box-data"> <div class="result-card-icon" style="background: var(--accent-50); color: var(--accent-600);">📦</div>
<div class="data-item coordinate-item"> <div class="result-card-title">边界框</div>
<span class="data-label">尺寸 (mm)</span> </div>
<span class="data-value"> <div v-if="geometry?.bounding_box" class="result-card-value">
<div class="mb-4">
<div class="text-muted text-sm">尺寸</div>
<div>
{{ geometry.bounding_box.dimensions[0].toFixed(2) }} × {{ geometry.bounding_box.dimensions[0].toFixed(2) }} ×
{{ geometry.bounding_box.dimensions[1].toFixed(2) }} × {{ geometry.bounding_box.dimensions[1].toFixed(2) }} ×
{{ geometry.bounding_box.dimensions[2].toFixed(2) }} {{ geometry.bounding_box.dimensions[2].toFixed(2) }} mm
</span> </div>
</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> </div>
<div v-else class="empty-state"> <div v-else class="text-muted">无边界框数据</div>
<p>无边界框数据</p>
</div>
</div> </div>
<div class="result-card"> <div class="result-card">
<h3>🔺 拓扑结构</h3> <div class="result-card-header">
<div v-if="geometry?.topology" class="topology-data"> <div class="result-card-icon" style="background: var(--warning-bg); color: var(--warning);">🔺</div>
<div class="data-item"> <div class="result-card-title">拓扑结构</div>
<span class="data-label">面数</span> </div>
<span class="data-value">{{ geometry.topology.faces || 0 }}</span> <div v-if="geometry?.topology" class="result-card-value">
</div> <div class="mb-4">
<div class="data-item"> <div class="text-muted text-sm">面 / 边 / 顶点</div>
<span class="data-label">边数</span> <div>{{ geometry.topology.faces || 0 }} / {{ geometry.topology.edges || 0 }} / {{ geometry.topology.vertices || 0 }}</div>
<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> </div>
<div v-else class="empty-state"> <div v-else class="text-muted">无拓扑数据</div>
<p>无拓扑数据</p>
</div>
</div> </div>
</section> </div>
<!-- 模具工艺信息 --> <div v-if="keyInfo" class="card mt-6">
<section class="grid-2"> <div class="card-header">
<div class="result-card"> <div class="card-title">
<h3>🔧 模具 / 工艺关键信息</h3> <div class="card-title-icon">🔧</div>
<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> </div>
<div class="card-body">
<div class="result-card"> <div class="grid-2">
<h3>🧱 网格摘要</h3> <div class="file-item" v-if="keyInfo.metadata?.shrinkage_rate">
<div v-if="meshSummary" class="metrics-data"> <div class="file-icon">📉</div>
<div class="data-item"> <div class="file-info">
<span class="data-label">网格质量等级</span> <div class="file-name">{{ keyInfo.metadata.shrinkage_rate }}</div>
<span class="data-value">{{ meshSummary.quality }}</span> <div class="file-meta">收缩率</div>
</div>
</div> </div>
<div class="data-item"> <div class="file-item" v-if="keyInfo.metadata?.draft_angle !== undefined">
<span class="data-label">顶点数</span> <div class="file-icon">📐</div>
<span class="data-value">{{ meshSummary.vertex_count }}</span> <div class="file-info">
<div class="file-name">{{ keyInfo.metadata.draft_angle }}°</div>
<div class="file-meta">拔模角</div>
</div>
</div> </div>
<div class="data-item"> <div class="file-item" v-if="keyInfo.manufacturing_info?.estimated_clamping_force">
<span class="data-label">面数</span> <div class="file-icon">💪</div>
<span class="data-value">{{ meshSummary.face_count }}</span> <div class="file-info">
<div class="file-name">{{ keyInfo.manufacturing_info.estimated_clamping_force }}</div>
<div class="file-meta">预估夹紧力</div>
</div>
</div> </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>
</div> </div>
</section> </div>
</div> </div>
</div> </div>
`, `,
}; };
// 路由配置
const routes = [ const routes = [
{ path: "/", component: DashboardView }, { path: "/", component: DashboardView },
{ path: "/history", component: HistoryView }, { path: "/history", component: HistoryView },
@@ -973,5 +882,6 @@ const router = createRouter({
routes, routes,
}); });
// 挂载应用 const app = createApp(App);
createApp(App).use(router).mount("#app"); app.use(router);
app.mount("#app");