Files
geMoldInsight/static/vue-app.js
T
2026-03-07 03:04:15 +08:00

1685 lines
58 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Gemold - 模具制造管理系统
* 版本: 4.0.0
*/
const { createApp, ref, computed, onMounted, reactive, watch, nextTick } = Vue;
const { createRouter, createWebHistory, useRoute, useRouter } = VueRouter;
const appState = reactive({
user: null,
token: null,
loading: false,
notifications: [],
initialized: false
});
function formatFileSize(bytes) {
if (!bytes || bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "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('zh-CN');
} catch {
return dateString;
}
}
function formatDate(dateString) {
if (!dateString) return "N/A";
try {
return new Date(dateString).toLocaleDateString('zh-CN');
} catch {
return dateString;
}
}
function formatCurrency(amount) {
if (amount === null || amount === undefined) return "¥0.00";
return "¥" + Number(amount).toFixed(2);
}
let notificationId = 0;
function addNotification(message, type = 'info') {
const id = ++notificationId;
const notification = { id, message, type, timestamp: new Date(), visible: true };
appState.notifications.push(notification);
setTimeout(() => {
const index = appState.notifications.findIndex(n => n.id === id);
if (index > -1) {
appState.notifications[index].visible = false;
setTimeout(() => {
const idx = appState.notifications.findIndex(n => n.id === id);
if (idx > -1) appState.notifications.splice(idx, 1);
}, 300);
}
}, 5000);
}
function handleApiError(error, context = '') {
console.error(`API错误 [${context}]:`, error);
const message = error.message || '请求失败,请稍后重试';
addNotification(message, 'error');
return message;
}
async function apiRequest(url, options = {}) {
const headers = {
'Content-Type': 'application/json',
...options.headers
};
if (appState.token) {
headers['Authorization'] = `Bearer ${appState.token}`;
}
const response = await fetch(url, { ...options, headers });
if (response.status === 401) {
appState.user = null;
appState.token = null;
localStorage.removeItem('token');
localStorage.removeItem('user');
throw new Error('登录已过期,请重新登录');
}
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: '请求失败' }));
throw new Error(error.detail || '请求失败');
}
return response.json();
}
function saveAuth(token, user) {
appState.token = token;
appState.user = user;
localStorage.setItem('token', token);
localStorage.setItem('user', JSON.stringify(user));
}
function clearAuth() {
appState.token = null;
appState.user = null;
localStorage.removeItem('token');
localStorage.removeItem('user');
}
function initAuth() {
const token = localStorage.getItem('token');
const userStr = localStorage.getItem('user');
if (token && userStr) {
try {
appState.token = token;
appState.user = JSON.parse(userStr);
} catch {
clearAuth();
}
}
appState.initialized = true;
}
const App = {
setup() {
const route = useRoute();
const router = useRouter();
const menuItems = computed(() => {
const items = [
{ path: '/', label: '首页', icon: '⌂' },
{ path: '/moldinsight', label: 'MoldInsight', icon: '◈' },
{ path: '/inventory', label: '进销存', icon: '⊞' }
];
if (appState.user?.is_superuser) {
items.push({ path: '/users', label: '用户管理', icon: '👤' });
}
return items;
});
const isActive = (path) => {
if (path === '/') return route.path === '/';
return route.path.startsWith(path);
};
const handleLogout = async () => {
try {
await apiRequest('/api/auth/logout', { method: 'POST' });
} catch {}
clearAuth();
addNotification('已退出登录', 'success');
router.push('/login');
};
onMounted(() => {
initAuth();
});
const getPriorityText = (priority) => {
const priorityMap = {
'critical': '紧急',
'high': '高',
'medium': '中',
'low': '低'
};
return priorityMap[priority] || priority;
};
return {
route,
router,
appState,
menuItems,
isActive,
handleLogout,
getPriorityText,
dismissNotification: (id) => {
const index = appState.notifications.findIndex(n => n.id === id);
if (index > -1) {
appState.notifications[index].visible = false;
setTimeout(() => {
const idx = appState.notifications.findIndex(n => n.id === id);
if (idx > -1) appState.notifications.splice(idx, 1);
}, 300);
}
}
};
},
template: `
<div class="app-container">
<div class="notification-container" v-if="appState.notifications.length > 0">
<TransitionGroup name="notification">
<div
v-for="notification in appState.notifications"
:key="notification.id"
:class="['notification', 'notification-' + notification.type]"
>
<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>
<button class="notification-close" @click="dismissNotification(notification.id)">×</button>
</div>
</TransitionGroup>
</div>
<header class="app-header">
<div class="header-content">
<div class="logo" @click="router.push('/')">
<div class="logo-icon">G</div>
<div>
<div class="logo-text">Gemold</div>
<div class="logo-subtitle">模具制造管理系统</div>
</div>
</div>
<nav class="nav-menu" v-if="appState.user">
<router-link
v-for="item in menuItems"
:key="item.path"
:to="item.path"
:class="['nav-item', { active: isActive(item.path) }]"
>
{{ item.label }}
</router-link>
</nav>
<div class="user-section">
<template v-if="appState.user">
<div class="user-info">
<div class="user-avatar">{{ (appState.user.full_name || appState.user.username).charAt(0).toUpperCase() }}</div>
<span class="user-name">{{ appState.user.full_name || appState.user.username }}</span>
</div>
<button class="btn btn-secondary btn-sm" @click="handleLogout">退出</button>
</template>
<template v-else>
<router-link to="/login" class="btn btn-primary btn-sm">登录</router-link>
</template>
</div>
</div>
</header>
<main class="main-content">
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</main>
</div>
`,
};
const LoginView = {
setup() {
const router = useRouter();
const state = reactive({
username: '',
password: '',
loading: false,
error: ''
});
onMounted(() => {
if (appState.user) {
router.push('/');
}
});
const handleSubmit = async () => {
if (!state.username || !state.password) {
state.error = '请填写用户名和密码';
return;
}
state.loading = true;
state.error = '';
try {
const res = await fetch('/api/auth/login/json', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: state.username,
password: state.password
})
});
if (!res.ok) {
const error = await res.json();
throw new Error(error.detail || '登录失败');
}
const data = await res.json();
saveAuth(data.access_token, data.user);
addNotification('登录成功', 'success');
router.push('/');
} catch (e) {
state.error = e.message;
addNotification(e.message, 'error');
} finally {
state.loading = false;
}
};
return { state, handleSubmit };
},
template: `
<div class="login-container">
<div class="login-card">
<div class="login-header">
<div class="login-logo">G</div>
<h1 class="login-title">欢迎回来</h1>
<p class="login-subtitle">登录到 Gemold 系统</p>
</div>
<form @submit.prevent="handleSubmit" class="login-form">
<div class="form-group">
<label class="form-label">用户名</label>
<input
v-model="state.username"
type="text"
class="form-input"
placeholder="请输入用户名"
autocomplete="username"
/>
</div>
<div class="form-group">
<label class="form-label">密码</label>
<input
v-model="state.password"
type="password"
class="form-input"
placeholder="请输入密码"
autocomplete="current-password"
/>
</div>
<div v-if="state.error" class="form-error">{{ state.error }}</div>
<button type="submit" class="btn btn-primary btn-lg w-full" :disabled="state.loading">
{{ state.loading ? '登录中...' : '登录' }}
</button>
</form>
<div class="login-footer" style="margin-top: var(--space-6); text-align: center;">
<p style="font-size: var(--text-sm); color: var(--text-tertiary);">如需开通账号,请联系管理员</p>
</div>
</div>
</div>
`
};
const HomeView = {
setup() {
const router = useRouter();
const state = reactive({
stats: null,
loading: true
});
const loadStats = async () => {
try {
const [inventoryStats, health] = await Promise.all([
apiRequest('/api/inventory/dashboard').catch(() => null),
apiRequest('/health').catch(() => null)
]);
state.stats = { inventory: inventoryStats, health };
} catch (e) {
handleApiError(e, '加载统计数据');
} finally {
state.loading = false;
}
};
onMounted(() => {
if (!appState.user) {
router.push('/login');
return;
}
loadStats();
});
return { state, formatNumber, formatCurrency, appState };
},
template: `
<div class="page-container">
<div class="page-header">
<h1>欢迎回来,{{ appState.user?.full_name || appState.user?.username }}</h1>
<p>系统概览</p>
</div>
<div v-if="state.loading" class="loading-state">
<div class="spinner"></div>
<span>加载中...</span>
</div>
<div v-else class="dashboard-grid">
<div class="stat-card" @click="$router.push('/inventory')">
<div class="stat-icon">📦</div>
<div class="stat-content">
<div class="stat-value">{{ state.stats?.inventory?.product_count || 0 }}</div>
<div class="stat-label">产品数量</div>
</div>
</div>
<div class="stat-card" @click="$router.push('/inventory')">
<div class="stat-icon">📊</div>
<div class="stat-content">
<div class="stat-value">{{ state.stats?.inventory?.total_stock || 0 }}</div>
<div class="stat-label">库存总量</div>
</div>
</div>
<div class="stat-card" @click="$router.push('/inventory')">
<div class="stat-icon">💰</div>
<div class="stat-content">
<div class="stat-value">{{ formatCurrency(state.stats?.inventory?.total_value || 0) }}</div>
<div class="stat-label">库存价值</div>
</div>
</div>
<div class="stat-card" @click="$router.push('/moldinsight')">
<div class="stat-icon">◈</div>
<div class="stat-content">
<div class="stat-value">{{ state.stats?.health?.total_tasks || 0 }}</div>
<div class="stat-label">分析任务</div>
</div>
</div>
<div class="stat-card" @click="$router.push('/inventory')">
<div class="stat-icon">🏭</div>
<div class="stat-content">
<div class="stat-value">{{ state.stats?.inventory?.supplier_count || 0 }}</div>
<div class="stat-label">供应商</div>
</div>
</div>
<div class="stat-card" @click="$router.push('/inventory')">
<div class="stat-icon">👥</div>
<div class="stat-content">
<div class="stat-value">{{ state.stats?.inventory?.customer_count || 0 }}</div>
<div class="stat-label">客户</div>
</div>
</div>
</div>
<div v-if="state.stats?.inventory?.low_stock_products?.length" class="section">
<h2 class="section-title">低库存预警</h2>
<div class="table-container">
<table class="data-table">
<thead>
<tr>
<th>SKU</th>
<th>产品名称</th>
<th>当前库存</th>
<th>最低库存</th>
</tr>
</thead>
<tbody>
<tr v-for="item in state.stats.inventory.low_stock_products" :key="item.id">
<td>{{ item.sku }}</td>
<td>{{ item.name }}</td>
<td class="text-warning">{{ item.quantity }}</td>
<td>{{ item.min_stock }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="quick-actions">
<h2 class="section-title">快速操作</h2>
<div class="action-grid">
<button class="action-card" @click="$router.push('/moldinsight')">
<span class="action-icon">◈</span>
<span class="action-label">模具分析</span>
</button>
<button class="action-card" @click="$router.push('/inventory')">
<span class="action-icon">📦</span>
<span class="action-label">库存管理</span>
</button>
<button class="action-card" @click="$router.push('/inventory')">
<span class="action-icon">📥</span>
<span class="action-label">采购入库</span>
</button>
<button class="action-card" @click="$router.push('/inventory')">
<span class="action-icon">📤</span>
<span class="action-label">销售出库</span>
</button>
</div>
</div>
</div>
`
};
const UsersView = {
setup() {
const router = useRouter();
const state = reactive({
users: [],
roles: [],
loading: true,
showUserModal: false,
editingUser: null,
userForm: {
username: '',
email: '',
password: '',
full_name: '',
role_ids: []
}
});
const loadUsers = async () => {
try {
state.users = await apiRequest('/api/auth/users');
} catch (e) {
handleApiError(e, '加载用户列表');
} finally {
state.loading = false;
}
};
const loadRoles = async () => {
try {
state.roles = await apiRequest('/api/auth/roles');
} catch (e) {
handleApiError(e, '加载角色列表');
}
};
const openUserModal = (user = null) => {
state.editingUser = user;
if (user) {
state.userForm = {
username: user.username,
email: user.email,
password: '',
full_name: user.full_name || '',
role_ids: user.roles.map(r => {
const role = state.roles.find(role => role.code === r);
return role ? role.id : null;
}).filter(id => id !== null)
};
} else {
state.userForm = {
username: '',
email: '',
password: '',
full_name: '',
role_ids: []
};
}
state.showUserModal = true;
};
const saveUser = async () => {
if (!state.userForm.username || !state.userForm.email) {
addNotification('请填写用户名和邮箱', 'error');
return;
}
if (!state.editingUser && !state.userForm.password) {
addNotification('请填写密码', 'error');
return;
}
try {
if (state.editingUser) {
await apiRequest(`/api/auth/users/${state.editingUser.id}`, {
method: 'PUT',
body: JSON.stringify({
email: state.userForm.email,
full_name: state.userForm.full_name || null,
role_ids: state.userForm.role_ids
})
});
addNotification('用户更新成功', 'success');
} else {
await apiRequest('/api/auth/users', {
method: 'POST',
body: JSON.stringify(state.userForm)
});
addNotification('用户创建成功', 'success');
}
state.showUserModal = false;
loadUsers();
} catch (e) {
handleApiError(e, '保存用户');
}
};
const deleteUser = async (user) => {
if (!confirm(`确定要删除用户 ${user.username} 吗?`)) return;
try {
await apiRequest(`/api/auth/users/${user.id}`, { method: 'DELETE' });
addNotification('用户已删除', 'success');
loadUsers();
} catch (e) {
handleApiError(e, '删除用户');
}
};
const resetPassword = async (user) => {
const newPassword = prompt(`请输入 ${user.username} 的新密码:`);
if (!newPassword || newPassword.length < 6) {
addNotification('密码长度至少6位', 'error');
return;
}
try {
await apiRequest(`/api/auth/users/${user.id}/reset-password`, {
method: 'PUT',
body: JSON.stringify(newPassword)
});
addNotification('密码已重置', 'success');
} catch (e) {
handleApiError(e, '重置密码');
}
};
onMounted(async () => {
if (!appState.user?.is_superuser) {
router.push('/');
return;
}
await loadRoles();
loadUsers();
});
return { state, appState, openUserModal, saveUser, deleteUser, resetPassword, formatDateTime };
},
template: `
<div class="page-container">
<div class="page-header">
<div>
<h1>用户管理</h1>
<p>管理系统用户和权限</p>
</div>
<button class="btn-primary" @click="openUserModal()">+ 添加用户</button>
</div>
<div v-if="state.loading" class="loading-state">
<div class="spinner"></div>
<span>加载中...</span>
</div>
<div v-else class="table-container">
<table class="data-table">
<thead>
<tr>
<th>用户名</th>
<th>邮箱</th>
<th>姓名</th>
<th>状态</th>
<th>角色</th>
<th>注册时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="user in state.users" :key="user.id">
<td>{{ user.username }}</td>
<td>{{ user.email }}</td>
<td>{{ user.full_name || '-' }}</td>
<td>
<span :class="['badge', user.is_active ? 'badge-success' : 'badge-error']">
{{ user.is_active ? '正常' : '禁用' }}
</span>
</td>
<td>
<span v-for="role in user.roles" :key="role" class="badge badge-info" style="margin-right: 4px;">
{{ role }}
</span>
</td>
<td>{{ formatDateTime(user.created_at) }}</td>
<td>
<div class="action-buttons">
<button class="btn-sm btn-primary" @click="openUserModal(user)">编辑</button>
<button class="btn-sm btn-warning" @click="resetPassword(user)">重置密码</button>
<button v-if="user.id !== appState.user?.id" class="btn-sm btn-error" @click="deleteUser(user)">删除</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div v-if="state.showUserModal" class="modal-overlay" @click.self="state.showUserModal = false">
<div class="modal-content">
<div class="modal-header">
<h2>{{ state.editingUser ? '编辑用户' : '添加用户' }}</h2>
<button class="modal-close" @click="state.showUserModal = false">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>用户名</label>
<input v-model="state.userForm.username" type="text" :disabled="!!state.editingUser" />
</div>
<div class="form-group">
<label>邮箱</label>
<input v-model="state.userForm.email" type="email" />
</div>
<div class="form-group" v-if="!state.editingUser">
<label>密码</label>
<input v-model="state.userForm.password" type="password" />
</div>
<div class="form-group">
<label>姓名</label>
<input v-model="state.userForm.full_name" type="text" />
</div>
<div class="form-group">
<label>角色</label>
<div class="checkbox-group">
<label v-for="role in state.roles" :key="role.id" class="checkbox-label">
<input type="checkbox" :value="role.id" v-model="state.userForm.role_ids" />
{{ role.name }}
</label>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" @click="state.showUserModal = false">取消</button>
<button class="btn-primary" @click="saveUser">保存</button>
</div>
</div>
</div>
</div>
`
};
const MoldInsightView = {
setup() {
const router = useRouter();
const state = reactive({
selectedFile: null,
uploading: false,
error: "",
currentTask: null,
polling: false,
dragOver: false,
progress: 0,
history: null,
showFileHistory: false,
selectedFileHistory: null,
fileHistoryRecords: []
});
const loadHistory = async () => {
try {
state.history = await apiRequest('/api/history');
} catch (e) {
console.error('加载历史记录失败:', e);
}
};
const viewFileHistory = async (filename) => {
try {
state.selectedFileHistory = filename;
state.fileHistoryRecords = await apiRequest(`/api/history/${encodeURIComponent(filename)}`);
state.showFileHistory = true;
} catch (e) {
handleApiError(e, '加载文件历史');
}
};
const viewResult = (record) => {
state.showFileHistory = false;
router.push(`/moldinsight/result/${record.id}`);
};
const handleFileChange = (event) => {
const file = event.target.files[0];
if (!file) return;
validateAndSelectFile(file);
};
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) {
state.error = "文件大小不能超过 100MB";
state.selectedFile = null;
return;
}
state.error = "";
state.selectedFile = file;
addNotification(`已选择文件: ${file.name}`, 'success');
};
const handleDrop = (event) => {
event.preventDefault();
state.dragOver = false;
const files = event.dataTransfer.files;
if (files.length > 0) validateAndSelectFile(files[0]);
};
const uploadFile = async () => {
if (!state.selectedFile) return;
state.uploading = true;
state.error = "";
state.progress = 0;
const formData = new FormData();
formData.append("file", state.selectedFile);
try {
const res = await fetch("/api/upload", {
method: "POST",
headers: appState.token ? { 'Authorization': `Bearer ${appState.token}` } : {},
body: formData
});
if (!res.ok) throw new Error(`上传失败: ${res.status}`);
const data = await res.json();
state.currentTask = { task_id: data.task_id, status: "processing", filename: data.file_info?.filename };
addNotification('文件上传成功,开始分析...', 'success');
startPolling(data.task_id);
} catch (e) {
state.error = handleApiError(e, '文件上传');
} finally {
state.uploading = false;
}
};
const startPolling = async (taskId) => {
state.polling = true;
state.progress = 10;
let pollCount = 0;
const poll = async () => {
try {
pollCount++;
state.progress = Math.min(90, 10 + pollCount * 0.5);
const task = await apiRequest(`/api/status/${taskId}`, { method: 'POST' });
state.currentTask = task;
if (task.status === "completed") {
state.polling = false;
state.progress = 100;
addNotification('分析完成', 'success');
loadHistory();
router.push(`/moldinsight/result/${taskId}`);
return;
}
if (task.status === "failed") {
state.polling = false;
state.error = task.error || "分析失败";
addNotification('分析失败', 'error');
return;
}
if (pollCount < 300) setTimeout(poll, 2000);
} catch (e) {
state.polling = false;
state.error = handleApiError(e, '轮询状态');
}
};
poll();
};
onMounted(() => {
if (!appState.user) {
router.push('/login');
return;
}
loadHistory();
});
return {
state,
handleFileChange,
handleDrop,
uploadFile,
formatFileSize,
formatDateTime,
viewFileHistory,
viewResult
};
},
template: `
<div class="page-container">
<div class="page-header">
<h1>MoldInsight</h1>
<p>STP 模具几何分析</p>
</div>
<div class="upload-section">
<div
:class="['upload-zone', { 'drag-over': state.dragOver }]"
@dragover.prevent="state.dragOver = true"
@dragleave.prevent="state.dragOver = false"
@drop="handleDrop"
@click="$refs.fileInput.click()"
>
<input
ref="fileInput"
type="file"
accept=".stp,.step"
@change="handleFileChange"
hidden
/>
<div class="upload-icon">📁</div>
<div class="upload-text">
<span class="upload-title">点击选择或拖拽文件</span>
<span class="upload-hint">支持 .stp, .step 格式,最大 100MB</span>
</div>
</div>
<div v-if="state.selectedFile" class="file-info">
<span class="file-name">{{ state.selectedFile.name }}</span>
<span class="file-size">{{ formatFileSize(state.selectedFile.size) }}</span>
</div>
<div v-if="state.error" class="error-message">{{ state.error }}</div>
<button
v-if="state.selectedFile"
class="btn-primary"
@click="uploadFile"
:disabled="state.uploading || state.polling"
>
{{ state.uploading ? '上传中...' : state.polling ? '分析中...' : '开始分析' }}
</button>
<div v-if="state.polling" class="progress-bar">
<div class="progress-fill" :style="{ width: state.progress + '%' }"></div>
</div>
</div>
<div v-if="state.history?.files?.length" class="section">
<h2 class="section-title">分析历史</h2>
<div class="table-container">
<table class="data-table">
<thead>
<tr>
<th>文件名</th>
<th>上传次数</th>
<th>文件大小</th>
<th>最新状态</th>
<th>最新分析时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="file in state.history.files" :key="file.filename">
<td>{{ file.filename }}</td>
<td>
<span class="badge badge-info">{{ file.upload_count }} 次</span>
</td>
<td>{{ formatFileSize(file.file_size) }}</td>
<td>
<span :class="['badge', file.latest_status === 'completed' ? 'badge-success' : file.latest_status === 'failed' ? 'badge-error' : 'badge-warning']">
{{ file.latest_status }}
</span>
</td>
<td>{{ formatDateTime(file.latest_upload_time) }}</td>
<td>
<div class="action-buttons">
<button v-if="file.latest_status === 'completed'" class="btn-sm btn-primary" @click="router.push('/moldinsight/result/' + file.latest_id)">
查看最新
</button>
<button class="btn-sm btn-secondary" @click="viewFileHistory(file.filename)">
历史记录
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div v-if="state.showFileHistory" class="modal-overlay" @click.self="state.showFileHistory = false">
<div class="modal-content" style="max-width: 800px;">
<div class="modal-header">
<h2>{{ state.selectedFileHistory }} - 历史记录</h2>
<button class="modal-close" @click="state.showFileHistory = false">×</button>
</div>
<div class="modal-body">
<table class="data-table">
<thead>
<tr>
<th>上传时间</th>
<th>文件大小</th>
<th>状态</th>
<th>体积 (mm³)</th>
<th>表面积 (mm²)</th>
<th>重量 (g)</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="record in state.fileHistoryRecords" :key="record.id">
<td>{{ formatDateTime(record.upload_time) }}</td>
<td>{{ formatFileSize(record.file_size) }}</td>
<td>
<span :class="['badge', record.status === 'completed' ? 'badge-success' : record.status === 'failed' ? 'badge-error' : 'badge-warning']">
{{ record.status }}
</span>
</td>
<td>{{ record.volume ? formatNumber(record.volume) : '-' }}</td>
<td>{{ record.surface_area ? formatNumber(record.surface_area) : '-' }}</td>
<td>{{ record.product_weight ? record.product_weight.toFixed(2) : '-' }}</td>
<td>
<button v-if="record.has_analysis" class="btn-sm btn-primary" @click="viewResult(record)">
查看详情
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
`
};
const ResultView = {
setup() {
const route = useRoute();
const router = useRouter();
const state = reactive({
task: null,
loading: true,
error: ''
});
const loadTask = async () => {
try {
state.task = await apiRequest(`/api/status/${route.params.taskId}`, { method: 'POST' });
console.log('任务数据:', state.task);
console.log('key_info:', state.task.key_info);
console.log('cavity_data:', state.task.cavity_data);
console.log('analysis_result:', state.task.analysis_result);
console.log('geometry_data:', state.task.geometry_data);
} catch (e) {
state.error = handleApiError(e, '加载任务详情');
} finally {
state.loading = false;
}
};
onMounted(() => {
if (!appState.user) {
router.push('/login');
return;
}
loadTask();
});
const getPriorityText = (priority) => {
const priorityMap = {
'critical': '紧急',
'high': '高',
'medium': '中',
'low': '低'
};
return priorityMap[priority] || priority;
};
return { state, formatFileSize, formatDateTime, formatNumber, getPriorityText };
},
template: `
<div class="page-container">
<div class="page-header">
<button class="btn-back" @click="$router.back()">← 返回</button>
<h1>分析结果</h1>
</div>
<div v-if="state.loading" class="loading-state">
<div class="spinner"></div>
<span>加载中...</span>
</div>
<div v-else-if="state.error" class="error-state">
<p>{{ state.error }}</p>
</div>
<div v-else-if="state.task" class="result-container">
<div class="result-header">
<h2>{{ state.task.filename }}</h2>
<span :class="['badge', state.task.status === 'completed' ? 'badge-success' : 'badge-error']">
{{ state.task.status }}
</span>
</div>
<div class="result-grid">
<div class="result-card">
<h3>文件信息</h3>
<div class="info-list">
<div class="info-item">
<span class="info-label">文件大小</span>
<span class="info-value">{{ formatFileSize(state.task.file_size) }}</span>
</div>
<div class="info-item">
<span class="info-label">分析时间</span>
<span class="info-value">{{ formatDateTime(state.task.completed_at) }}</span>
</div>
<div class="info-item">
<span class="info-label">状态</span>
<span class="info-value">{{ state.task.status }}</span>
</div>
</div>
</div>
<div class="result-card">
<h3>几何数据</h3>
<div class="info-list">
<div class="info-item" v-if="state.task.mesh_summary">
<span class="info-label">顶点数</span>
<span class="info-value">{{ formatNumber(state.task.mesh_summary.vertex_count) }}</span>
</div>
<div class="info-item" v-if="state.task.mesh_summary">
<span class="info-label">面数</span>
<span class="info-value">{{ formatNumber(state.task.mesh_summary.face_count) }}</span>
</div>
<div class="info-item" v-if="state.task.mesh_summary || state.task.geometry_data">
<span class="info-label">边数</span>
<span class="info-value">{{ formatNumber(state.task.geometry_data?.topology?.edge_count || state.task.geometry_data?.topology?.edges || state.task.mesh_summary?.edge_count || 'N/A') }}</span>
</div>
<div class="info-item" v-if="state.task.geometry_data">
<span class="info-label">体积</span>
<span class="info-value">{{ formatNumber(state.task.geometry_data.volume) }} mm³</span>
</div>
<div class="info-item" v-if="state.task.geometry_data">
<span class="info-label">表面积</span>
<span class="info-value">{{ formatNumber(state.task.geometry_data.surface_area) }} mm²</span>
</div>
</div>
</div>
</div>
<div v-if="state.task.cavity_data || state.task.key_info" class="viewer-section">
<h3>模具信息</h3>
<div class="result-grid">
<div class="result-card">
<h4>基本信息</h4>
<div class="info-list">
<div class="info-item">
<span class="info-label">零件名称</span>
<span class="info-value">{{ state.task.cavity_data?.metadata?.part_name || state.task.key_info?.metadata?.part_name || 'N/A' }}</span>
</div>
<div class="info-item">
<span class="info-label">材料</span>
<span class="info-value">{{ state.task.cavity_data?.metadata?.material || state.task.key_info?.metadata?.material || 'N/A' }}</span>
</div>
</div>
</div>
<div class="result-card" v-if="state.task.cavity_data?.mold_cavities || state.task.key_info?.mold_cavities">
<h4>型腔信息</h4>
<div class="info-list">
<div class="info-item">
<span class="info-label">型腔数量</span>
<span class="info-value">{{ Object.keys(state.task.cavity_data?.mold_cavities || state.task.key_info?.mold_cavities || {}).length }}</span>
</div>
</div>
</div>
</div>
</div>
<div v-if="state.task.html_file" class="viewer-section">
<h3>3D 预览</h3>
<iframe :src="state.task.html_file" class="viewer-frame"></iframe>
</div>
<div v-if="state.task.analysis_result" class="viewer-section">
<h3>分析结果详情</h3>
<!-- 分析摘要 -->
<div class="analysis-summary" v-if="state.task.analysis_result.analysis_summary">
<div class="summary-header">
<h4>分析摘要</h4>
<span class="summary-icon">📋</span>
</div>
<div class="summary-content">
{{ state.task.analysis_result.analysis_summary }}
</div>
</div>
<div class="result-grid">
<div class="result-card full-width">
<h4>1. 产品特征识别</h4>
<div class="info-list">
<div class="info-item">
<span class="info-label">整体轮廓和尺寸比例</span>
<span class="info-value">{{ state.task.analysis_result?.geometry_data?.bounding_box?.dimensions ?
(state.task.analysis_result.geometry_data.bounding_box.dimensions[0]?.toFixed(1) || 0) + ' × ' +
(state.task.analysis_result.geometry_data.bounding_box.dimensions[1]?.toFixed(1) || 0) + ' × ' +
(state.task.analysis_result.geometry_data.bounding_box.dimensions[2]?.toFixed(1) || 0) + ' mm' : 'N/A' }}</span>
</div>
<div class="info-item">
<span class="info-label">壁厚分布</span>
<span class="info-value">{{ state.task.analysis_result.detected_features?.find(f => f.feature_type === 'wall_thickness')?.description || '待分析' }}</span>
</div>
<div class="info-item">
<span class="info-label">加强筋位置和密度</span>
<span class="info-value">{{ state.task.analysis_result.detected_features?.filter(f => f.feature_type === 'rib').length || 0 }} 个加强筋</span>
</div>
<div class="info-item">
<span class="info-label">孔洞和凹槽位置</span>
<span class="info-value">{{ state.task.analysis_result.detected_features?.filter(f => f.feature_type === 'hole' || f.feature_type === 'pocket').length || 0 }} 个孔洞/凹槽</span>
</div>
<div class="info-item">
<span class="info-label">倒扣区域检测</span>
<span class="info-value">{{ state.task.analysis_result.detected_features?.filter(f => f.feature_type === 'undercut').length || 0 }} 个倒扣区域</span>
</div>
<div class="info-item">
<span class="info-label">对称性分析</span>
<span class="info-value">{{ state.task.analysis_result.geometry_data?.symmetry || '非对称' }}</span>
</div>
<div class="info-item">
<span class="info-label">重心位置</span>
<span class="info-value">{{ state.task.analysis_result.geometry_data?.center_of_mass ?
'(' + (state.task.analysis_result.geometry_data.center_of_mass[0]?.toFixed(2) || 0) + ', ' +
(state.task.analysis_result.geometry_data.center_of_mass[1]?.toFixed(2) || 0) + ', ' +
(state.task.analysis_result.geometry_data.center_of_mass[2]?.toFixed(2) || 0) + ') mm' : 'N/A' }}</span>
</div>
</div>
</div>
<div class="result-card full-width">
<h4>2. 泡沫包装设计决策</h4>
<div class="info-list">
<div class="info-item">
<span class="info-label">泡沫厚度建议</span>
<span class="info-value">{{ state.task.analysis_result.design_recommendations?.find(r => r.priority === 'high')?.recommendation || '根据产品重量和脆弱程度自动计算' }}</span>
</div>
<div class="info-item">
<span class="info-label">加强筋布局</span>
<span class="info-value">基于产品薄弱区域自动布置</span>
</div>
<div class="info-item">
<span class="info-label">取手槽位置</span>
<span class="info-value">基于重心位置:{{ state.task.analysis_result.geometry_data?.center_of_mass ? '自动优化' : '手动设置' }}</span>
</div>
<div class="info-item">
<span class="info-label">通风孔位置</span>
<span class="info-value">防止真空吸附:建议在产品最大平面区域设置通风孔</span>
</div>
<div class="info-item">
<span class="info-label">定位结构设计</span>
<span class="info-value">{{ state.task.analysis_result.detected_features?.filter(f => f.feature_type === '定位').length || 0 }} 个定位特征</span>
</div>
<div class="info-item">
<span class="info-label">分型面选择</span>
<span class="info-value">基于产品几何:自动推荐最优分型面</span>
</div>
<!-- 设计建议列表 -->
<div class="recommendations-section" v-if="state.task.analysis_result.design_recommendations?.length">
<h5>详细设计建议</h5>
<div class="recommendations-list">
<div v-for="rec in state.task.analysis_result.design_recommendations.sort((a, b) => {
const priorityOrder = { 'critical': 0, 'high': 1, 'medium': 2, 'low': 3 };
return priorityOrder[a.priority] - priorityOrder[b.priority];
})" :key="rec.type" class="recommendation-item" :class="rec.priority">
<span class="priority-badge" :class="rec.priority">{{ getPriorityText(rec.priority) }}</span>
<span class="recommendation-text">{{ rec.description }}</span>
<span class="recommendation-reason" v-if="rec.reason">({{ rec.reason }})</span>
</div>
</div>
</div>
</div>
</div>
<div class="result-card full-width">
<h4>3. 模具工程决策</h4>
<div class="info-list">
<div class="info-item">
<span class="info-label">型腔数量建议</span>
<span class="info-value">{{ state.task.cavity_data?.mold_cavities ? Object.keys(state.task.cavity_data.mold_cavities).length : 1 }} 腔</span>
</div>
<div class="info-item">
<span class="info-label">模架尺寸</span>
<span class="info-value">{{ state.task.key_info?.mold_parameters?.mold_size || '基于泡沫外形自动计算' }}</span>
</div>
<div class="info-item" v-if="state.task.key_info?.geometric_characteristics">
<span class="info-label">预估锁模力</span>
<span class="info-value">{{ state.task.key_info.geometric_characteristics.estimated_clamping_force || '自动计算' }} 吨</span>
</div>
<div class="info-item">
<span class="info-label">顶出系统</span>
<span class="info-value">顶针顶出(自动布局)</span>
</div>
<div class="info-item">
<span class="info-label">冷却水路设计</span>
<span class="info-value">{{ state.task.analysis_result.geometry_data?.volume > 1000000 ? '需要冷却水路' : '自然冷却' }}</span>
</div>
<div class="info-item">
<span class="info-label">材料选择</span>
<span class="info-value">{{ state.task.analysis_result.quality_metrics?.volume ? '根据产量和精度自动推荐' : '7075 铝合金' }}</span>
</div>
<div class="info-item" v-if="state.task.analysis_result.quality_metrics">
<span class="info-label">体积利用率</span>
<span class="info-value">{{ (state.task.analysis_result.quality_metrics.volume_utilization * 100)?.toFixed(1) || 'N/A' }}%</span>
</div>
<div class="info-item" v-if="state.task.analysis_result.quality_metrics">
<span class="info-label">拓扑复杂度</span>
<span class="info-value">{{ state.task.analysis_result.quality_metrics.topology_complexity?.toFixed(2) || 'N/A' }}</span>
</div>
<div class="info-item" v-if="state.task.analysis_result.quality_metrics">
<span class="info-label">壁厚均匀性</span>
<span class="info-value">{{ (state.task.analysis_result.quality_metrics.wall_uniformity * 100)?.toFixed(1) || 'N/A' }}%</span>
</div>
<div class="info-item" v-if="state.task.key_info?.quality_considerations">
<span class="info-label">翘曲风险</span>
<span class="info-value">{{ state.task.key_info.quality_considerations.warpage_risk || '低风险' }}</span>
</div>
<div class="info-item" v-if="state.task.key_info?.quality_considerations">
<span class="info-label">潜在焊缝线</span>
<span class="info-value">{{ state.task.key_info.quality_considerations.potential_weld_lines || 0 }} 条</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`
};
const InventoryView = {
setup() {
const router = useRouter();
const route = useRoute();
const state = reactive({
activeTab: 'dashboard',
dashboard: null,
products: [],
suppliers: [],
customers: [],
warehouses: [],
inventory: [],
movements: [],
loading: false
});
const loadDashboard = async () => {
state.loading = true;
try {
state.dashboard = await apiRequest('/api/inventory/dashboard');
} catch (e) {
handleApiError(e, '加载仪表盘');
} finally {
state.loading = false;
}
};
const loadProducts = async () => {
state.loading = true;
try {
state.products = await apiRequest('/api/inventory/products');
} catch (e) {
handleApiError(e, '加载产品');
} finally {
state.loading = false;
}
};
const loadSuppliers = async () => {
state.loading = true;
try {
state.suppliers = await apiRequest('/api/inventory/suppliers');
} catch (e) {
handleApiError(e, '加载供应商');
} finally {
state.loading = false;
}
};
const loadCustomers = async () => {
state.loading = true;
try {
state.customers = await apiRequest('/api/inventory/customers');
} catch (e) {
handleApiError(e, '加载客户');
} finally {
state.loading = false;
}
};
const loadInventory = async () => {
state.loading = true;
try {
state.inventory = await apiRequest('/api/inventory/inventory');
} catch (e) {
handleApiError(e, '加载库存');
} finally {
state.loading = false;
}
};
const loadMovements = async () => {
state.loading = true;
try {
state.movements = await apiRequest('/api/inventory/stock-movements');
} catch (e) {
handleApiError(e, '加载变动记录');
} finally {
state.loading = false;
}
};
const switchTab = (tab) => {
state.activeTab = tab;
switch (tab) {
case 'dashboard': loadDashboard(); break;
case 'products': loadProducts(); break;
case 'suppliers': loadSuppliers(); break;
case 'customers': loadCustomers(); break;
case 'inventory': loadInventory(); break;
case 'movements': loadMovements(); break;
}
};
onMounted(() => {
if (!appState.user) {
router.push('/login');
return;
}
loadDashboard();
});
return {
state,
switchTab,
formatNumber,
formatCurrency,
formatDateTime
};
},
template: `
<div class="page-container">
<div class="page-header">
<h1>进销存管理</h1>
<p>库存、采购、销售管理</p>
</div>
<div class="tabs">
<button :class="['tab', { active: state.activeTab === 'dashboard' }]" @click="switchTab('dashboard')">仪表盘</button>
<button :class="['tab', { active: state.activeTab === 'products' }]" @click="switchTab('products')">产品</button>
<button :class="['tab', { active: state.activeTab === 'inventory' }]" @click="switchTab('inventory')">库存</button>
<button :class="['tab', { active: state.activeTab === 'suppliers' }]" @click="switchTab('suppliers')">供应商</button>
<button :class="['tab', { active: state.activeTab === 'customers' }]" @click="switchTab('customers')">客户</button>
<button :class="['tab', { active: state.activeTab === 'movements' }]" @click="switchTab('movements')">变动记录</button>
</div>
<div v-if="state.loading" class="loading-state">
<div class="spinner"></div>
<span>加载中...</span>
</div>
<div v-else>
<div v-if="state.activeTab === 'dashboard'" class="dashboard-grid">
<div class="stat-card">
<div class="stat-icon">📦</div>
<div class="stat-content">
<div class="stat-value">{{ state.dashboard?.product_count || 0 }}</div>
<div class="stat-label">产品数量</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">📊</div>
<div class="stat-content">
<div class="stat-value">{{ state.dashboard?.total_stock || 0 }}</div>
<div class="stat-label">库存总量</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">💰</div>
<div class="stat-content">
<div class="stat-value">{{ formatCurrency(state.dashboard?.total_value || 0) }}</div>
<div class="stat-label">库存价值</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">🏭</div>
<div class="stat-content">
<div class="stat-value">{{ state.dashboard?.supplier_count || 0 }}</div>
<div class="stat-label">供应商</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">👥</div>
<div class="stat-content">
<div class="stat-value">{{ state.dashboard?.customer_count || 0 }}</div>
<div class="stat-label">客户</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">🏪</div>
<div class="stat-content">
<div class="stat-value">{{ state.dashboard?.warehouse_count || 0 }}</div>
<div class="stat-label">仓库</div>
</div>
</div>
</div>
<div v-else-if="state.activeTab === 'products'" class="table-container">
<table class="data-table">
<thead>
<tr>
<th>SKU</th>
<th>名称</th>
<th>分类</th>
<th>单位</th>
<th>成本价</th>
<th>销售价</th>
</tr>
</thead>
<tbody>
<tr v-for="product in state.products" :key="product.id">
<td>{{ product.sku }}</td>
<td>{{ product.name }}</td>
<td>{{ product.category || '-' }}</td>
<td>{{ product.unit }}</td>
<td>{{ formatCurrency(product.cost_price) }}</td>
<td>{{ formatCurrency(product.sale_price) }}</td>
</tr>
</tbody>
</table>
</div>
<div v-else-if="state.activeTab === 'inventory'" class="table-container">
<table class="data-table">
<thead>
<tr>
<th>SKU</th>
<th>产品</th>
<th>仓库</th>
<th>数量</th>
<th>可用</th>
</tr>
</thead>
<tbody>
<tr v-for="item in state.inventory" :key="item.id">
<td>{{ item.product_sku }}</td>
<td>{{ item.product_name }}</td>
<td>{{ item.warehouse_name }}</td>
<td>{{ item.quantity }}</td>
<td>{{ item.available_quantity }}</td>
</tr>
</tbody>
</table>
</div>
<div v-else-if="state.activeTab === 'suppliers'" class="table-container">
<table class="data-table">
<thead>
<tr>
<th>编码</th>
<th>名称</th>
<th>联系人</th>
<th>电话</th>
<th>邮箱</th>
</tr>
</thead>
<tbody>
<tr v-for="supplier in state.suppliers" :key="supplier.id">
<td>{{ supplier.code }}</td>
<td>{{ supplier.name }}</td>
<td>{{ supplier.contact_person || '-' }}</td>
<td>{{ supplier.phone || '-' }}</td>
<td>{{ supplier.email || '-' }}</td>
</tr>
</tbody>
</table>
</div>
<div v-else-if="state.activeTab === 'customers'" class="table-container">
<table class="data-table">
<thead>
<tr>
<th>编码</th>
<th>名称</th>
<th>联系人</th>
<th>电话</th>
<th>邮箱</th>
</tr>
</thead>
<tbody>
<tr v-for="customer in state.customers" :key="customer.id">
<td>{{ customer.code }}</td>
<td>{{ customer.name }}</td>
<td>{{ customer.contact_person || '-' }}</td>
<td>{{ customer.phone || '-' }}</td>
<td>{{ customer.email || '-' }}</td>
</tr>
</tbody>
</table>
</div>
<div v-else-if="state.activeTab === 'movements'" class="table-container">
<table class="data-table">
<thead>
<tr>
<th>产品</th>
<th>类型</th>
<th>数量</th>
<th>变动前</th>
<th>变动后</th>
<th>时间</th>
</tr>
</thead>
<tbody>
<tr v-for="movement in state.movements" :key="movement.id">
<td>{{ movement.product_name }}</td>
<td>
<span :class="['badge', movement.movement_type === 'in' ? 'badge-success' : movement.movement_type === 'out' ? 'badge-error' : 'badge-warning']">
{{ movement.movement_type === 'in' ? '入库' : movement.movement_type === 'out' ? '出库' : '调整' }}
</span>
</td>
<td>{{ movement.quantity }}</td>
<td>{{ movement.before_quantity }}</td>
<td>{{ movement.after_quantity }}</td>
<td>{{ formatDateTime(movement.created_at) }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
`
};
const routes = [
{ path: "/", component: HomeView },
{ path: "/login", component: LoginView },
{ path: "/users", component: UsersView },
{ path: "/moldinsight", component: MoldInsightView },
{ path: "/moldinsight/result/:taskId", component: ResultView },
{ path: "/inventory", component: InventoryView }
];
const router = createRouter({
history: createWebHistory(),
routes
});
router.beforeEach((to, from, next) => {
const publicPages = ['/login'];
const authRequired = !publicPages.includes(to.path);
if (authRequired && !appState.user) {
return next('/login');
}
if (to.path === '/login' && appState.user) {
return next('/');
}
next();
});
const app = createApp(App);
app.use(router);
app.mount("#app");