deving
This commit is contained in:
@@ -405,6 +405,13 @@ async def process_file_core(
|
||||
mesh_json=mesh_json,
|
||||
quality="medium",
|
||||
)
|
||||
# 将简要网格摘要写入内存任务,便于前端展示汇总信息
|
||||
tasks[task_id]["mesh_summary"] = {
|
||||
"vertex_count": len(vertices),
|
||||
"face_count": len(faces),
|
||||
"point_count": pointcloud.get("count"),
|
||||
"quality": "medium",
|
||||
}
|
||||
except Exception as mesh_err:
|
||||
# 网格失败不影响整体流程,只记录日志
|
||||
logger.warning(f"网格生成或保存失败,不影响主流程: {mesh_err}")
|
||||
|
||||
@@ -0,0 +1,745 @@
|
||||
// static/vue-app.js
|
||||
// 使用 CDN 引入的 Vue3 + Vue Router,构建单页应用
|
||||
|
||||
const { createApp, ref, computed, onMounted } = Vue;
|
||||
const { createRouter, createWebHistory, useRoute, useRouter } = VueRouter;
|
||||
|
||||
// 通用工具函数
|
||||
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();
|
||||
} catch {
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
|
||||
function statusText(status) {
|
||||
const map = {
|
||||
pending: "排队中",
|
||||
processing: "处理中",
|
||||
completed: "已完成",
|
||||
failed: "失败",
|
||||
};
|
||||
return map[status] || status || "未知";
|
||||
}
|
||||
|
||||
// 顶部导航 + 布局
|
||||
const App = {
|
||||
setup() {
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const health = ref(null);
|
||||
|
||||
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) {
|
||||
health.value = await res.json();
|
||||
}
|
||||
} catch {
|
||||
health.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(loadHealth);
|
||||
|
||||
return { route, router, health, isActive };
|
||||
},
|
||||
template: `
|
||||
<div class="app-shell">
|
||||
<header class="app-header">
|
||||
<div class="app-title">
|
||||
<span class="logo">🔧</span>
|
||||
<div>
|
||||
<h1>STP 模具几何分析中心</h1>
|
||||
<p>上传模型 → 自动分析几何 / 型腔 / 工艺 → 聚合展示关键指标</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>{{ health?.pythonocc ? '可用' : '未知' }}</strong></span>
|
||||
<span v-if="health"> | 当前任务数: {{ health.total_tasks }}</span>
|
||||
</div>
|
||||
<div class="footer-right">
|
||||
<span>后端版本: 3.0.0</span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
|
||||
// 仪表盘视图:上传 + 总览
|
||||
const DashboardView = {
|
||||
setup() {
|
||||
const router = useRouter();
|
||||
|
||||
const selectedFile = ref(null);
|
||||
const uploading = ref(false);
|
||||
const error = ref("");
|
||||
const currentTask = ref(null);
|
||||
const polling = ref(false);
|
||||
const historySummary = ref(null);
|
||||
|
||||
const totalFiles = computed(() => historySummary.value?.total_files || 0);
|
||||
const totalRecords = computed(() =>
|
||||
(historySummary.value?.files || []).reduce(
|
||||
(sum, f) => sum + (f.record_count || 0),
|
||||
0
|
||||
)
|
||||
);
|
||||
const latestFile = computed(() =>
|
||||
(historySummary.value?.files || [])[0] || null
|
||||
);
|
||||
|
||||
const handleFileChange = (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
if (
|
||||
!file.name.toLowerCase().endsWith(".stp") &&
|
||||
!file.name.toLowerCase().endsWith(".step")
|
||||
) {
|
||||
error.value = "请选择 STP 或 STEP 格式文件";
|
||||
selectedFile.value = null;
|
||||
return;
|
||||
}
|
||||
if (file.size > 100 * 1024 * 1024) {
|
||||
error.value = "文件大小不能超过 100MB";
|
||||
selectedFile.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
error.value = "";
|
||||
selectedFile.value = file;
|
||||
};
|
||||
|
||||
const uploadFile = async () => {
|
||||
if (!selectedFile.value) return;
|
||||
uploading.value = true;
|
||||
error.value = "";
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", selectedFile.value);
|
||||
|
||||
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();
|
||||
currentTask.value = {
|
||||
task_id: data.task_id,
|
||||
status: "processing",
|
||||
filename: data.file_info?.filename,
|
||||
file_size: data.file_info?.size,
|
||||
};
|
||||
startPolling(data.task_id);
|
||||
} catch (e) {
|
||||
error.value = e.message || "上传失败";
|
||||
} finally {
|
||||
uploading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const startPolling = async (taskId) => {
|
||||
polling.value = true;
|
||||
const poll = async () => {
|
||||
try {
|
||||
const res = await fetch(`/status/${taskId}`, { method: "POST" });
|
||||
if (!res.ok) {
|
||||
throw new Error("查询任务状态失败");
|
||||
}
|
||||
const task = await res.json();
|
||||
currentTask.value = task;
|
||||
|
||||
if (task.status === "completed") {
|
||||
polling.value = false;
|
||||
// 跳转到结果详情页
|
||||
router.push(`/result/${task.task_id}`);
|
||||
} else if (task.status === "failed") {
|
||||
polling.value = false;
|
||||
error.value = `分析失败: ${task.error || "未知错误"}`;
|
||||
} else {
|
||||
setTimeout(poll, 1000);
|
||||
}
|
||||
} catch (e) {
|
||||
polling.value = false;
|
||||
error.value = e.message || "轮询失败";
|
||||
}
|
||||
};
|
||||
poll();
|
||||
};
|
||||
|
||||
const loadHistorySummary = async () => {
|
||||
try {
|
||||
const res = await fetch("/api/history", { method: "POST" });
|
||||
if (res.ok) {
|
||||
historySummary.value = await res.json();
|
||||
}
|
||||
} catch {
|
||||
historySummary.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadHistorySummary();
|
||||
});
|
||||
|
||||
return {
|
||||
selectedFile,
|
||||
uploading,
|
||||
error,
|
||||
currentTask,
|
||||
polling,
|
||||
historySummary,
|
||||
totalFiles,
|
||||
totalRecords,
|
||||
latestFile,
|
||||
handleFileChange,
|
||||
uploadFile,
|
||||
formatFileSize,
|
||||
statusText,
|
||||
formatDateTime,
|
||||
};
|
||||
},
|
||||
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");
|
||||
|
||||
+6
-195
@@ -4,204 +4,15 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>历史记录 - STP文件几何分析工具</title>
|
||||
<title>历史记录 - STP 模具几何分析中心</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<style>
|
||||
.history-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header-nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.nav-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
display: grid;
|
||||
gap: 15px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.file-item.expanded {
|
||||
border-color: #4CAF50;
|
||||
background: #f9fff9;
|
||||
}
|
||||
|
||||
.file-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.file-meta {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.expand-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.record-list {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.file-item.expanded .record-list {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.record-item {
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.record-item:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.record-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.record-time {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.record-status {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background: #e8f5e8;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.status-processing {
|
||||
background: #fff3e0;
|
||||
color: #f57c00;
|
||||
}
|
||||
|
||||
.status-failed {
|
||||
background: #ffebee;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.record-details {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 10px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.no-records {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 40px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #4CAF50;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="history-container">
|
||||
<div class="header-nav">
|
||||
<div>
|
||||
<h1>📊 历史记录</h1>
|
||||
<p>查看所有上传的STP文件分析记录</p>
|
||||
</div>
|
||||
<div class="nav-buttons">
|
||||
<a href="/" class="upload-btn" style="text-decoration: none; display: inline-block; text-align: center;">
|
||||
📁 上传新文件
|
||||
</a>
|
||||
<a href="/" class="upload-btn" style="text-decoration: none; display: inline-block; text-align: center;">
|
||||
🔄 返回主页
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="app"></div>
|
||||
|
||||
<div class="file-list" id="fileList">
|
||||
<div class="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>正在加载历史记录...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/history.js"></script>
|
||||
<!-- Vue3 + Vue Router via CDN -->
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||
<script src="https://unpkg.com/vue-router@4/dist/vue-router.global.prod.js"></script>
|
||||
<script src="/static/vue-app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+6
-118
@@ -4,127 +4,15 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>STP文件几何分析工具</title>
|
||||
<title>STP 模具几何分析中心</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🔧 STP文件几何分析工具</h1>
|
||||
<p>上传STP/STEP文件,自动分析几何属性和拓扑结构</p>
|
||||
</div>
|
||||
<div id="app"></div>
|
||||
|
||||
<!-- 上传区域 - 默认显示 -->
|
||||
<div class="upload-section" id="uploadSection">
|
||||
<h2>选择STP文件</h2>
|
||||
<p>支持 .stp 和 .step 格式文件</p>
|
||||
|
||||
<div class="upload-area" id="uploadArea">
|
||||
<div class="upload-icon">📁</div>
|
||||
<h3>拖放文件到此处或点击选择</h3>
|
||||
<p>最大文件大小: 100MB</p>
|
||||
<input type="file" id="fileInput" class="file-input" accept=".stp,.step">
|
||||
<button class="upload-btn" onclick="document.getElementById('fileInput').click()">
|
||||
选择文件
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button class="upload-btn" id="uploadBtn" onclick="uploadFile()" disabled>
|
||||
开始分析
|
||||
</button>
|
||||
|
||||
<div class="loading" id="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>正在分析文件,请稍候...</p>
|
||||
</div>
|
||||
|
||||
<div class="error-message" id="errorMessage"></div>
|
||||
</div>
|
||||
|
||||
<!-- 结果区域 - 默认隐藏 -->
|
||||
<div class="results-section" id="resultsSection" style="display: none;">
|
||||
<h2>📊 分析结果</h2>
|
||||
|
||||
<!-- 任务基本信息 -->
|
||||
<div class="result-card">
|
||||
<h3>📝 任务信息</h3>
|
||||
<div class="task-info" id="taskInfo"></div>
|
||||
</div>
|
||||
|
||||
<!-- 几何属性 -->
|
||||
<div class="result-card">
|
||||
<h3>📐 几何属性</h3>
|
||||
<div class="geometry-data" id="geometryData"></div>
|
||||
</div>
|
||||
|
||||
<!-- 边界框信息 -->
|
||||
<div class="result-card">
|
||||
<h3>📦 边界框信息</h3>
|
||||
<div class="bounding-box-data" id="boundingBoxData"></div>
|
||||
</div>
|
||||
|
||||
<!-- 拓扑结构 -->
|
||||
<div class="result-card">
|
||||
<h3>🔺 拓扑结构</h3>
|
||||
<div class="topology-data" id="topologyData"></div>
|
||||
</div>
|
||||
|
||||
<div class="result-card">
|
||||
<h3>🔧 模具型腔设计</h3>
|
||||
<div class="cavity-data" id="cavityData"></div>
|
||||
</div>
|
||||
|
||||
<div class="result-card">
|
||||
<h3>📐 关键工艺参数</h3>
|
||||
<div class="key-info-data" id="keyInfoData"></div>
|
||||
</div>
|
||||
|
||||
<!-- 检测到的特征 -->
|
||||
<div class="result-card">
|
||||
<h3>🎯 检测到的特征</h3>
|
||||
<div class="features-data" id="featuresData"></div>
|
||||
</div>
|
||||
|
||||
<!-- 设计建议 -->
|
||||
<div class="result-card">
|
||||
<h3>💡 设计建议</h3>
|
||||
<div class="recommendations-data" id="recommendationsData"></div>
|
||||
</div>
|
||||
|
||||
<!-- 质量指标 -->
|
||||
<div class="result-card">
|
||||
<h3>📈 质量指标</h3>
|
||||
<div class="metrics-data" id="metricsData"></div>
|
||||
</div>
|
||||
|
||||
<!-- 分析信息 -->
|
||||
<div class="result-card">
|
||||
<h3>🔍 分析信息</h3>
|
||||
<div class="analysis-info" id="analysisInfo"></div>
|
||||
</div>
|
||||
|
||||
<!-- 返回按钮 -->
|
||||
<div style="text-align: center; margin-top: 30px;">
|
||||
<button class="upload-btn" onclick="showUploadSection()">
|
||||
🔄 分析新文件
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="system-info">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<p>PythonOCC 可用: {{ pythonocc_available }} | 服务版本: 3.0.0</p>
|
||||
<div>
|
||||
<a href="/history" class="upload-btn" style="margin-left: 10px; text-decoration: none; display: inline-block; text-align: center;">
|
||||
📋 查看历史记录
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/script.js"></script>
|
||||
<!-- Vue3 + Vue Router via CDN -->
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||
<script src="https://unpkg.com/vue-router@4/dist/vue-router.global.prod.js"></script>
|
||||
<script src="/static/vue-app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+6
-425
@@ -4,434 +4,15 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>分析结果 - STP文件几何分析工具</title>
|
||||
<title>分析结果 - STP 模具几何分析中心</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<style>
|
||||
.result-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header-nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.nav-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.task-info-card {
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.task-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background: #e8f5e8;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.status-processing {
|
||||
background: #fff3e0;
|
||||
color: #f57c00;
|
||||
}
|
||||
|
||||
.status-failed {
|
||||
background: #ffebee;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.results-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.result-section {
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.data-grid {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.data-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.data-label {
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.data-value {
|
||||
color: #333;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #ffebee;
|
||||
border: 1px solid #ffcdd2;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
color: #c62828;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 40px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="result-container">
|
||||
<div class="header-nav">
|
||||
<div>
|
||||
<h1>📊 分析结果详情</h1>
|
||||
<p>查看STP文件的详细分析结果</p>
|
||||
</div>
|
||||
<div class="nav-buttons">
|
||||
<button class="upload-btn" onclick="location.href='/history'">
|
||||
📋 返回历史记录
|
||||
</button>
|
||||
<button class="upload-btn" onclick="location.href='/upload'">
|
||||
📁 上传新文件
|
||||
</button>
|
||||
<button class="upload-btn" onclick="location.href='/'">
|
||||
🏠 返回主页
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="app"></div>
|
||||
|
||||
<div id="taskInfo" class="task-info-card">
|
||||
<div class="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>正在加载任务信息...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="results-grid" id="resultsGrid">
|
||||
<!-- 结果内容将通过JavaScript动态加载 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 从URL获取任务ID
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
const taskId = pathParts[pathParts.length - 1];
|
||||
|
||||
// 页面加载完成后获取任务数据
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadTaskData(taskId);
|
||||
});
|
||||
|
||||
async function loadTaskData(taskId) {
|
||||
try {
|
||||
const response = await fetch(`/status/${taskId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('获取任务数据失败');
|
||||
}
|
||||
|
||||
const task = await response.json();
|
||||
displayTaskInfo(task);
|
||||
displayResults(task);
|
||||
|
||||
} catch (error) {
|
||||
document.getElementById('taskInfo').innerHTML = `
|
||||
<div class="error-message">
|
||||
<h3>❌ 加载失败</h3>
|
||||
<p>${error.message}</p>
|
||||
<button class="upload-btn" onclick="loadTaskData('${taskId}')">
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function displayTaskInfo(task) {
|
||||
const taskInfo = document.getElementById('taskInfo');
|
||||
|
||||
taskInfo.innerHTML = `
|
||||
<h2>📝 任务信息</h2>
|
||||
<div class="task-info-grid">
|
||||
<div class="info-item">
|
||||
<div class="info-label">任务ID</div>
|
||||
<div class="info-value">${task.task_id || 'N/A'}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">文件名</div>
|
||||
<div class="info-value">${task.filename || 'N/A'}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">文件大小</div>
|
||||
<div class="info-value">${task.file_size ? formatFileSize(task.file_size) : 'N/A'}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">状态</div>
|
||||
<div class="info-value">
|
||||
<span class="status-badge status-${task.status}">${getStatusText(task.status)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">上传时间</div>
|
||||
<div class="info-value">${task.upload_time ? new Date(task.upload_time).toLocaleString() : 'N/A'}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">完成时间</div>
|
||||
<div class="info-value">${task.completed_at ? new Date(task.completed_at).toLocaleString() : 'N/A'}</div>
|
||||
</div>
|
||||
</div>
|
||||
${task.error ? `
|
||||
<div class="error-message" style="margin-top: 15px;">
|
||||
<strong>错误信息:</strong> ${task.error}
|
||||
</div>
|
||||
` : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
function displayResults(task) {
|
||||
const resultsGrid = document.getElementById('resultsGrid');
|
||||
|
||||
if (task.status !== 'completed') {
|
||||
resultsGrid.innerHTML = `
|
||||
<div class="no-data">
|
||||
<div style="font-size: 48px; margin-bottom: 20px;">⏳</div>
|
||||
<h3>任务尚未完成</h3>
|
||||
<p>当前状态: ${getStatusText(task.status)}</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
resultsGrid.innerHTML = `
|
||||
<div class="result-section">
|
||||
<div class="section-title">📐 几何属性</div>
|
||||
<div class="data-grid">
|
||||
${displayGeometryData(task.geometry_data)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result-section">
|
||||
<div class="section-title">🔧 关键工艺参数</div>
|
||||
<div class="data-grid">
|
||||
${displayKeyInfo(task.key_info)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result-section">
|
||||
<div class="section-title">📦 边界框信息</div>
|
||||
<div class="data-grid">
|
||||
${displayBoundingBoxData(task.geometry_data)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result-section">
|
||||
<div class="section-title">🔺 拓扑结构</div>
|
||||
<div class="data-grid">
|
||||
${displayTopologyData(task.geometry_data)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function displayGeometryData(geometryData) {
|
||||
if (!geometryData) return '<div class="no-data">无几何数据</div>';
|
||||
|
||||
return `
|
||||
<div class="data-item">
|
||||
<div class="data-label">体积</div>
|
||||
<div class="data-value">${geometryData.volume ? formatNumber(geometryData.volume) + ' mm³' : 'N/A'}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">表面积</div>
|
||||
<div class="data-value">${geometryData.surface_area ? formatNumber(geometryData.surface_area) + ' mm²' : 'N/A'}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">体积表面积比</div>
|
||||
<div class="data-value">${geometryData.volume && geometryData.surface_area ? (geometryData.volume / geometryData.surface_area).toFixed(4) : 'N/A'}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function displayKeyInfo(keyInfo) {
|
||||
if (!keyInfo) return '<div class="no-data">无关键信息数据</div>';
|
||||
|
||||
const metadata = keyInfo.metadata || {};
|
||||
const manufacturingInfo = keyInfo.manufacturing_info || {};
|
||||
const moldCavities = keyInfo.mold_cavities || {};
|
||||
const cavityKeyInfo = moldCavities.cavity_key_info || {};
|
||||
const geoChars = cavityKeyInfo.geometric_characteristics || {};
|
||||
|
||||
return `
|
||||
<div class="data-item">
|
||||
<div class="data-label">收缩率</div>
|
||||
<div class="data-value">${metadata.shrinkage_rate !== undefined ? metadata.shrinkage_rate : 'N/A'}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">拔模角</div>
|
||||
<div class="data-value">${metadata.draft_angle !== undefined ? metadata.draft_angle + '°' : 'N/A'}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">分型线长度</div>
|
||||
<div class="data-value">${manufacturingInfo.parting_line_length || 'N/A'}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">产品体积</div>
|
||||
<div class="data-value">${geoChars.product_volume || 'N/A'}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">产品重量</div>
|
||||
<div class="data-value">${geoChars.product_weight || 'N/A'}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">壁厚范围</div>
|
||||
<div class="data-value">${geoChars.wall_thickness_range || 'N/A'}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">型腔材料</div>
|
||||
<div class="data-value">${manufacturingInfo.mold_material || 'N/A'}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">预估周期</div>
|
||||
<div class="data-value">${manufacturingInfo.estimated_cycle_time || 'N/A'}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function displayBoundingBoxData(geometryData) {
|
||||
if (!geometryData || !geometryData.bounding_box) return '<div class="no-data">无边界框数据</div>';
|
||||
|
||||
const bbox = geometryData.bounding_box;
|
||||
return `
|
||||
<div class="data-item">
|
||||
<div class="data-label">最小坐标</div>
|
||||
<div class="data-value">X: ${bbox.min[0].toFixed(2)}<br>Y: ${bbox.min[1].toFixed(2)}<br>Z: ${bbox.min[2].toFixed(2)}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">最大坐标</div>
|
||||
<div class="data-value">X: ${bbox.max[0].toFixed(2)}<br>Y: ${bbox.max[1].toFixed(2)}<br>Z: ${bbox.max[2].toFixed(2)}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">尺寸</div>
|
||||
<div class="data-value">${bbox.dimensions[0].toFixed(2)} × ${bbox.dimensions[1].toFixed(2)} × ${bbox.dimensions[2].toFixed(2)} mm</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function displayTopologyData(geometryData) {
|
||||
if (!geometryData || !geometryData.topology) return '<div class="no-data">无拓扑数据</div>';
|
||||
|
||||
const topo = geometryData.topology;
|
||||
return `
|
||||
<div class="data-item">
|
||||
<div class="data-label">面数</div>
|
||||
<div class="data-value">${topo.faces || 0}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">边数</div>
|
||||
<div class="data-value">${topo.edges || 0}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">顶点数</div>
|
||||
<div class="data-value">${topo.vertices || 0}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
function getStatusText(status) {
|
||||
const statusMap = {
|
||||
'processing': '处理中',
|
||||
'completed': '已完成',
|
||||
'failed': '失败'
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
}
|
||||
|
||||
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) return 'N/A';
|
||||
if (num >= 1000000) {
|
||||
return (num / 1000000).toFixed(2) + 'M';
|
||||
} else if (num >= 1000) {
|
||||
return (num / 1000).toFixed(2) + 'K';
|
||||
} else {
|
||||
return num.toFixed(2);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<!-- Vue3 + Vue Router via CDN -->
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||
<script src="https://unpkg.com/vue-router@4/dist/vue-router.global.prod.js"></script>
|
||||
<script src="/static/vue-app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user