Files
geMoldInsight/static/vue-app.js
T
2026-03-04 23:19:09 +08:00

1402 lines
43 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();
});
return {
route,
router,
appState,
menuItems,
isActive,
handleLogout,
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">◆</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) }]"
>
<span class="nav-icon">{{ item.icon }}</span>
{{ item.label }}
</router-link>
</nav>
<div class="user-section">
<template v-if="appState.user">
<div class="user-info">
<span class="user-name">{{ appState.user.full_name || appState.user.username }}</span>
<span v-if="appState.user.is_superuser" class="admin-badge">管理员</span>
</div>
<button class="btn-logout" @click="handleLogout">退出</button>
</template>
<template v-else>
<router-link to="/login" class="btn-login">登录</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>
<footer class="app-footer">
<div class="footer-content">
<span class="footer-item">Gemold v4.0.0</span>
<span class="footer-divider"></span>
<span class="footer-item">模具制造管理系统</span>
</div>
</footer>
</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="auth-page">
<div class="auth-card">
<div class="auth-header">
<div class="auth-logo">◆</div>
<h1>登录</h1>
<p>登录到 Gemold 系统</p>
</div>
<form @submit.prevent="handleSubmit" class="auth-form">
<div class="form-group">
<label>用户名</label>
<input
v-model="state.username"
type="text"
placeholder="请输入用户名"
autocomplete="username"
/>
</div>
<div class="form-group">
<label>密码</label>
<input
v-model="state.password"
type="password"
placeholder="请输入密码"
autocomplete="current-password"
/>
</div>
<div v-if="state.error" class="error-message">{{ state.error }}</div>
<button type="submit" class="btn-primary btn-full" :disabled="state.loading">
{{ state.loading ? '登录中...' : '登录' }}
</button>
</form>
<div class="auth-footer">
<p class="auth-tip">如需开通账号,请联系管理员</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
});
const loadHistory = async () => {
try {
state.history = await apiRequest('/api/history');
} catch (e) {
console.error('加载历史记录失败:', e);
}
};
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');
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
};
},
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>
</tr>
</thead>
<tbody>
<tr v-for="file in state.history.files.slice(0, 5)" :key="file.task_id">
<td>{{ file.filename }}</td>
<td>{{ formatFileSize(file.file_size) }}</td>
<td>
<span :class="['badge', file.status === 'completed' ? 'badge-success' : file.status === 'failed' ? 'badge-error' : 'badge-warning']">
{{ file.status }}
</span>
</td>
<td>{{ formatDateTime(file.created_at) }}</td>
<td>
<button v-if="file.status === 'completed'" class="btn-sm btn-primary" @click="$router.push('/moldinsight/result/' + file.task_id)">
查看
</button>
</td>
</tr>
</tbody>
</table>
</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);
} catch (e) {
state.error = handleApiError(e, '加载任务详情');
} finally {
state.loading = false;
}
};
onMounted(() => {
if (!appState.user) {
router.push('/login');
return;
}
loadTask();
});
return { state, formatFileSize, formatDateTime, formatNumber };
},
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>
</div>
<div class="result-card" v-if="state.task.geometry_data || state.task.mesh_summary">
<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.geometry_data">
<span class="info-label">边数</span>
<span class="info-value">{{ formatNumber(state.task.geometry_data.edge_count) }}</span>
</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>
</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");