deving
This commit is contained in:
+183
-60
@@ -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: `
|
||||
|
||||
Reference in New Issue
Block a user