This commit is contained in:
cjw
2026-02-17 00:42:55 +08:00
parent 34afd80298
commit 06225e0d65
7 changed files with 423 additions and 1005 deletions
+183 -60
View File
@@ -1,9 +1,18 @@
// static/vue-app.js
// 使用 CDN 引入的 Vue3 + Vue Router,构建单页应用
/**
* STP模具几何分析中心 - Vue3单页应用
* 优化版本:统一前端实现,增强用户体验
*/
const { createApp, ref, computed, onMounted } = Vue;
const { createApp, ref, computed, onMounted, reactive } = Vue;
const { createRouter, createWebHistory, useRoute, useRouter } = VueRouter;
// 全局状态管理
const appState = reactive({
health: null,
loading: false,
notifications: []
});
// 通用工具函数
function formatFileSize(bytes) {
if (!bytes || bytes === 0) return "0 Bytes";
@@ -23,7 +32,7 @@ function formatNumber(num) {
function formatDateTime(dateString) {
if (!dateString) return "N/A";
try {
return new Date(dateString).toLocaleString();
return new Date(dateString).toLocaleString('zh-CN');
} catch {
return dateString;
}
@@ -39,12 +48,49 @@ function statusText(status) {
return map[status] || status || "未知";
}
// 状态样式映射
function getStatusClass(status) {
const classMap = {
pending: "status-pending",
processing: "status-processing",
completed: "status-completed",
failed: "status-failed"
};
return classMap[status] || "status-pending";
}
// 添加通知
function addNotification(message, type = 'info') {
const notification = {
id: Date.now(),
message,
type,
timestamp: new Date()
};
appState.notifications.push(notification);
// 自动移除通知
setTimeout(() => {
const index = appState.notifications.findIndex(n => n.id === notification.id);
if (index > -1) {
appState.notifications.splice(index, 1);
}
}, 5000);
}
// 错误处理
function handleApiError(error, context = '') {
console.error(`API错误 [${context}]:`, error);
const message = error.message || '请求失败,请稍后重试';
addNotification(message, 'error');
return message;
}
// 顶部导航 + 布局
const App = {
setup() {
const route = useRoute();
const router = useRouter();
const health = ref(null);
const isActive = (pathPrefix) =>
computed(() => route.path === pathPrefix || route.path.startsWith(pathPrefix));
@@ -53,30 +99,66 @@ const App = {
try {
const res = await fetch("/health", { method: "POST" });
if (res.ok) {
health.value = await res.json();
appState.health = await res.json();
}
} catch {
health.value = null;
} catch (error) {
appState.health = null;
handleApiError(error, '健康检查');
}
};
onMounted(loadHealth);
const dismissNotification = (id) => {
const index = appState.notifications.findIndex(n => n.id === id);
if (index > -1) {
appState.notifications.splice(index, 1);
}
};
return { route, router, health, isActive };
onMounted(() => {
loadHealth();
// 定时检查健康状态(每30秒)
setInterval(loadHealth, 30000);
});
return {
route,
router,
appState,
isActive,
dismissNotification,
getStatusClass,
statusText
};
},
template: `
<div class="app-shell">
<!-- 通知区域 -->
<div class="notification-container" v-if="appState.notifications.length > 0">
<div
v-for="notification in appState.notifications"
:key="notification.id"
:class="['notification', 'notification-' + notification.type]"
>
<span class="notification-message">{{ notification.message }}</span>
<button class="notification-close" @click="dismissNotification(notification.id)">×</button>
</div>
</div>
<header class="app-header">
<div class="app-title">
<span class="logo">🔧</span>
<div>
<h1>STP 模具几何分析中心</h1>
<p>上传模型 → 自动分析几何 / 型腔 / 工艺 → 聚合展示关键指标</p>
<p class="app-subtitle">专业模具几何分析,支持型腔自动生成与工艺参数计算</p>
</div>
</div>
<nav class="app-nav">
<router-link :class="['nav-link', { active: isActive('/').value }]" to="/">仪表盘</router-link>
<router-link :class="['nav-link', { active: isActive('/history').value }]" to="/history">历史记录</router-link>
<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>
@@ -86,11 +168,15 @@ const App = {
<footer class="app-footer">
<div class="footer-left">
<span>PythonOCC: <strong>{{ health?.pythonocc ? '可用' : '未知' }}</strong></span>
<span v-if="health"> | 当前任务数: {{ health.total_tasks }}</span>
<span>PythonOCC: <strong :class="appState.health?.pythonocc ? 'status-success' : 'status-error'">
{{ appState.health?.pythonocc ? '可用' : '未知' }}
</strong></span>
<span v-if="appState.health"> | 当前任务数: {{ appState.health.total_tasks }}</span>
</div>
<div class="footer-right">
<span>后端版本: 3.0.0</span>
<span>版本: 3.0.0</span>
<span> | </span>
<span>模具几何分析系统</span>
</div>
</footer>
</div>
@@ -102,53 +188,77 @@ 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 state = reactive({
selectedFile: null,
uploading: false,
error: "",
currentTask: null,
polling: false,
historySummary: null,
dragOver: false
});
const totalFiles = computed(() => historySummary.value?.total_files || 0);
const totalFiles = computed(() => state.historySummary?.total_files || 0);
const totalRecords = computed(() =>
(historySummary.value?.files || []).reduce(
(state.historySummary?.files || []).reduce(
(sum, f) => sum + (f.record_count || 0),
0
)
);
const latestFile = computed(() =>
(historySummary.value?.files || [])[0] || null
(state.historySummary?.files || [])[0] || null
);
const handleFileChange = (event) => {
const file = event.target.files[0];
if (!file) return;
validateAndSelectFile(file);
};
if (
!file.name.toLowerCase().endsWith(".stp") &&
!file.name.toLowerCase().endsWith(".step")
) {
error.value = "请选择 STP 或 STEP 格式文件";
selectedFile.value = null;
const validateAndSelectFile = (file) => {
if (!file.name.toLowerCase().endsWith(".stp") &&
!file.name.toLowerCase().endsWith(".step")) {
state.error = "请选择 STP 或 STEP 格式文件";
state.selectedFile = null;
return;
}
if (file.size > 100 * 1024 * 1024) {
error.value = "文件大小不能超过 100MB";
selectedFile.value = null;
state.error = "文件大小不能超过 100MB";
state.selectedFile = null;
return;
}
error.value = "";
selectedFile.value = file;
state.error = "";
state.selectedFile = file;
addNotification(`已选择文件: ${file.name}`, 'success');
};
const handleDragOver = (event) => {
event.preventDefault();
state.dragOver = true;
};
const handleDragLeave = (event) => {
event.preventDefault();
state.dragOver = false;
};
const handleDrop = (event) => {
event.preventDefault();
state.dragOver = false;
const files = event.dataTransfer.files;
if (files.length > 0) {
validateAndSelectFile(files[0]);
}
};
const uploadFile = async () => {
if (!selectedFile.value) return;
uploading.value = true;
error.value = "";
if (!state.selectedFile) return;
state.uploading = true;
state.error = "";
const formData = new FormData();
formData.append("file", selectedFile.value);
formData.append("file", state.selectedFile);
try {
const res = await fetch("/upload", {
@@ -159,22 +269,24 @@ const DashboardView = {
throw new Error(`上传失败: ${res.status} ${res.statusText}`);
}
const data = await res.json();
currentTask.value = {
state.currentTask = {
task_id: data.task_id,
status: "processing",
filename: data.file_info?.filename,
file_size: data.file_info?.size,
};
addNotification(`文件上传成功,开始分析...`, 'success');
startPolling(data.task_id);
} catch (e) {
error.value = e.message || "上传失败";
const errorMsg = handleApiError(e, '文件上传');
state.error = errorMsg;
} finally {
uploading.value = false;
state.uploading = false;
}
};
const startPolling = async (taskId) => {
polling.value = true;
state.polling = true;
const poll = async () => {
try {
const res = await fetch(`/status/${taskId}`, { method: "POST" });
@@ -182,21 +294,26 @@ const DashboardView = {
throw new Error("查询任务状态失败");
}
const task = await res.json();
currentTask.value = task;
state.currentTask = task;
if (task.status === "completed") {
polling.value = false;
// 跳转到结果详情页
router.push(`/result/${task.task_id}`);
state.polling = false;
addNotification(`分析完成,正在跳转到结果页面...`, 'success');
setTimeout(() => {
router.push(`/result/${task.task_id}`);
}, 1000);
} else if (task.status === "failed") {
polling.value = false;
error.value = `分析失败: ${task.error || "未知错误"}`;
state.polling = false;
const errorMsg = task.error || "未知错误";
state.error = `分析失败: ${errorMsg}`;
addNotification(`分析失败: ${errorMsg}`, 'error');
} else {
setTimeout(poll, 1000);
}
} catch (e) {
polling.value = false;
error.value = e.message || "轮询失败";
state.polling = false;
const errorMsg = handleApiError(e, '任务轮询');
state.error = errorMsg;
}
};
poll();
@@ -206,32 +323,38 @@ const DashboardView = {
try {
const res = await fetch("/api/history", { method: "POST" });
if (res.ok) {
historySummary.value = await res.json();
state.historySummary = await res.json();
}
} catch {
historySummary.value = null;
} catch (e) {
state.historySummary = null;
handleApiError(e, '加载历史记录');
}
};
const clearFile = () => {
state.selectedFile = null;
state.error = "";
};
onMounted(() => {
loadHistorySummary();
});
return {
selectedFile,
uploading,
error,
currentTask,
polling,
historySummary,
state,
totalFiles,
totalRecords,
latestFile,
handleFileChange,
handleDragOver,
handleDragLeave,
handleDrop,
uploadFile,
clearFile,
formatFileSize,
statusText,
formatDateTime,
getStatusClass
};
},
template: `