/**
* 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: `
{{ notification.type === 'success' ? '✓' : notification.type === 'error' ? '✕' : notification.type === 'warning' ? '!' : 'i' }}
{{ notification.message }}
`,
};
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: `
`
};
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: `
📦
{{ state.stats?.inventory?.product_count || 0 }}
产品数量
📊
{{ state.stats?.inventory?.total_stock || 0 }}
库存总量
💰
{{ formatCurrency(state.stats?.inventory?.total_value || 0) }}
库存价值
◈
{{ state.stats?.health?.total_tasks || 0 }}
分析任务
🏭
{{ state.stats?.inventory?.supplier_count || 0 }}
供应商
👥
{{ state.stats?.inventory?.customer_count || 0 }}
客户
低库存预警
| SKU |
产品名称 |
当前库存 |
最低库存 |
| {{ item.sku }} |
{{ item.name }} |
{{ item.quantity }} |
{{ item.min_stock }} |
快速操作
`
};
const UsersView = {
setup() {
const router = useRouter();
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;
}
};
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: `
| 用户名 |
邮箱 |
姓名 |
状态 |
角色 |
注册时间 |
操作 |
| {{ user.username }} |
{{ user.email }} |
{{ user.full_name || '-' }} |
{{ user.is_active ? '正常' : '禁用' }}
|
{{ user.is_superuser ? '管理员' : '普通用户' }}
|
{{ formatDateTime(user.created_at) }} |
|
`
};
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('/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("/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(`/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: `
{{ state.selectedFile.name }}
{{ formatFileSize(state.selectedFile.size) }}
{{ state.error }}
最近分析
| 文件名 |
大小 |
状态 |
分析时间 |
操作 |
| {{ file.filename }} |
{{ formatFileSize(file.file_size) }} |
{{ file.status }}
|
{{ formatDateTime(file.created_at) }} |
|
`
};
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(`/status/${route.params.taskId}`, { method: 'POST' });
} 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: `
文件信息
文件大小
{{ formatFileSize(state.task.file_size) }}
分析时间
{{ formatDateTime(state.task.completed_at) }}
几何数据
顶点数
{{ formatNumber(state.task.geometry_data.vertex_count) }}
面数
{{ formatNumber(state.task.geometry_data.face_count) }}
边数
{{ formatNumber(state.task.geometry_data.edge_count) }}
3D 预览
`
};
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: `
📦
{{ state.dashboard?.product_count || 0 }}
产品数量
📊
{{ state.dashboard?.total_stock || 0 }}
库存总量
💰
{{ formatCurrency(state.dashboard?.total_value || 0) }}
库存价值
🏭
{{ state.dashboard?.supplier_count || 0 }}
供应商
👥
{{ state.dashboard?.customer_count || 0 }}
客户
🏪
{{ state.dashboard?.warehouse_count || 0 }}
仓库
| SKU |
名称 |
分类 |
单位 |
成本价 |
销售价 |
| {{ product.sku }} |
{{ product.name }} |
{{ product.category || '-' }} |
{{ product.unit }} |
{{ formatCurrency(product.cost_price) }} |
{{ formatCurrency(product.sale_price) }} |
| SKU |
产品 |
仓库 |
数量 |
可用 |
| {{ item.product_sku }} |
{{ item.product_name }} |
{{ item.warehouse_name }} |
{{ item.quantity }} |
{{ item.available_quantity }} |
| 编码 |
名称 |
联系人 |
电话 |
邮箱 |
| {{ supplier.code }} |
{{ supplier.name }} |
{{ supplier.contact_person || '-' }} |
{{ supplier.phone || '-' }} |
{{ supplier.email || '-' }} |
| 编码 |
名称 |
联系人 |
电话 |
邮箱 |
| {{ customer.code }} |
{{ customer.name }} |
{{ customer.contact_person || '-' }} |
{{ customer.phone || '-' }} |
{{ customer.email || '-' }} |
| 产品 |
类型 |
数量 |
变动前 |
变动后 |
时间 |
| {{ movement.product_name }} |
{{ movement.movement_type === 'in' ? '入库' : movement.movement_type === 'out' ? '出库' : '调整' }}
|
{{ movement.quantity }} |
{{ movement.before_quantity }} |
{{ movement.after_quantity }} |
{{ formatDateTime(movement.created_at) }} |
`
};
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");