2026-02-17 00:42:55 +08:00
|
|
|
|
/**
|
2026-03-04 00:47:41 +08:00
|
|
|
|
* Gemold - 模具制造管理系统
|
|
|
|
|
|
* 版本: 4.0.0
|
2026-02-17 00:42:55 +08:00
|
|
|
|
*/
|
2026-02-17 00:12:36 +08:00
|
|
|
|
|
2026-03-03 23:57:04 +08:00
|
|
|
|
const { createApp, ref, computed, onMounted, reactive, watch, nextTick } = Vue;
|
2026-02-17 00:12:36 +08:00
|
|
|
|
const { createRouter, createWebHistory, useRoute, useRouter } = VueRouter;
|
|
|
|
|
|
|
2026-02-17 00:42:55 +08:00
|
|
|
|
const appState = reactive({
|
2026-03-04 00:47:41 +08:00
|
|
|
|
user: null,
|
|
|
|
|
|
token: null,
|
2026-02-17 00:42:55 +08:00
|
|
|
|
loading: false,
|
2026-03-03 23:57:04 +08:00
|
|
|
|
notifications: [],
|
2026-03-04 00:47:41 +08:00
|
|
|
|
initialized: false
|
2026-02-17 00:42:55 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-02-17 00:12:36 +08:00
|
|
|
|
function formatFileSize(bytes) {
|
2026-03-04 00:10:05 +08:00
|
|
|
|
if (!bytes || bytes === 0) return "0 B";
|
2026-02-17 00:12:36 +08:00
|
|
|
|
const k = 1024;
|
2026-03-04 00:10:05 +08:00
|
|
|
|
const sizes = ["B", "KB", "MB", "GB"];
|
2026-02-17 00:12:36 +08:00
|
|
|
|
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 {
|
2026-02-17 00:42:55 +08:00
|
|
|
|
return new Date(dateString).toLocaleString('zh-CN');
|
2026-02-17 00:12:36 +08:00
|
|
|
|
} catch {
|
|
|
|
|
|
return dateString;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-04 00:47:41 +08:00
|
|
|
|
function formatDate(dateString) {
|
|
|
|
|
|
if (!dateString) return "N/A";
|
|
|
|
|
|
try {
|
|
|
|
|
|
return new Date(dateString).toLocaleDateString('zh-CN');
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return dateString;
|
|
|
|
|
|
}
|
2026-02-17 00:12:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-04 00:47:41 +08:00
|
|
|
|
function formatCurrency(amount) {
|
|
|
|
|
|
if (amount === null || amount === undefined) return "¥0.00";
|
|
|
|
|
|
return "¥" + Number(amount).toFixed(2);
|
2026-02-17 00:42:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-03 23:57:04 +08:00
|
|
|
|
let notificationId = 0;
|
|
|
|
|
|
|
2026-02-17 00:42:55 +08:00
|
|
|
|
function addNotification(message, type = 'info') {
|
2026-03-03 23:57:04 +08:00
|
|
|
|
const id = ++notificationId;
|
2026-03-04 00:47:41 +08:00
|
|
|
|
const notification = { id, message, type, timestamp: new Date(), visible: true };
|
2026-02-17 00:42:55 +08:00
|
|
|
|
appState.notifications.push(notification);
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
2026-03-03 23:57:04 +08:00
|
|
|
|
const index = appState.notifications.findIndex(n => n.id === id);
|
2026-02-17 00:42:55 +08:00
|
|
|
|
if (index > -1) {
|
2026-03-03 23:57:04 +08:00
|
|
|
|
appState.notifications[index].visible = false;
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
const idx = appState.notifications.findIndex(n => n.id === id);
|
2026-03-04 00:47:41 +08:00
|
|
|
|
if (idx > -1) appState.notifications.splice(idx, 1);
|
2026-03-03 23:57:04 +08:00
|
|
|
|
}, 300);
|
2026-02-17 00:42:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
}, 5000);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleApiError(error, context = '') {
|
|
|
|
|
|
console.error(`API错误 [${context}]:`, error);
|
|
|
|
|
|
const message = error.message || '请求失败,请稍后重试';
|
|
|
|
|
|
addNotification(message, 'error');
|
|
|
|
|
|
return message;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-04 00:47:41 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-17 00:12:36 +08:00
|
|
|
|
const App = {
|
|
|
|
|
|
setup() {
|
|
|
|
|
|
const route = useRoute();
|
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
|
2026-03-04 00:47:41 +08:00
|
|
|
|
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: '👤' });
|
2026-02-17 00:12:36 +08:00
|
|
|
|
}
|
2026-03-04 00:47:41 +08:00
|
|
|
|
|
|
|
|
|
|
return items;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const isActive = (path) => {
|
|
|
|
|
|
if (path === '/') return route.path === '/';
|
|
|
|
|
|
return route.path.startsWith(path);
|
2026-02-17 00:12:36 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-04 00:47:41 +08:00
|
|
|
|
const handleLogout = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await apiRequest('/api/auth/logout', { method: 'POST' });
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
clearAuth();
|
|
|
|
|
|
addNotification('已退出登录', 'success');
|
|
|
|
|
|
router.push('/login');
|
2026-02-17 00:42:55 +08:00
|
|
|
|
};
|
2026-02-17 00:12:36 +08:00
|
|
|
|
|
2026-02-17 00:42:55 +08:00
|
|
|
|
onMounted(() => {
|
2026-03-04 00:47:41 +08:00
|
|
|
|
initAuth();
|
2026-02-17 00:42:55 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
route,
|
|
|
|
|
|
router,
|
|
|
|
|
|
appState,
|
2026-03-04 00:47:41 +08:00
|
|
|
|
menuItems,
|
2026-02-17 00:42:55 +08:00
|
|
|
|
isActive,
|
2026-03-04 00:47:41 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-17 00:42:55 +08:00
|
|
|
|
};
|
2026-02-17 00:12:36 +08:00
|
|
|
|
},
|
|
|
|
|
|
template: `
|
2026-03-04 00:10:05 +08:00
|
|
|
|
<div class="app-container">
|
2026-02-17 00:42:55 +08:00
|
|
|
|
<div class="notification-container" v-if="appState.notifications.length > 0">
|
2026-03-03 23:57:04 +08:00
|
|
|
|
<TransitionGroup name="notification">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="notification in appState.notifications"
|
|
|
|
|
|
:key="notification.id"
|
2026-03-04 00:10:05 +08:00
|
|
|
|
:class="['notification', 'notification-' + notification.type]"
|
2026-03-03 23:57:04 +08:00
|
|
|
|
>
|
2026-03-04 00:10:05 +08:00
|
|
|
|
<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>
|
2026-03-03 23:57:04 +08:00
|
|
|
|
<button class="notification-close" @click="dismissNotification(notification.id)">×</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</TransitionGroup>
|
2026-02-17 00:42:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-02-17 00:12:36 +08:00
|
|
|
|
<header class="app-header">
|
2026-03-04 00:10:05 +08:00
|
|
|
|
<div class="header-content">
|
2026-03-04 00:47:41 +08:00
|
|
|
|
<div class="logo" @click="router.push('/')">
|
|
|
|
|
|
<div class="logo-icon">◆</div>
|
2026-03-04 00:10:05 +08:00
|
|
|
|
<div>
|
2026-03-04 00:47:41 +08:00
|
|
|
|
<div class="logo-text">Gemold</div>
|
|
|
|
|
|
<div class="logo-subtitle">模具制造管理系统</div>
|
2026-03-04 00:10:05 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-04 00:47:41 +08:00
|
|
|
|
<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 }}
|
2026-03-04 00:10:05 +08:00
|
|
|
|
</router-link>
|
|
|
|
|
|
</nav>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="user-section">
|
2026-03-04 00:47:41 +08:00
|
|
|
|
<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>
|
2026-02-17 00:12:36 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
2026-03-04 00:10:05 +08:00
|
|
|
|
<main class="main-content">
|
2026-03-03 23:57:04 +08:00
|
|
|
|
<router-view v-slot="{ Component }">
|
2026-03-04 00:10:05 +08:00
|
|
|
|
<transition name="fade" mode="out-in">
|
2026-03-03 23:57:04 +08:00
|
|
|
|
<component :is="Component" />
|
|
|
|
|
|
</transition>
|
|
|
|
|
|
</router-view>
|
2026-02-17 00:12:36 +08:00
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
|
|
<footer class="app-footer">
|
2026-03-04 00:10:05 +08:00
|
|
|
|
<div class="footer-content">
|
2026-03-04 00:47:41 +08:00
|
|
|
|
<span class="footer-item">Gemold v4.0.0</span>
|
2026-03-04 00:10:05 +08:00
|
|
|
|
<span class="footer-divider"></span>
|
2026-03-04 00:47:41 +08:00
|
|
|
|
<span class="footer-item">模具制造管理系统</span>
|
2026-02-17 00:12:36 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</footer>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-04 00:47:41 +08:00
|
|
|
|
const LoginView = {
|
|
|
|
|
|
setup() {
|
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
const state = reactive({
|
|
|
|
|
|
isLogin: true,
|
|
|
|
|
|
username: '',
|
|
|
|
|
|
password: '',
|
|
|
|
|
|
email: '',
|
|
|
|
|
|
full_name: '',
|
|
|
|
|
|
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 {
|
|
|
|
|
|
if (state.isLogin) {
|
|
|
|
|
|
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('/');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (!state.email) {
|
|
|
|
|
|
state.error = '请填写邮箱';
|
|
|
|
|
|
state.loading = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const res = await fetch('/api/auth/register', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
username: state.username,
|
|
|
|
|
|
password: state.password,
|
|
|
|
|
|
email: state.email,
|
|
|
|
|
|
full_name: state.full_name || null
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
|
const error = await res.json();
|
|
|
|
|
|
throw new Error(error.detail || '注册失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
addNotification('注册成功,请登录', 'success');
|
|
|
|
|
|
state.isLogin = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
} 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>{{ state.isLogin ? '登录' : '注册' }}</h1>
|
|
|
|
|
|
<p>{{ state.isLogin ? '登录到 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" v-if="!state.isLogin">
|
|
|
|
|
|
<label>邮箱</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="state.email"
|
|
|
|
|
|
type="email"
|
|
|
|
|
|
placeholder="请输入邮箱"
|
|
|
|
|
|
autocomplete="email"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="form-group" v-if="!state.isLogin">
|
|
|
|
|
|
<label>姓名</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="state.full_name"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
placeholder="请输入姓名(可选)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>密码</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="state.password"
|
|
|
|
|
|
type="password"
|
|
|
|
|
|
placeholder="请输入密码"
|
|
|
|
|
|
:autocomplete="state.isLogin ? 'current-password' : 'new-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 ? '处理中...' : (state.isLogin ? '登录' : '注册') }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="auth-footer">
|
|
|
|
|
|
<span>{{ state.isLogin ? '没有账户?' : '已有账户?' }}</span>
|
|
|
|
|
|
<a @click="state.isLogin = !state.isLogin; state.error = ''">
|
|
|
|
|
|
{{ state.isLogin ? '立即注册' : '立即登录' }}
|
|
|
|
|
|
</a>
|
|
|
|
|
|
</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 = {
|
2026-02-17 00:12:36 +08:00
|
|
|
|
setup() {
|
|
|
|
|
|
const router = useRouter();
|
2026-03-04 00:47:41 +08:00
|
|
|
|
const state = reactive({
|
|
|
|
|
|
users: [],
|
|
|
|
|
|
loading: true
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const loadUsers = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
state.users = await apiRequest('/api/auth/users');
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
handleApiError(e, '加载用户列表');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
state.loading = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-02-17 00:12:36 +08:00
|
|
|
|
|
2026-03-04 00:47:41 +08:00
|
|
|
|
const toggleActive = async (user) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const updated = await apiRequest(`/api/auth/users/${user.id}/toggle-active`, { method: 'PUT' });
|
|
|
|
|
|
const index = state.users.findIndex(u => u.id === user.id);
|
|
|
|
|
|
if (index > -1) state.users[index] = updated;
|
|
|
|
|
|
addNotification(`用户 ${user.username} 已${updated.is_active ? '启用' : '禁用'}`, 'success');
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
handleApiError(e, '切换用户状态');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const toggleAdmin = async (user) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const updated = await apiRequest(`/api/auth/users/${user.id}/toggle-admin`, { method: 'PUT' });
|
|
|
|
|
|
const index = state.users.findIndex(u => u.id === user.id);
|
|
|
|
|
|
if (index > -1) state.users[index] = updated;
|
|
|
|
|
|
addNotification(`用户 ${user.username} ${updated.is_superuser ? '已设为管理员' : '已取消管理员'}`, 'success');
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
handleApiError(e, '切换管理员权限');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
if (!appState.user?.is_superuser) {
|
|
|
|
|
|
router.push('/');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
loadUsers();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return { state, appState, toggleActive, toggleAdmin, formatDateTime };
|
|
|
|
|
|
},
|
|
|
|
|
|
template: `
|
|
|
|
|
|
<div class="page-container">
|
|
|
|
|
|
<div class="page-header">
|
|
|
|
|
|
<h1>用户管理</h1>
|
|
|
|
|
|
<p>管理系统用户和权限</p>
|
|
|
|
|
|
</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 :class="['badge', user.is_superuser ? 'badge-primary' : 'badge-neutral']">
|
|
|
|
|
|
{{ user.is_superuser ? '管理员' : '普通用户' }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td>{{ formatDateTime(user.created_at) }}</td>
|
|
|
|
|
|
<td>
|
|
|
|
|
|
<div class="action-buttons">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-if="user.id !== appState.user?.id"
|
|
|
|
|
|
:class="['btn-sm', user.is_active ? 'btn-warning' : 'btn-success']"
|
|
|
|
|
|
@click="toggleActive(user)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ user.is_active ? '禁用' : '启用' }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-if="user.id !== appState.user?.id"
|
|
|
|
|
|
:class="['btn-sm', user.is_superuser ? 'btn-warning' : 'btn-primary']"
|
|
|
|
|
|
@click="toggleAdmin(user)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ user.is_superuser ? '取消管理员' : '设为管理员' }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const MoldInsightView = {
|
|
|
|
|
|
setup() {
|
|
|
|
|
|
const router = useRouter();
|
2026-02-17 00:42:55 +08:00
|
|
|
|
const state = reactive({
|
|
|
|
|
|
selectedFile: null,
|
|
|
|
|
|
uploading: false,
|
|
|
|
|
|
error: "",
|
|
|
|
|
|
currentTask: null,
|
|
|
|
|
|
polling: false,
|
2026-03-03 23:57:04 +08:00
|
|
|
|
dragOver: false,
|
2026-03-04 00:47:41 +08:00
|
|
|
|
progress: 0,
|
|
|
|
|
|
history: null
|
2026-02-17 00:42:55 +08:00
|
|
|
|
});
|
2026-02-17 00:12:36 +08:00
|
|
|
|
|
2026-03-04 00:47:41 +08:00
|
|
|
|
const loadHistory = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
state.history = await apiRequest('/history');
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('加载历史记录失败:', e);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-02-17 00:12:36 +08:00
|
|
|
|
|
|
|
|
|
|
const handleFileChange = (event) => {
|
|
|
|
|
|
const file = event.target.files[0];
|
|
|
|
|
|
if (!file) return;
|
2026-02-17 00:42:55 +08:00
|
|
|
|
validateAndSelectFile(file);
|
|
|
|
|
|
};
|
2026-02-17 00:12:36 +08:00
|
|
|
|
|
2026-02-17 00:42:55 +08:00
|
|
|
|
const validateAndSelectFile = (file) => {
|
2026-03-04 00:47:41 +08:00
|
|
|
|
if (!file.name.toLowerCase().endsWith(".stp") && !file.name.toLowerCase().endsWith(".step")) {
|
2026-02-17 00:42:55 +08:00
|
|
|
|
state.error = "请选择 STP 或 STEP 格式文件";
|
|
|
|
|
|
state.selectedFile = null;
|
2026-02-17 00:12:36 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (file.size > 100 * 1024 * 1024) {
|
2026-02-17 00:42:55 +08:00
|
|
|
|
state.error = "文件大小不能超过 100MB";
|
|
|
|
|
|
state.selectedFile = null;
|
2026-02-17 00:12:36 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-02-17 00:42:55 +08:00
|
|
|
|
state.error = "";
|
|
|
|
|
|
state.selectedFile = file;
|
|
|
|
|
|
addNotification(`已选择文件: ${file.name}`, 'success');
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleDrop = (event) => {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
state.dragOver = false;
|
|
|
|
|
|
const files = event.dataTransfer.files;
|
2026-03-04 00:47:41 +08:00
|
|
|
|
if (files.length > 0) validateAndSelectFile(files[0]);
|
2026-02-17 00:12:36 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const uploadFile = async () => {
|
2026-02-17 00:42:55 +08:00
|
|
|
|
if (!state.selectedFile) return;
|
|
|
|
|
|
state.uploading = true;
|
|
|
|
|
|
state.error = "";
|
2026-03-03 23:57:04 +08:00
|
|
|
|
state.progress = 0;
|
2026-02-17 00:12:36 +08:00
|
|
|
|
|
|
|
|
|
|
const formData = new FormData();
|
2026-02-17 00:42:55 +08:00
|
|
|
|
formData.append("file", state.selectedFile);
|
2026-02-17 00:12:36 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch("/upload", {
|
|
|
|
|
|
method: "POST",
|
2026-03-04 00:47:41 +08:00
|
|
|
|
headers: appState.token ? { 'Authorization': `Bearer ${appState.token}` } : {},
|
|
|
|
|
|
body: formData
|
2026-02-17 00:12:36 +08:00
|
|
|
|
});
|
2026-03-04 00:47:41 +08:00
|
|
|
|
if (!res.ok) throw new Error(`上传失败: ${res.status}`);
|
2026-02-17 00:12:36 +08:00
|
|
|
|
const data = await res.json();
|
2026-03-04 00:47:41 +08:00
|
|
|
|
state.currentTask = { task_id: data.task_id, status: "processing", filename: data.file_info?.filename };
|
|
|
|
|
|
addNotification('文件上传成功,开始分析...', 'success');
|
2026-02-17 00:12:36 +08:00
|
|
|
|
startPolling(data.task_id);
|
|
|
|
|
|
} catch (e) {
|
2026-03-04 00:47:41 +08:00
|
|
|
|
state.error = handleApiError(e, '文件上传');
|
2026-02-17 00:12:36 +08:00
|
|
|
|
} finally {
|
2026-02-17 00:42:55 +08:00
|
|
|
|
state.uploading = false;
|
2026-02-17 00:12:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const startPolling = async (taskId) => {
|
2026-02-17 00:42:55 +08:00
|
|
|
|
state.polling = true;
|
2026-03-03 23:57:04 +08:00
|
|
|
|
state.progress = 10;
|
2026-02-17 00:47:39 +08:00
|
|
|
|
let pollCount = 0;
|
|
|
|
|
|
|
2026-02-17 00:12:36 +08:00
|
|
|
|
const poll = async () => {
|
|
|
|
|
|
try {
|
2026-02-17 00:47:39 +08:00
|
|
|
|
pollCount++;
|
2026-03-03 23:57:04 +08:00
|
|
|
|
state.progress = Math.min(90, 10 + pollCount * 0.5);
|
|
|
|
|
|
|
2026-03-04 00:47:41 +08:00
|
|
|
|
const task = await apiRequest(`/status/${taskId}`, { method: 'POST' });
|
2026-02-17 00:42:55 +08:00
|
|
|
|
state.currentTask = task;
|
2026-02-17 00:12:36 +08:00
|
|
|
|
|
|
|
|
|
|
if (task.status === "completed") {
|
2026-02-17 00:42:55 +08:00
|
|
|
|
state.polling = false;
|
2026-03-03 23:57:04 +08:00
|
|
|
|
state.progress = 100;
|
2026-03-04 00:47:41 +08:00
|
|
|
|
addNotification('分析完成', 'success');
|
|
|
|
|
|
router.push(`/moldinsight/result/${taskId}`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (task.status === "failed") {
|
2026-02-17 00:47:39 +08:00
|
|
|
|
state.polling = false;
|
2026-03-04 00:47:41 +08:00
|
|
|
|
state.error = task.error || "分析失败";
|
|
|
|
|
|
addNotification('分析失败', 'error');
|
|
|
|
|
|
return;
|
2026-02-17 00:12:36 +08:00
|
|
|
|
}
|
2026-03-04 00:47:41 +08:00
|
|
|
|
|
|
|
|
|
|
if (pollCount < 300) setTimeout(poll, 2000);
|
2026-02-17 00:12:36 +08:00
|
|
|
|
} catch (e) {
|
2026-02-17 00:42:55 +08:00
|
|
|
|
state.polling = false;
|
2026-03-04 00:47:41 +08:00
|
|
|
|
state.error = handleApiError(e, '轮询状态');
|
2026-02-17 00:12:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-04 00:47:41 +08:00
|
|
|
|
poll();
|
2026-02-17 00:42:55 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-17 00:12:36 +08:00
|
|
|
|
onMounted(() => {
|
2026-03-04 00:47:41 +08:00
|
|
|
|
if (!appState.user) {
|
|
|
|
|
|
router.push('/login');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
loadHistory();
|
2026-02-17 00:12:36 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-04 00:47:41 +08:00
|
|
|
|
return {
|
|
|
|
|
|
state,
|
|
|
|
|
|
handleFileChange,
|
|
|
|
|
|
handleDrop,
|
|
|
|
|
|
uploadFile,
|
|
|
|
|
|
formatFileSize,
|
|
|
|
|
|
formatDateTime
|
2026-02-17 00:12:36 +08:00
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
template: `
|
2026-03-04 00:47:41 +08:00
|
|
|
|
<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>
|
2026-03-03 23:57:04 +08:00
|
|
|
|
</div>
|
2026-03-04 00:47:41 +08:00
|
|
|
|
|
|
|
|
|
|
<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>
|
2026-03-03 23:57:04 +08:00
|
|
|
|
</div>
|
2026-03-04 00:47:41 +08:00
|
|
|
|
|
|
|
|
|
|
<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>
|
2026-03-03 23:57:04 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-04 00:47:41 +08:00
|
|
|
|
|
|
|
|
|
|
<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>
|
2026-02-17 00:12:36 +08:00
|
|
|
|
</div>
|
2026-03-03 23:57:04 +08:00
|
|
|
|
</div>
|
2026-02-17 00:12:36 +08:00
|
|
|
|
</div>
|
2026-03-04 00:47:41 +08:00
|
|
|
|
`
|
2026-02-17 00:12:36 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-04 00:47:41 +08:00
|
|
|
|
const ResultView = {
|
2026-02-17 00:12:36 +08:00
|
|
|
|
setup() {
|
2026-03-04 00:47:41 +08:00
|
|
|
|
const route = useRoute();
|
2026-02-17 00:12:36 +08:00
|
|
|
|
const router = useRouter();
|
2026-03-04 00:47:41 +08:00
|
|
|
|
const state = reactive({
|
|
|
|
|
|
task: null,
|
|
|
|
|
|
loading: true,
|
|
|
|
|
|
error: ''
|
|
|
|
|
|
});
|
2026-02-17 00:12:36 +08:00
|
|
|
|
|
2026-03-04 00:47:41 +08:00
|
|
|
|
const loadTask = async () => {
|
2026-02-17 00:12:36 +08:00
|
|
|
|
try {
|
2026-03-04 00:47:41 +08:00
|
|
|
|
state.task = await apiRequest(`/status/${route.params.taskId}`, { method: 'POST' });
|
2026-02-17 00:12:36 +08:00
|
|
|
|
} catch (e) {
|
2026-03-04 00:47:41 +08:00
|
|
|
|
state.error = handleApiError(e, '加载任务详情');
|
2026-02-17 00:12:36 +08:00
|
|
|
|
} finally {
|
2026-03-04 00:47:41 +08:00
|
|
|
|
state.loading = false;
|
2026-02-17 00:12:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-04 00:47:41 +08:00
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
if (!appState.user) {
|
|
|
|
|
|
router.push('/login');
|
|
|
|
|
|
return;
|
2026-02-17 00:12:36 +08:00
|
|
|
|
}
|
2026-03-04 00:47:41 +08:00
|
|
|
|
loadTask();
|
|
|
|
|
|
});
|
2026-02-17 00:12:36 +08:00
|
|
|
|
|
2026-03-04 00:47:41 +08:00
|
|
|
|
return { state, formatFileSize, formatDateTime, formatNumber };
|
2026-02-17 00:12:36 +08:00
|
|
|
|
},
|
|
|
|
|
|
template: `
|
2026-03-04 00:47:41 +08:00
|
|
|
|
<div class="page-container">
|
|
|
|
|
|
<div class="page-header">
|
|
|
|
|
|
<button class="btn-back" @click="$router.back()">← 返回</button>
|
|
|
|
|
|
<h1>分析结果</h1>
|
2026-03-04 00:10:05 +08:00
|
|
|
|
</div>
|
2026-03-04 00:47:41 +08:00
|
|
|
|
|
|
|
|
|
|
<div v-if="state.loading" class="loading-state">
|
|
|
|
|
|
<div class="spinner"></div>
|
|
|
|
|
|
<span>加载中...</span>
|
2026-02-17 00:12:36 +08:00
|
|
|
|
</div>
|
2026-03-04 00:47:41 +08:00
|
|
|
|
|
|
|
|
|
|
<div v-else-if="state.error" class="error-state">
|
|
|
|
|
|
<p>{{ state.error }}</p>
|
2026-02-17 00:12:36 +08:00
|
|
|
|
</div>
|
2026-03-04 00:47:41 +08:00
|
|
|
|
|
|
|
|
|
|
<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>
|
2026-02-17 00:12:36 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-04 00:47:41 +08:00
|
|
|
|
|
|
|
|
|
|
<div class="result-card" v-if="state.task.geometry_data">
|
|
|
|
|
|
<h3>几何数据</h3>
|
|
|
|
|
|
<div class="info-list">
|
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
|
<span class="info-label">顶点数</span>
|
|
|
|
|
|
<span class="info-value">{{ formatNumber(state.task.geometry_data.vertex_count) }}</span>
|
2026-02-17 00:12:36 +08:00
|
|
|
|
</div>
|
2026-03-04 00:47:41 +08:00
|
|
|
|
<div class="info-item">
|
|
|
|
|
|
<span class="info-label">面数</span>
|
|
|
|
|
|
<span class="info-value">{{ formatNumber(state.task.geometry_data.face_count) }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
|
<span class="info-label">边数</span>
|
|
|
|
|
|
<span class="info-value">{{ formatNumber(state.task.geometry_data.edge_count) }}</span>
|
2026-02-17 00:12:36 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-04 00:47:41 +08:00
|
|
|
|
</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>
|
2026-02-17 00:12:36 +08:00
|
|
|
|
</div>
|
2026-03-04 00:47:41 +08:00
|
|
|
|
</div>
|
2026-02-17 00:12:36 +08:00
|
|
|
|
</div>
|
2026-03-04 00:47:41 +08:00
|
|
|
|
`
|
2026-02-17 00:12:36 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-04 00:47:41 +08:00
|
|
|
|
const InventoryView = {
|
2026-02-17 00:12:36 +08:00
|
|
|
|
setup() {
|
2026-03-04 00:47:41 +08:00
|
|
|
|
const router = useRouter();
|
2026-02-17 00:12:36 +08:00
|
|
|
|
const route = useRoute();
|
2026-03-04 00:47:41 +08:00
|
|
|
|
const state = reactive({
|
|
|
|
|
|
activeTab: 'dashboard',
|
|
|
|
|
|
dashboard: null,
|
|
|
|
|
|
products: [],
|
|
|
|
|
|
suppliers: [],
|
|
|
|
|
|
customers: [],
|
|
|
|
|
|
warehouses: [],
|
|
|
|
|
|
inventory: [],
|
|
|
|
|
|
movements: [],
|
|
|
|
|
|
loading: false
|
|
|
|
|
|
});
|
2026-02-17 00:12:36 +08:00
|
|
|
|
|
2026-03-04 00:47:41 +08:00
|
|
|
|
const loadDashboard = async () => {
|
|
|
|
|
|
state.loading = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
state.dashboard = await apiRequest('/api/inventory/dashboard');
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
handleApiError(e, '加载仪表盘');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
state.loading = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-02-17 00:12:36 +08:00
|
|
|
|
|
2026-03-04 00:47:41 +08:00
|
|
|
|
const loadProducts = async () => {
|
|
|
|
|
|
state.loading = true;
|
2026-02-17 00:12:36 +08:00
|
|
|
|
try {
|
2026-03-04 00:47:41 +08:00
|
|
|
|
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');
|
2026-02-17 00:12:36 +08:00
|
|
|
|
} catch (e) {
|
2026-03-04 00:47:41 +08:00
|
|
|
|
handleApiError(e, '加载库存');
|
2026-02-17 00:12:36 +08:00
|
|
|
|
} finally {
|
2026-03-04 00:47:41 +08:00
|
|
|
|
state.loading = false;
|
2026-02-17 00:12:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-04 00:47:41 +08:00
|
|
|
|
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
|
2026-02-17 00:12:36 +08:00
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
template: `
|
2026-03-04 00:47:41 +08:00
|
|
|
|
<div class="page-container">
|
|
|
|
|
|
<div class="page-header">
|
|
|
|
|
|
<h1>进销存管理</h1>
|
|
|
|
|
|
<p>库存、采购、销售管理</p>
|
2026-02-17 00:12:36 +08:00
|
|
|
|
</div>
|
2026-03-04 00:47:41 +08:00
|
|
|
|
|
|
|
|
|
|
<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>
|
2026-03-04 00:10:05 +08:00
|
|
|
|
</div>
|
2026-03-04 00:47:41 +08:00
|
|
|
|
|
|
|
|
|
|
<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>
|
2026-03-03 23:57:04 +08:00
|
|
|
|
</div>
|
2026-02-17 00:12:36 +08:00
|
|
|
|
</div>
|
2026-03-04 00:47:41 +08:00
|
|
|
|
<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>
|
2026-02-17 00:12:36 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-04 00:47:41 +08:00
|
|
|
|
<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>
|
2026-03-03 23:57:04 +08:00
|
|
|
|
</div>
|
2026-02-17 00:12:36 +08:00
|
|
|
|
</div>
|
2026-03-04 00:47:41 +08:00
|
|
|
|
<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>
|
2026-02-17 00:12:36 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-04 00:47:41 +08:00
|
|
|
|
<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>
|
2026-03-04 00:10:05 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-04 00:47:41 +08:00
|
|
|
|
<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>
|
2026-02-17 00:12:36 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-04 00:10:05 +08:00
|
|
|
|
</div>
|
2026-03-04 00:47:41 +08:00
|
|
|
|
|
|
|
|
|
|
<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>
|
2026-02-17 00:12:36 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-04 00:47:41 +08:00
|
|
|
|
`
|
2026-02-17 00:12:36 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const routes = [
|
2026-03-04 00:47:41 +08:00
|
|
|
|
{ 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 }
|
2026-02-17 00:12:36 +08:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const router = createRouter({
|
|
|
|
|
|
history: createWebHistory(),
|
2026-03-04 00:47:41 +08:00
|
|
|
|
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();
|
2026-02-17 00:12:36 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-04 00:10:05 +08:00
|
|
|
|
const app = createApp(App);
|
|
|
|
|
|
app.use(router);
|
|
|
|
|
|
app.mount("#app");
|