Files
geMoldInsight/static/vue-app.js
T

888 lines
28 KiB
JavaScript
Raw Normal View History

2026-02-17 00:42:55 +08:00
/**
* STP模具几何分析中心 - Vue3单页应用
2026-03-04 00:10:05 +08:00
* 版本: 5.0.0 - Modern Minimal UI
* 设计灵感: Linear, Notion, Vercel, Stripe
2026-02-17 00:42:55 +08:00
*/
2026-02-17 00:12:36 +08:00
2026-03-03 23:57:04 +08:00
const { createApp, ref, computed, onMounted, reactive, watch, nextTick } = Vue;
2026-02-17 00:12:36 +08:00
const { createRouter, createWebHistory, useRoute, useRouter } = VueRouter;
2026-02-17 00:42:55 +08:00
const appState = reactive({
health: null,
loading: false,
2026-03-03 23:57:04 +08:00
notifications: [],
2026-03-04 00:10:05 +08:00
theme: 'modern'
2026-02-17 00:42:55 +08:00
});
2026-02-17 00:12:36 +08:00
function formatFileSize(bytes) {
2026-03-04 00:10:05 +08:00
if (!bytes || bytes === 0) return "0 B";
2026-02-17 00:12:36 +08:00
const k = 1024;
2026-03-04 00:10:05 +08:00
const sizes = ["B", "KB", "MB", "GB"];
2026-02-17 00:12:36 +08:00
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 {
2026-02-17 00:42:55 +08:00
return new Date(dateString).toLocaleString('zh-CN');
2026-02-17 00:12:36 +08:00
} catch {
return dateString;
}
}
function statusText(status) {
const map = {
pending: "排队中",
processing: "处理中",
completed: "已完成",
failed: "失败",
};
return map[status] || status || "未知";
}
2026-02-17 00:42:55 +08:00
function getStatusClass(status) {
const classMap = {
2026-03-04 00:10:05 +08:00
pending: "badge-warning",
processing: "badge-info",
completed: "badge-success",
failed: "badge-error"
2026-02-17 00:42:55 +08:00
};
2026-03-04 00:10:05 +08:00
return classMap[status] || "badge-neutral";
2026-02-17 00:42:55 +08:00
}
2026-03-03 23:57:04 +08:00
let notificationId = 0;
2026-02-17 00:42:55 +08:00
function addNotification(message, type = 'info') {
2026-03-03 23:57:04 +08:00
const id = ++notificationId;
2026-02-17 00:42:55 +08:00
const notification = {
2026-03-03 23:57:04 +08:00
id,
2026-02-17 00:42:55 +08:00
message,
type,
2026-03-03 23:57:04 +08:00
timestamp: new Date(),
visible: true
2026-02-17 00:42:55 +08:00
};
appState.notifications.push(notification);
setTimeout(() => {
2026-03-03 23:57:04 +08:00
const index = appState.notifications.findIndex(n => n.id === id);
2026-02-17 00:42:55 +08:00
if (index > -1) {
2026-03-03 23:57:04 +08:00
appState.notifications[index].visible = false;
setTimeout(() => {
const idx = appState.notifications.findIndex(n => n.id === id);
if (idx > -1) {
appState.notifications.splice(idx, 1);
}
}, 300);
2026-02-17 00:42:55 +08:00
}
}, 5000);
}
function handleApiError(error, context = '') {
console.error(`API错误 [${context}]:`, error);
const message = error.message || '请求失败,请稍后重试';
addNotification(message, 'error');
return message;
}
2026-02-17 00:12:36 +08:00
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) {
2026-02-17 00:42:55 +08:00
appState.health = await res.json();
2026-02-17 00:12:36 +08:00
}
2026-02-17 00:42:55 +08:00
} catch (error) {
appState.health = null;
2026-02-17 00:12:36 +08:00
}
};
2026-02-17 00:42:55 +08:00
const dismissNotification = (id) => {
const index = appState.notifications.findIndex(n => n.id === id);
if (index > -1) {
2026-03-03 23:57:04 +08:00
appState.notifications[index].visible = false;
setTimeout(() => {
const idx = appState.notifications.findIndex(n => n.id === id);
if (idx > -1) {
appState.notifications.splice(idx, 1);
}
}, 300);
2026-02-17 00:42:55 +08:00
}
};
2026-02-17 00:12:36 +08:00
2026-02-17 00:42:55 +08:00
onMounted(() => {
loadHealth();
setInterval(loadHealth, 30000);
});
return {
route,
router,
appState,
isActive,
dismissNotification,
getStatusClass,
statusText
};
2026-02-17 00:12:36 +08:00
},
template: `
2026-03-04 00:10:05 +08:00
<div class="app-container">
2026-02-17 00:42:55 +08:00
<div class="notification-container" v-if="appState.notifications.length > 0">
2026-03-03 23:57:04 +08:00
<TransitionGroup name="notification">
<div
v-for="notification in appState.notifications"
:key="notification.id"
2026-03-04 00:10:05 +08:00
:class="['notification', 'notification-' + notification.type]"
2026-03-03 23:57:04 +08:00
>
2026-03-04 00:10:05 +08:00
<div class="notification-icon">
{{ notification.type === 'success' ? '✓' : notification.type === 'error' ? '✕' : notification.type === 'warning' ? '!' : 'i' }}
</div>
<div class="notification-content">
<div class="notification-message">{{ notification.message }}</div>
</div>
2026-03-03 23:57:04 +08:00
<button class="notification-close" @click="dismissNotification(notification.id)">×</button>
</div>
</TransitionGroup>
2026-02-17 00:42:55 +08:00
</div>
2026-02-17 00:12:36 +08:00
<header class="app-header">
2026-03-04 00:10:05 +08:00
<div class="header-content">
<div class="logo">
<div class="logo-icon">◈</div>
<div>
<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>
2026-02-17 00:12:36 +08:00
</div>
</div>
</header>
2026-03-04 00:10:05 +08:00
<main class="main-content">
2026-03-03 23:57:04 +08:00
<router-view v-slot="{ Component }">
2026-03-04 00:10:05 +08:00
<transition name="fade" mode="out-in">
2026-03-03 23:57:04 +08:00
<component :is="Component" />
</transition>
</router-view>
2026-02-17 00:12:36 +08:00
</main>
<footer class="app-footer">
2026-03-04 00:10:05 +08:00
<div class="footer-content">
2026-03-03 23:57:04 +08:00
<span class="footer-item">
<span class="footer-label">PythonOCC:</span>
2026-03-04 00:10:05 +08:00
<strong>{{ appState.health?.pythonocc ? '可用' : '检测中...' }}</strong>
2026-03-03 23:57:04 +08:00
</span>
2026-03-04 00:10:05 +08:00
<span class="footer-divider"></span>
2026-03-03 23:57:04 +08:00
<span v-if="appState.health" class="footer-item">
2026-03-04 00:10:05 +08:00
<span class="footer-label">任务数:</span>
2026-03-03 23:57:04 +08:00
<strong>{{ appState.health.total_tasks }}</strong>
</span>
2026-03-04 00:10:05 +08:00
<span class="footer-divider"></span>
<span class="footer-item">版本 5.0.0</span>
2026-02-17 00:12:36 +08:00
</div>
</footer>
</div>
`,
};
const DashboardView = {
setup() {
const router = useRouter();
2026-02-17 00:42:55 +08:00
const state = reactive({
selectedFile: null,
uploading: false,
error: "",
currentTask: null,
polling: false,
historySummary: null,
2026-03-03 23:57:04 +08:00
dragOver: false,
progress: 0
2026-02-17 00:42:55 +08:00
});
2026-02-17 00:12:36 +08:00
2026-02-17 00:42:55 +08:00
const totalFiles = computed(() => state.historySummary?.total_files || 0);
2026-02-17 00:12:36 +08:00
const totalRecords = computed(() =>
2026-02-17 00:42:55 +08:00
(state.historySummary?.files || []).reduce(
2026-02-17 00:12:36 +08:00
(sum, f) => sum + (f.record_count || 0),
0
)
);
const latestFile = computed(() =>
2026-02-17 00:42:55 +08:00
(state.historySummary?.files || [])[0] || null
2026-02-17 00:12:36 +08:00
);
const handleFileChange = (event) => {
const file = event.target.files[0];
if (!file) return;
2026-02-17 00:42:55 +08:00
validateAndSelectFile(file);
};
2026-02-17 00:12:36 +08:00
2026-02-17 00:42:55 +08:00
const validateAndSelectFile = (file) => {
if (!file.name.toLowerCase().endsWith(".stp") &&
!file.name.toLowerCase().endsWith(".step")) {
state.error = "请选择 STP 或 STEP 格式文件";
state.selectedFile = null;
2026-02-17 00:12:36 +08:00
return;
}
if (file.size > 100 * 1024 * 1024) {
2026-02-17 00:42:55 +08:00
state.error = "文件大小不能超过 100MB";
state.selectedFile = null;
2026-02-17 00:12:36 +08:00
return;
}
2026-02-17 00:42:55 +08:00
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]);
}
2026-02-17 00:12:36 +08:00
};
const uploadFile = async () => {
2026-02-17 00:42:55 +08:00
if (!state.selectedFile) return;
state.uploading = true;
state.error = "";
2026-03-03 23:57:04 +08:00
state.progress = 0;
2026-02-17 00:12:36 +08:00
const formData = new FormData();
2026-02-17 00:42:55 +08:00
formData.append("file", state.selectedFile);
2026-02-17 00:12:36 +08:00
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();
2026-02-17 00:42:55 +08:00
state.currentTask = {
2026-02-17 00:12:36 +08:00
task_id: data.task_id,
status: "processing",
filename: data.file_info?.filename,
file_size: data.file_info?.size,
};
2026-02-17 00:42:55 +08:00
addNotification(`文件上传成功,开始分析...`, 'success');
2026-02-17 00:12:36 +08:00
startPolling(data.task_id);
} catch (e) {
2026-02-17 00:42:55 +08:00
const errorMsg = handleApiError(e, '文件上传');
state.error = errorMsg;
2026-02-17 00:12:36 +08:00
} finally {
2026-02-17 00:42:55 +08:00
state.uploading = false;
2026-02-17 00:12:36 +08:00
}
};
const startPolling = async (taskId) => {
2026-02-17 00:42:55 +08:00
state.polling = true;
2026-03-03 23:57:04 +08:00
state.progress = 10;
2026-02-17 00:47:39 +08:00
let pollCount = 0;
2026-03-03 23:57:04 +08:00
const maxPolls = 300;
2026-02-17 00:47:39 +08:00
2026-02-17 00:12:36 +08:00
const poll = async () => {
try {
2026-02-17 00:47:39 +08:00
pollCount++;
2026-03-03 23:57:04 +08:00
state.progress = Math.min(90, 10 + pollCount * 0.5);
2026-02-17 00:12:36 +08:00
const res = await fetch(`/status/${taskId}`, { method: "POST" });
2026-02-17 00:47:39 +08:00
if (res.status === 404) {
state.polling = false;
state.error = "任务不存在或已被删除";
addNotification("任务不存在或已被删除", 'error');
return;
}
2026-02-17 00:12:36 +08:00
if (!res.ok) {
2026-02-17 00:47:39 +08:00
throw new Error(`查询任务状态失败: ${res.status} ${res.statusText}`);
2026-02-17 00:12:36 +08:00
}
2026-02-17 00:47:39 +08:00
2026-02-17 00:12:36 +08:00
const task = await res.json();
2026-02-17 00:42:55 +08:00
state.currentTask = task;
2026-02-17 00:12:36 +08:00
if (task.status === "completed") {
2026-02-17 00:42:55 +08:00
state.polling = false;
2026-03-03 23:57:04 +08:00
state.progress = 100;
2026-03-04 00:10:05 +08:00
addNotification(`分析完成,正在跳转...`, 'success');
2026-02-17 00:42:55 +08:00
setTimeout(() => {
router.push(`/result/${task.task_id}`);
}, 1000);
2026-02-17 00:12:36 +08:00
} else if (task.status === "failed") {
2026-02-17 00:42:55 +08:00
state.polling = false;
2026-03-03 23:57:04 +08:00
state.progress = 0;
2026-02-17 00:42:55 +08:00
const errorMsg = task.error || "未知错误";
state.error = `分析失败: ${errorMsg}`;
addNotification(`分析失败: ${errorMsg}`, 'error');
2026-02-17 00:47:39 +08:00
} else if (pollCount >= maxPolls) {
state.polling = false;
2026-03-03 23:57:04 +08:00
state.progress = 0;
2026-02-17 00:47:39 +08:00
state.error = "任务处理超时,请稍后查看结果";
2026-03-04 00:10:05 +08:00
addNotification("任务处理超时", 'warning');
2026-02-17 00:12:36 +08:00
} else {
setTimeout(poll, 1000);
}
} catch (e) {
2026-02-17 00:42:55 +08:00
state.polling = false;
2026-03-03 23:57:04 +08:00
state.progress = 0;
2026-02-17 00:42:55 +08:00
const errorMsg = handleApiError(e, '任务轮询');
state.error = errorMsg;
2026-02-17 00:12:36 +08:00
}
};
poll();
};
const loadHistorySummary = async () => {
try {
const res = await fetch("/api/history", { method: "POST" });
if (res.ok) {
2026-02-17 00:42:55 +08:00
state.historySummary = await res.json();
2026-02-17 00:12:36 +08:00
}
2026-02-17 00:42:55 +08:00
} catch (e) {
state.historySummary = null;
2026-02-17 00:12:36 +08:00
}
};
2026-02-17 00:42:55 +08:00
const clearFile = () => {
state.selectedFile = null;
state.error = "";
};
2026-02-17 00:12:36 +08:00
onMounted(() => {
loadHistorySummary();
});
return {
2026-02-17 00:42:55 +08:00
state,
2026-02-17 00:12:36 +08:00
totalFiles,
totalRecords,
latestFile,
handleFileChange,
2026-02-17 00:42:55 +08:00
handleDragOver,
handleDragLeave,
handleDrop,
2026-02-17 00:12:36 +08:00
uploadFile,
2026-02-17 00:42:55 +08:00
clearFile,
2026-02-17 00:12:36 +08:00
formatFileSize,
statusText,
formatDateTime,
2026-02-17 00:42:55 +08:00
getStatusClass
2026-02-17 00:12:36 +08:00
};
},
template: `
2026-03-04 00:10:05 +08:00
<div>
2026-03-03 23:57:04 +08:00
<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>
2026-03-04 00:10:05 +08:00
<div class="stat-value">{{ state.polling ? '...' : '就绪' }}</div>
2026-03-03 23:57:04 +08:00
<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>
2026-03-04 00:10:05 +08:00
<div class="content-grid">
<div class="card">
<div class="card-header">
<div class="card-title">
<div class="card-title-icon">📤</div>
上传文件
</div>
</div>
<div class="card-body">
2026-02-17 00:47:39 +08:00
<label
2026-03-04 00:10:05 +08:00
class="upload-zone"
2026-02-17 00:47:39 +08:00
:class="{ 'drag-over': state.dragOver }"
@dragover.prevent="handleDragOver"
@dragleave.prevent="handleDragLeave"
@drop.prevent="handleDrop"
>
2026-02-17 00:12:36 +08:00
<input type="file" accept=".stp,.step" @change="handleFileChange" hidden />
2026-03-04 00:10:05 +08:00
<div class="upload-icon">⬆</div>
<div v-if="!state.selectedFile" class="upload-title">点击选择或拖拽文件</div>
<div v-if="!state.selectedFile" class="upload-hint">支持 .stp / .step 格式,最大 100MB</div>
<div v-else class="upload-title">{{ state.selectedFile.name }}</div>
<div v-if="state.selectedFile" class="upload-hint">{{ formatFileSize(state.selectedFile.size) }}</div>
<div class="upload-formats">
<span class="format-tag">.stp</span>
<span class="format-tag">.step</span>
2026-02-17 00:12:36 +08:00
</div>
</label>
2026-03-04 00:10:05 +08:00
<div class="flex gap-2 mt-4">
<button
v-if="state.selectedFile"
class="btn btn-secondary"
@click="clearFile"
>
清除
</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>
2026-02-17 00:12:36 +08:00
2026-03-04 00:10:05 +08:00
<p v-if="state.error" class="badge badge-error mt-4">{{ state.error }}</p>
2026-02-17 00:12:36 +08:00
2026-03-04 00:10:05 +08:00
<div v-if="state.currentTask" class="mt-6">
<div class="flex items-center justify-between mb-2">
<span class="text-secondary">当前任务</span>
<span :class="['badge', getStatusClass(state.currentTask.status)]">
2026-03-03 23:57:04 +08:00
{{ statusText(state.currentTask.status) }}
</span>
</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: state.progress + '%' }"></div>
</div>
2026-03-04 00:10:05 +08:00
<div class="text-muted text-sm mt-2">{{ state.currentTask.filename }}</div>
2026-02-17 00:12:36 +08:00
</div>
</div>
</div>
2026-03-04 00:10:05 +08:00
<div class="card">
<div class="card-header">
<div class="card-title">
<div class="card-title-icon">📊</div>
项目总览
</div>
</div>
<div class="card-body">
<div v-if="latestFile" class="file-item">
<div class="file-icon">📄</div>
<div class="file-info">
2026-03-03 23:57:04 +08:00
<div class="file-name">{{ latestFile.filename }}</div>
<div class="file-meta">
2026-03-04 00:10:05 +08:00
<span>{{ latestFile.record_count }} 条记录</span>
<span>{{ formatDateTime(latestFile.last_upload) }}</span>
2026-02-17 00:12:36 +08:00
</div>
</div>
</div>
2026-03-04 00:10:05 +08:00
<div v-else class="empty-state">
<div class="empty-icon">📁</div>
<div class="empty-title">暂无历史记录</div>
<div class="empty-description">上传一个 STP 文件开始分析</div>
</div>
2026-02-17 00:12:36 +08:00
2026-03-04 00:10:05 +08:00
<router-link class="btn btn-secondary btn-full mt-6" to="/history">
查看历史记录
</router-link>
</div>
2026-02-17 00:12:36 +08:00
</div>
2026-03-03 23:57:04 +08:00
</div>
2026-02-17 00:12:36 +08:00
</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: `
2026-03-04 00:10:05 +08:00
<div>
<div class="result-header">
<div class="result-title">
📋 历史记录
</div>
</div>
<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>
2026-02-17 00:12:36 +08:00
</div>
2026-03-04 00:10:05 +08:00
<p v-if="error" class="badge badge-error">{{ error }}</p>
2026-02-17 00:12:36 +08:00
2026-03-03 23:57:04 +08:00
<div v-if="!loading && files.length === 0 && !error" class="empty-state">
2026-03-04 00:10:05 +08:00
<div class="empty-icon">📁</div>
<div class="empty-title">暂无历史记录</div>
<div class="empty-description">上传文件后这里会显示分析历史</div>
2026-02-17 00:12:36 +08:00
</div>
2026-03-03 23:57:04 +08:00
<TransitionGroup name="list" tag="div" class="file-list" v-if="files.length">
2026-02-17 00:12:36 +08:00
<div
v-for="file in files"
:key="file.filename"
2026-03-04 00:10:05 +08:00
class="card"
style="margin-bottom: var(--space-4);"
2026-02-17 00:12:36 +08:00
>
<div class="file-header" @click="toggleExpand(file.filename)">
2026-03-03 23:57:04 +08:00
<div class="file-info">
2026-03-04 00:10:05 +08:00
<div class="file-name">{{ file.filename }}</div>
2026-02-17 00:12:36 +08:00
<div class="file-meta">
2026-03-04 00:10:05 +08:00
{{ file.record_count }} 条记录 · {{ formatDateTime(file.last_upload) }}
2026-02-17 00:12:36 +08:00
</div>
</div>
2026-03-04 00:10:05 +08:00
<button class="btn btn-ghost">
2026-02-17 00:12:36 +08:00
{{ expanded[file.filename] ? '收起' : '展开' }}
</button>
</div>
2026-03-04 00:10:05 +08:00
<transition name="slide-up">
<div v-if="expanded[file.filename]" style="padding: var(--space-4); border-top: 1px solid var(--border-light);">
<div v-if="!file.records" class="flex items-center justify-center gap-2">
<div class="loading-spinner"></div>
<span class="text-secondary">加载中...</span>
2026-02-17 00:12:36 +08:00
</div>
2026-03-04 00:10:05 +08:00
<div v-else-if="file.records.length === 0" class="text-center text-muted">
暂无详细记录
2026-02-17 00:12:36 +08:00
</div>
2026-03-04 00:10:05 +08:00
<TransitionGroup name="list" tag="div" v-else class="file-list">
2026-03-03 23:57:04 +08:00
<div
v-for="record in file.records"
:key="record.task_id"
2026-03-04 00:10:05 +08:00
class="file-item"
2026-03-03 23:57:04 +08:00
@click="openResult(record.task_id)"
>
2026-03-04 00:10:05 +08:00
<div class="file-icon">📊</div>
<div class="file-info">
<div class="file-name">{{ record.task_id.slice(0, 8) }}...</div>
<div class="file-meta">
<span>{{ formatDateTime(record.upload_time) }}</span>
<span>{{ formatFileSize(record.file_size) }}</span>
</div>
2026-03-03 23:57:04 +08:00
</div>
2026-03-04 00:10:05 +08:00
<span :class="['badge', record.status === 'completed' ? 'badge-success' : record.status === 'failed' ? 'badge-error' : 'badge-warning']">
{{ statusText(record.status) }}
</span>
2026-03-03 23:57:04 +08:00
</div>
</TransitionGroup>
2026-02-17 00:12:36 +08:00
</div>
2026-03-03 23:57:04 +08:00
</transition>
2026-02-17 00:12:36 +08:00
</div>
2026-03-03 23:57:04 +08:00
</TransitionGroup>
2026-02-17 00:12:36 +08:00
</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: `
2026-03-04 00:10:05 +08:00
<div>
<div class="result-header">
<div class="result-title">
📊 分析结果
</div>
<router-link class="btn btn-secondary" to="/">返回仪表盘</router-link>
2026-02-17 00:12:36 +08:00
</div>
2026-03-04 00:10:05 +08:00
<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>
2026-02-17 00:12:36 +08:00
2026-03-04 00:10:05 +08:00
<p v-if="error" class="badge badge-error">{{ error }}</p>
<div v-if="task && !loading">
<div class="card mb-6">
<div class="card-header">
<div class="card-title">
<div class="card-title-icon">📝</div>
任务信息
2026-02-17 00:12:36 +08:00
</div>
2026-03-04 00:10:05 +08:00
<span :class="['badge', task.status === 'completed' ? 'badge-success' : task.status === 'failed' ? 'badge-error' : 'badge-info']">
{{ statusText(task.status) }}
</span>
2026-02-17 00:12:36 +08:00
</div>
2026-03-04 00:10:05 +08:00
<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>
2026-02-17 00:12:36 +08:00
</div>
2026-03-04 00:10:05 +08:00
<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>
2026-02-17 00:12:36 +08:00
</div>
2026-03-04 00:10:05 +08:00
<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>
2026-02-17 00:12:36 +08:00
</div>
</div>
2026-03-04 00:10:05 +08:00
<div v-if="task.error" class="badge badge-error mt-4">
错误: {{ task.error }}
2026-03-03 23:57:04 +08:00
</div>
2026-02-17 00:12:36 +08:00
</div>
2026-03-04 00:10:05 +08:00
</div>
2026-02-17 00:12:36 +08:00
2026-03-04 00:10:05 +08:00
<div class="result-grid">
2026-03-03 23:57:04 +08:00
<div class="result-card">
2026-03-04 00:10:05 +08:00
<div class="result-card-header">
<div class="result-card-icon" style="background: var(--primary-50); color: var(--primary-600);">📐</div>
<div class="result-card-title">几何属性</div>
</div>
<div v-if="geometry" class="result-card-value">
<div class="mb-4">
<div class="text-muted text-sm">体积</div>
<div>{{ geometry.volume ? formatNumber(geometry.volume) : 'N/A' }} mm³</div>
2026-02-17 00:12:36 +08:00
</div>
2026-03-04 00:10:05 +08:00
<div class="mb-4">
<div class="text-muted text-sm">表面积</div>
<div>{{ geometry.surface_area ? formatNumber(geometry.surface_area) : 'N/A' }} mm²</div>
2026-02-17 00:12:36 +08:00
</div>
</div>
2026-03-04 00:10:05 +08:00
<div v-else class="text-muted">无几何数据</div>
2026-02-17 00:12:36 +08:00
</div>
2026-03-03 23:57:04 +08:00
<div class="result-card">
2026-03-04 00:10:05 +08:00
<div class="result-card-header">
<div class="result-card-icon" style="background: var(--accent-50); color: var(--accent-600);">📦</div>
<div class="result-card-title">边界框</div>
2026-02-17 00:12:36 +08:00
</div>
2026-03-04 00:10:05 +08:00
<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[1].toFixed(2) }} ×
{{ geometry.bounding_box.dimensions[2].toFixed(2) }} mm
</div>
</div>
2026-03-03 23:57:04 +08:00
</div>
2026-03-04 00:10:05 +08:00
<div v-else class="text-muted">无边界框数据</div>
2026-02-17 00:12:36 +08:00
</div>
2026-03-03 23:57:04 +08:00
<div class="result-card">
2026-03-04 00:10:05 +08:00
<div class="result-card-header">
<div class="result-card-icon" style="background: var(--warning-bg); color: var(--warning);">🔺</div>
<div class="result-card-title">拓扑结构</div>
2026-02-17 00:12:36 +08:00
</div>
2026-03-04 00:10:05 +08:00
<div v-if="geometry?.topology" class="result-card-value">
<div class="mb-4">
<div class="text-muted text-sm">面 / 边 / 顶点</div>
<div>{{ geometry.topology.faces || 0 }} / {{ geometry.topology.edges || 0 }} / {{ geometry.topology.vertices || 0 }}</div>
</div>
2026-03-03 23:57:04 +08:00
</div>
2026-03-04 00:10:05 +08:00
<div v-else class="text-muted">无拓扑数据</div>
2026-02-17 00:12:36 +08:00
</div>
2026-03-04 00:10:05 +08:00
</div>
2026-02-17 00:12:36 +08:00
2026-03-04 00:10:05 +08:00
<div v-if="keyInfo" class="card mt-6">
<div class="card-header">
<div class="card-title">
<div class="card-title-icon">🔧</div>
工艺参数
</div>
</div>
<div class="card-body">
<div class="grid-2">
<div class="file-item" v-if="keyInfo.metadata?.shrinkage_rate">
<div class="file-icon">📉</div>
<div class="file-info">
<div class="file-name">{{ keyInfo.metadata.shrinkage_rate }}</div>
<div class="file-meta">收缩率</div>
</div>
2026-02-17 00:12:36 +08:00
</div>
2026-03-04 00:10:05 +08:00
<div class="file-item" v-if="keyInfo.metadata?.draft_angle !== undefined">
<div class="file-icon">📐</div>
<div class="file-info">
<div class="file-name">{{ keyInfo.metadata.draft_angle }}°</div>
<div class="file-meta">拔模角</div>
</div>
2026-02-17 00:12:36 +08:00
</div>
2026-03-04 00:10:05 +08:00
<div class="file-item" v-if="keyInfo.manufacturing_info?.estimated_clamping_force">
<div class="file-icon">💪</div>
<div class="file-info">
<div class="file-name">{{ keyInfo.manufacturing_info.estimated_clamping_force }}</div>
<div class="file-meta">预估夹紧力</div>
</div>
2026-02-17 00:12:36 +08:00
</div>
</div>
</div>
2026-03-04 00:10:05 +08:00
</div>
2026-02-17 00:12:36 +08:00
</div>
</div>
`,
};
const routes = [
{ path: "/", component: DashboardView },
{ path: "/history", component: HistoryView },
{ path: "/result/:taskId", component: ResultView },
];
const router = createRouter({
history: createWebHistory(),
routes,
});
2026-03-04 00:10:05 +08:00
const app = createApp(App);
app.use(router);
app.mount("#app");