/** * 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 { const date = new Date(dateString); if (isNaN(date.getTime())) return dateString; const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0'); return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`; } 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; const notificationDedup = new Map(); function addNotification(message, type = 'info') { const dedupKey = `${type}:${message}`; const now = Date.now(); const lastAt = notificationDedup.get(dedupKey) || 0; if (now - lastAt < 2500) return; notificationDedup.set(dedupKey, now); 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 parseErrorMessage(response) { try { const contentType = response.headers.get('content-type') || ''; if (contentType.includes('application/json')) { const body = await response.json().catch(() => null); if (body?.detail) { if (Array.isArray(body.detail)) { const lines = body.detail .map((e) => { const loc = Array.isArray(e?.loc) ? e.loc.join('.') : ''; const msg = e?.msg ? String(e.msg) : '校验失败'; return loc ? `${loc}: ${msg}` : msg; }) .filter(Boolean); return lines.length ? lines.join('\n') : '请求校验失败'; } if (typeof body.detail === 'object') return JSON.stringify(body.detail); return String(body.detail); } if (body?.message) return String(body.message); return '请求失败'; } const text = await response.text().catch(() => ''); const normalized = (text || '').trim(); if (!normalized) return '请求失败'; return normalized.length > 200 ? normalized.slice(0, 200) + '...' : normalized; } catch { return '请求失败'; } } 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 message = await parseErrorMessage(response); throw new Error(message); } 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: '/inventory', label: '进销存', icon: '⊞' }, { path: '/moldinsight', label: 'MoldInsight', icon: '◈' } ]; if (appState.user?.is_superuser) { items.push({ path: '/users', label: '用户管理', icon: '👤' }); } return items; }); const isActive = (path) => { if (path === '/') return route.path === '/'; return route.path.startsWith(path); }; const handleLogout = async () => { try { await apiRequest('/api/auth/logout', { method: 'POST' }); } catch {} clearAuth(); addNotification('已退出登录', 'success'); router.push('/login'); }; onMounted(() => { initAuth(); }); const getPriorityText = (priority) => { const priorityMap = { 'critical': '紧急', 'high': '高', 'medium': '中', 'low': '低' }; return priorityMap[priority] || priority; }; return { route, router, appState, menuItems, isActive, handleLogout, getPriorityText, dismissNotification: (id) => { const index = appState.notifications.findIndex(n => n.id === id); if (index > -1) { appState.notifications[index].visible = false; setTimeout(() => { const idx = appState.notifications.findIndex(n => n.id === id); if (idx > -1) appState.notifications.splice(idx, 1); }, 300); } } }; }, template: `
{{ notification.type === 'success' ? '✓' : notification.type === 'error' ? '✕' : notification.type === 'warning' ? '!' : 'i' }}
{{ notification.message }}
G
Gemold
模具制造管理系统
{{ item.label }}
粤ICP备2025386132号-1
`, }; const LoginView = { setup() { const router = useRouter(); const state = reactive({ username: '', password: '', loading: false, error: '', backendDbReady: true, backendMessage: '' }); onMounted(() => { if (appState.user) { router.push('/'); return; } fetch('/health') .then(r => r.ok ? r.json().catch(() => null) : null) .then(h => { if (h && h.database_connected === false) { state.backendDbReady = false; state.backendMessage = '检测到数据库状态异常。你仍可直接尝试登录;若失败请检查当前服务实例与数据库连接。'; } }) .catch(() => { state.backendDbReady = false; state.backendMessage = '健康检查请求失败。你仍可直接尝试登录;若失败请检查后端地址与网络。'; }); }); const enterDemoMode = () => { saveAuth('demo', { id: 0, username: 'demo', full_name: '演示用户', is_superuser: false }); addNotification('已进入演示模式', 'success'); router.push('/inventory'); }; 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 message = await parseErrorMessage(res); throw new Error(message || '登录失败'); } 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, enterDemoMode }; }, template: `
G

欢迎回来

登录到 Gemold 系统

提示
{{ state.backendMessage }}
{{ state.error }}

如需开通账号,请联系管理员

` }; const HomeView = { setup() { const router = useRouter(); const state = reactive({ stats: null, loading: true, backendDbReady: true, aluminumPrice: null, aluminumHistory: [], aluminumLoading: true }); const loadStats = async () => { try { const health = await apiRequest('/health').catch(() => null); state.backendDbReady = health?.database_connected !== false; const inventoryStats = state.backendDbReady ? await apiRequest('/api/dashboard').catch(() => null) : null; state.stats = { inventory: inventoryStats, health }; } catch (e) { handleApiError(e, '加载统计数据'); } finally { state.loading = false; } }; const loadAluminumPrice = async () => { try { const [current, history] = await Promise.all([ apiRequest('/api/aluminum-price/current').catch(() => null), apiRequest('/api/aluminum-price/history?days=30').catch(() => null) ]); state.aluminumPrice = current; state.aluminumHistory = history || []; } catch (e) { console.error('铝价数据加载失败:', e); } finally { state.aluminumLoading = false; nextTick(() => { renderAluminumChart(); }); } }; let chartInstance = null; const renderAluminumChart = () => { const canvas = document.getElementById('aluminumChart'); if (!canvas || !state.aluminumHistory.length) return; if (chartInstance) chartInstance.destroy(); const ctx = canvas.getContext('2d'); const labels = state.aluminumHistory.map(d => d.date.slice(5)); const prices = state.aluminumHistory.map(d => d.close); const gradient = ctx.createLinearGradient(0, 0, 0, 280); gradient.addColorStop(0, 'rgba(59, 130, 246, 0.35)'); gradient.addColorStop(1, 'rgba(59, 130, 246, 0.02)'); chartInstance = new Chart(ctx, { type: 'line', data: { labels, datasets: [{ label: '铝价 (元/吨)', data: prices, borderColor: '#3b82f6', backgroundColor: gradient, borderWidth: 2, fill: true, tension: 0.3, pointRadius: 0, pointHoverRadius: 5, pointHoverBackgroundColor: '#3b82f6', }] }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, plugins: { legend: { display: false }, tooltip: { backgroundColor: 'rgba(23, 23, 23, 0.9)', titleFont: { size: 12 }, bodyFont: { size: 13 }, padding: 10, cornerRadius: 8, displayColors: false, callbacks: { label: ctx => '¥' + ctx.parsed.y.toLocaleString() + ' 元/吨' } } }, scales: { x: { grid: { display: false }, ticks: { color: '#a3a3a3', font: { size: 10 }, maxTicksLimit: 8 } }, y: { grid: { color: 'rgba(0,0,0,0.05)' }, ticks: { color: '#a3a3a3', font: { size: 10 }, callback: v => v.toLocaleString() } } } } }); }; onMounted(() => { if (!appState.user) { router.push('/login'); return; } loadStats(); loadAluminumPrice(); }); return { state, formatNumber, formatCurrency, appState, loadAluminumPrice }; }, template: `

欢迎回来,{{ appState.user?.full_name || appState.user?.username }}

系统概览

正在加载数据...

📦
{{ 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 }}
客户
🪨 铝金属实时行情
数据来源: SHFE模拟

加载铝价数据...

¥{{ state.aluminumPrice.price?.toLocaleString() }}
{{ state.aluminumPrice.unit }}
{{ state.aluminumPrice.change >= 0 ? '▲' : '▼' }} {{ Math.abs(state.aluminumPrice.change)?.toLocaleString() }} ({{ state.aluminumPrice.change_percent >= 0 ? '+' : '' }}{{ state.aluminumPrice.change_percent }}%)
开盘价 ¥{{ state.aluminumPrice.open?.toLocaleString() }}
最高价 ¥{{ state.aluminumPrice.high?.toLocaleString() }}
最低价 ¥{{ state.aluminumPrice.low?.toLocaleString() }}
昨收价 ¥{{ state.aluminumPrice.prev_close?.toLocaleString() }}
近30日价格走势

铝价数据暂不可用

低库存预警

SKU 产品名称 当前库存 最低库存
{{ item.sku }} {{ item.name }} {{ item.quantity }} {{ item.min_stock }}

快速操作

` }; 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: `

用户管理

管理系统用户和权限

加载中...
用户名 邮箱 姓名 状态 角色 注册时间 操作
{{ user.username }} {{ user.email }} {{ user.full_name || '-' }} {{ user.is_active ? '正常' : '禁用' }} {{ role }} {{ formatDateTime(user.created_at) }}

{{ state.editingUser ? '编辑用户' : '添加用户' }}

` }; const MoldInsightView = { setup() { const router = useRouter(); const state = reactive({ selectedFile: null, selectedMaterial: 'ABS', moldParams: { draftAngle: 2.0, shrinkageRate: 0.5, partingPrecision: 0.1, cavityMatch: 95 }, uploading: false, error: "", currentTask: null, polling: false, dragOver: false, progress: 0, history: null, expandedFiles: {} }); const loadHistory = async () => { try { state.history = await apiRequest('/api/history'); } catch (e) { console.error('加载历史记录失败:', e); } }; const toggleFileHistory = async (filename) => { if (state.expandedFiles[filename]) { state.expandedFiles[filename] = null; } else { try { const records = await apiRequest(`/api/history/${encodeURIComponent(filename)}`); state.expandedFiles[filename] = records; } catch (e) { handleApiError(e, '加载文件历史'); } } }; const viewResult = (record) => { router.push(`/moldinsight/result/${record.task_id}`); }; const handleFileChange = (event) => { const file = event.target.files[0]; if (!file) return; validateAndSelectFile(file); }; const validateAndSelectFile = (file) => { if (!file.name.toLowerCase().endsWith(".stp") && !file.name.toLowerCase().endsWith(".step")) { state.error = "请选择 STP 或 STEP 格式文件"; state.selectedFile = null; return; } if (file.size > 100 * 1024 * 1024) { state.error = "文件大小不能超过 100MB"; state.selectedFile = null; return; } state.error = ""; state.selectedFile = file; addNotification(`已选择文件: ${file.name}`, 'success'); }; const handleDrop = (event) => { event.preventDefault(); state.dragOver = false; const files = event.dataTransfer.files; if (files.length > 0) validateAndSelectFile(files[0]); }; const uploadFile = async () => { if (!state.selectedFile) return; if (!appState.token) { state.error = '请先登录后再上传文件'; addNotification('请先登录', 'warning'); router.push('/login'); return; } if (appState.token === 'demo') { state.error = '演示模式不支持文件上传,请使用完整账户登录'; addNotification('演示模式不支持上传', 'warning'); return; } state.uploading = true; state.error = ""; state.progress = 0; const formData = new FormData(); formData.append("file", state.selectedFile); formData.append("material", state.selectedMaterial); formData.append("draft_angle", String(state.moldParams.draftAngle)); formData.append("shrinkage_rate", String(state.moldParams.shrinkageRate)); formData.append("parting_precision", String(state.moldParams.partingPrecision)); formData.append("cavity_match", String(state.moldParams.cavityMatch)); try { const res = await fetch("/api/upload", { method: "POST", headers: { 'Authorization': `Bearer ${appState.token}` }, body: formData }); if (res.status === 401) { clearAuth(); state.error = '登录已过期,请重新登录'; addNotification('登录已过期,请重新登录', 'warning'); router.push('/login'); return; } 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) => { console.log('[Polling] 开始轮询任务:', 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; state.task = task; if (task.status === "completed") { state.polling = false; state.progress = 100; addNotification('分析完成', 'success'); loadHistory(); router.push(`/moldinsight/result/${taskId}`); return; } if (task.status === "failed") { state.polling = false; state.error = task.error || "分析失败"; addNotification('分析失败', 'error'); return; } if (pollCount < 300) setTimeout(poll, 2000); } catch (e) { state.polling = false; state.error = handleApiError(e, '轮询状态'); } }; poll(); }; onMounted(() => { if (!appState.user) { router.push('/login'); return; } loadHistory(); }); const moldParamFields = [ { key: 'draftAngle', label: '拔模角', unit: '°', min: 1, max: 10, step: 0.5, defaultValue: 2.0, hint: '常规注塑件建议从 1.5° 到 3° 起步' }, { key: 'shrinkageRate', label: '收缩率', unit: '%', min: 0.5, max: 3.0, step: 0.1, defaultValue: 0.5, hint: '按材料牌号校核,默认值用于首轮方案评估' }, { key: 'partingPrecision', label: '分型精度', unit: 'mm', min: 0.01, max: 1.0, step: 0.01, defaultValue: 0.1, hint: '用于控制分型面拟合与边界容差' }, { key: 'cavityMatch', label: '型腔匹配度', unit: '%', min: 80, max: 100, step: 1, defaultValue: 95, hint: '数值越高越偏向紧配合与严格封合' } ]; return { state, handleFileChange, handleDrop, uploadFile, formatFileSize, formatDateTime, formatNumber, moldParamFields, toggleFileHistory, viewResult }; }, template: `

注塑模 STP 分析

上传 STEP/STP 产品件,完成自动分模、工程建议与导出

输入
STEP/STP 产品件,面向注塑模主流程
输出
分模方案、DFM 风险、注塑模系统摘要与 CAD 导出
目标
先确认推荐方案,再进入导出与 CAM 准备

1. 上传产品件

📁
点击选择或拖拽 STP/STEP 文件 支持注塑模产品件分析,最大 100MB
{{ state.selectedFile.name }} {{ formatFileSize(state.selectedFile.size) }}
{{ state.error }}

2. 注塑模参数

高级工艺参数
默认值适用于多数注塑件;仅在已知工艺约束时再调整。
{{ field.unit }}
默认 {{ field.defaultValue }}{{ field.unit }} 范围 {{ field.min }} - {{ field.max }}{{ field.unit }}
{{ field.hint }}
先选择 STP 文件,再填写材料并开始分析。
分析历史({{ state.history.files.length }} 个文件)
文件名 上传次数 文件大小 最新状态 最新分析时间 操作
` }; const ResultView = { setup() { const route = useRoute(); const router = useRouter(); const state = reactive({ task: null, loading: true, error: '', selectedSchemeId: null, previewStatus: 'idle', camLoading: false, camError: '', camPlan: null, camForm: { mold_steel: 'P20', surface_quality: 'standard', controller: 'fanuc', include_gcode: false } }); const camSteelOptions = [ { value: 'P20', label: 'P20' }, { value: '718H', label: '718H' }, { value: 'NAK80', label: 'NAK80' }, { value: 'S136', label: 'S136' }, { value: 'H13', label: 'H13' } ]; const camSurfaceOptions = [ { value: 'standard', label: '标准' }, { value: 'fine', label: '精细' }, { value: 'mirror', label: '镜面' } ]; const camControllerOptions = [ { value: 'fanuc', label: 'Fanuc' }, { value: 'siemens', label: 'Siemens' } ]; const loadTask = async () => { console.log('[ResultView] loadTask 开始, taskId:', route.params.taskId); try { state.task = await apiRequest(`/api/status/${route.params.taskId}`, { method: 'POST' }); console.log('任务数据:', state.task); console.log('key_info:', state.task.key_info); console.log('cavity_data:', state.task.cavity_data); console.log('analysis_result:', state.task.analysis_result); console.log('geometry_data:', state.task.geometry_data); if (state.task?.best_scheme_id) { state.selectedSchemeId = state.task.best_scheme_id; } else if (state.task?.candidate_schemes?.length) { state.selectedSchemeId = state.task.candidate_schemes[0].scheme_id; } const prefs = state.task?.cam_preferences || {}; state.camForm.mold_steel = prefs.mold_steel || 'P20'; state.camForm.surface_quality = prefs.surface_quality || 'standard'; state.camForm.controller = prefs.controller || 'fanuc'; state.camForm.include_gcode = Boolean(prefs.include_gcode || false); state.previewStatus = 'loading'; } catch (e) { state.error = handleApiError(e, '加载任务详情'); } finally { state.loading = false; } }; onMounted(() => { if (!appState.user) { router.push('/login'); return; } loadTask(); }); const getPriorityText = (priority) => { const priorityMap = { 'critical': '紧急', 'high': '高', 'medium': '中', 'low': '低' }; return priorityMap[priority] || priority; }; const candidateSchemes = computed(() => state.task?.candidate_schemes || []); const hasSingleScheme = computed(() => candidateSchemes.value.length === 1); const selectedScheme = computed(() => { if (!candidateSchemes.value.length) return null; return candidateSchemes.value.find(s => s.scheme_id === state.selectedSchemeId) || candidateSchemes.value[0]; }); const selectedCavityData = computed(() => selectedScheme.value?.cavity_data || state.task?.cavity_data || null); const selectedKeyInfo = computed(() => selectedScheme.value?.key_info || state.task?.key_info || null); const selectedHtmlFile = computed(() => selectedScheme.value?.html_file || state.task?.html_file || ''); const selectedDfmViolations = computed(() => selectedScheme.value?.dfm_violations || []); const selectedInjectionSystem = computed(() => selectedCavityData.value?.injection_system || state.task?.plan_result?.injection_system || null); const selectedSideActions = computed(() => { return selectedScheme.value?.side_actions || selectedCavityData.value?.side_actions || state.task?.side_actions || null; }); const parseEmbeddedSideActionAi = (report) => { const source = String(report || ''); const match = source.match(/\s*([\s\S]*?)\s*/); if (!match) return null; try { return JSON.parse(match[1]); } catch (error) { console.warn('AI 倒扣分析解析失败:', error); return null; } }; const stripEmbeddedSideActionAi = (report) => String(report || '') .replace(/\s*[\s\S]*?\s*/, '') .trim(); const normalizeSideActionAiAdvice = (raw, source = 'ai') => { if (!raw || typeof raw !== 'object') return null; const status = ['required', 'not_required', 'manual_review'].includes(raw.status) ? raw.status : 'manual_review'; const mechanism = ['slider', 'lifter', 'mixed', 'none', 'manual_review'].includes(raw.mechanism_recommendation) ? raw.mechanism_recommendation : 'manual_review'; const confidence = Number(raw.confidence); const statusLabelMap = { required: '需要倒扣/抽芯', not_required: '无需倒扣/抽芯', manual_review: '需人工确认' }; const mechanismLabelMap = { slider: '优先滑块', lifter: '优先斜顶', mixed: '滑块 + 斜顶', none: '无需侧向机构', manual_review: '人工评审' }; return { source, status, statusLabel: statusLabelMap[status], mechanismRecommendation: mechanism, mechanismLabel: mechanismLabelMap[mechanism], confidence: Number.isFinite(confidence) ? Math.max(0, Math.min(confidence, 1)) : null, conclusion: raw.conclusion || statusLabelMap[status], summary: raw.summary || '', reasons: Array.isArray(raw.reasons) ? raw.reasons.filter(Boolean).slice(0, 5) : [], standardAdvice: Array.isArray(raw.standard_advice) ? raw.standard_advice.filter(Boolean).slice(0, 5) : [], manualReviewItems: Array.isArray(raw.manual_review_items) ? raw.manual_review_items.filter(Boolean).slice(0, 4) : [] }; }; const fallbackSideActionAiAdvice = computed(() => { const sideActions = selectedSideActions.value; if (!sideActions || typeof sideActions !== 'object') return null; const summary = sideActions.summary || {}; const sliderCount = (sideActions.slider_mechanisms || []).length; const lifterCount = (sideActions.lifter_mechanisms || []).length; const totalCount = sliderCount + lifterCount; const mechanismRecommendation = sliderCount && lifterCount ? 'mixed' : sliderCount ? 'slider' : lifterCount ? 'lifter' : 'none'; const status = totalCount > 0 ? 'required' : summary.total_undercut_faces === 0 ? 'not_required' : 'manual_review'; return normalizeSideActionAiAdvice({ status, confidence: 0.55, conclusion: status === 'required' ? '规则分析判断当前产品需要倒扣/抽芯机构' : status === 'not_required' ? '规则分析未发现必须采用倒扣/抽芯机构的特征' : '当前规则结果不足以完成稳定判断,建议人工复核', mechanism_recommendation: mechanismRecommendation === 'none' ? 'none' : mechanismRecommendation, summary: status === 'required' ? `检测到 ${sliderCount} 个滑块需求、${lifterCount} 个斜顶需求,建议先按标准机构路线做结构评审。` : status === 'not_required' ? '当前方案未发现明显倒扣特征,可优先按常规模具结构推进。' : '现有数据可用于初筛,但不足以替代工程师对复杂倒扣的最终确认。', reasons: [ totalCount > 0 ? `规则分析识别出 ${totalCount} 处侧向机构需求` : '规则分析未识别出明确侧向机构需求', summary.complexity ? `当前复杂度判定为 ${summary.complexity}` : '当前复杂度信息不足', '当前倒扣方案若存在大行程滑块,优先按气动抽芯条件评估' ].filter(Boolean), standard_advice: (sideActions.recommendations || []).slice(0, 4), manual_review_items: [ '结合 3D 预览复核倒扣是否可通过改产品取消', '确认侧向机构与顶出、冷却、分型面是否存在干涉' ] }, 'rules'); }); const dfmLevelSummary = computed(() => { const base = { critical: 0, high: 0, medium: 0, low: 0 }; (selectedDfmViolations.value || []).forEach((item) => { const level = String(item.level || 'medium').toLowerCase(); if (base[level] === undefined) { base.medium += 1; } else { base[level] += 1; } }); return base; }); const sideActionAiAdvice = computed(() => { return normalizeSideActionAiAdvice( parseEmbeddedSideActionAi(state.task?.llm_report || ''), 'ai' ) || fallbackSideActionAiAdvice.value; }); const cleanedLlmReport = computed(() => stripEmbeddedSideActionAi(state.task?.llm_report || '')); const hasVisibleLlmReport = computed(() => Boolean(cleanedLlmReport.value)); const llmReportHtml = computed(() => renderMarkdownToHtml(cleanedLlmReport.value)); const stageTimingEntries = computed(() => { const timings = state.task?.stage_timings || {}; return Object.entries(timings) .filter(([, value]) => typeof value === 'number') .sort((a, b) => b[1] - a[1]); }); const sortedRecommendations = computed(() => { const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 }; return [...(state.task?.analysis_result?.design_recommendations || [])].sort((a, b) => { return (priorityOrder[a.priority] ?? 99) - (priorityOrder[b.priority] ?? 99); }); }); const primaryRecommendation = computed(() => sortedRecommendations.value[0] || null); const selectedDfmCount = computed(() => selectedDfmViolations.value?.length || 0); const selectedCavityCount = computed(() => { const cavityValue = selectedCavityData.value?.mold_cavities?.cavity_count; if (typeof cavityValue === 'number') return cavityValue; const cavityObj = selectedCavityData.value?.mold_cavities || selectedKeyInfo.value?.mold_cavities || {}; return cavityObj.cavity_count || Object.keys(cavityObj).filter(key => key.startsWith('cavity_')).length || 1; }); const selectedRiskLabel = computed(() => { if (!selectedDfmCount.value) return '低风险'; if (selectedDfmCount.value >= 4) return '高风险'; if (selectedDfmCount.value >= 2) return '中风险'; return '低风险'; }); const resultAnchorLinks = [ { id: 'candidate-schemes', label: '方案' }, { id: 'preview-3d', label: '预览' }, { id: 'ai-side-action', label: 'AI倒扣分析' }, { id: 'export-cam', label: 'CAM/CNC' }, { id: 'llm-report', label: '设计报告' } ]; const selectScheme = (schemeId) => { state.selectedSchemeId = schemeId; state.previewStatus = 'loading'; state.camPlan = null; state.camError = ''; }; const onPreviewLoad = () => { state.previewStatus = 'loaded'; }; const onPreviewError = () => { state.previewStatus = 'error'; }; const formatSchemeDirection = (scheme) => { if (!scheme) return 'N/A'; if (scheme.parting?.axis) return `${scheme.parting.axis} 轴`; const dir = scheme.parting?.direction; if (!Array.isArray(dir)) return 'N/A'; return `[${dir.map(v => Number(v).toFixed(2)).join(', ')}]`; }; const analysisFeatures = computed(() => state.task?.analysis_result?.detected_features || []); const getFeatureByTypes = (...types) => analysisFeatures.value.find(f => types.includes(f.feature_type)); const countFeaturesByTypes = (...types) => analysisFeatures.value.filter(f => types.includes(f.feature_type)).length; const getWallThicknessSummary = () => { const feature = getFeatureByTypes('thin_wall', 'thick_wall', 'wall_non_uniform'); if (!feature) return '待分析'; const params = feature.parameters || {}; if (params.min_thickness != null && params.max_thickness != null) { return `${Number(params.min_thickness).toFixed(2)} - ${Number(params.max_thickness).toFixed(2)} mm`; } if (params.average_thickness != null) { return `平均 ${Number(params.average_thickness).toFixed(2)} mm`; } return feature.recommendations?.[0] || '已完成分析'; }; const getUndercutCount = () => { const fromScheme = selectedScheme.value?.undercut_regions?.length || selectedCavityData.value?.undercut_regions?.length || 0; return fromScheme || countFeaturesByTypes('undercut'); }; const formatStageName = (name) => { const stageNameMap = { parse_stp: 'STP解析', generate_mesh: '网格生成', generate_cavity: '分模与型腔生成', build_plan_result: '方案结果组装', persist_artifacts: '结果持久化', analyze_design: '几何分析', verify_geometry: '几何验证', generate_llm_report: 'LLM报告生成' }; return stageNameMap[name] || name; }; const formatTiming = (value) => `${Number(value || 0).toFixed(3)} s`; const escapeHtml = (value) => String(value || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); const applyInlineMarkdown = (text) => { let html = escapeHtml(text); html = html.replace(/`([^`]+)`/g, '$1'); html = html.replace(/\*\*([^*]+)\*\*/g, '$1'); html = html.replace(/\*([^*]+)\*/g, '$1'); html = html.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '$1'); return html; }; const renderMarkdownToHtml = (markdown) => { const source = String(markdown || '').replace(/\r\n/g, '\n').trim(); if (!source) return ''; const lines = source.split('\n'); const html = []; let inList = false; let inCode = false; let codeBuffer = []; const closeList = () => { if (inList) { html.push(''); inList = false; } }; const closeCode = () => { if (inCode) { html.push(`
${escapeHtml(codeBuffer.join('\n'))}
`); inCode = false; codeBuffer = []; } }; lines.forEach((rawLine) => { const line = rawLine.trimEnd(); if (line.startsWith('```')) { closeList(); if (inCode) { closeCode(); } else { inCode = true; codeBuffer = []; } return; } if (inCode) { codeBuffer.push(rawLine); return; } const trimmed = line.trim(); if (!trimmed) { closeList(); html.push(''); return; } const headingMatch = trimmed.match(/^(#{1,6})\s+(.*)$/); if (headingMatch) { closeList(); const level = headingMatch[1].length; html.push(`${applyInlineMarkdown(headingMatch[2])}`); return; } if (/^[-*]\s+/.test(trimmed)) { if (!inList) { html.push('
    '); inList = true; } html.push(`
  • ${applyInlineMarkdown(trimmed.replace(/^[-*]\s+/, ''))}
  • `); return; } if (/^\d+\.\s+/.test(trimmed)) { closeList(); html.push(`

    ${applyInlineMarkdown(trimmed)}

    `); return; } if (/^>\s?/.test(trimmed)) { closeList(); html.push(`
    ${applyInlineMarkdown(trimmed.replace(/^>\s?/, ''))}
    `); return; } closeList(); html.push(`

    ${applyInlineMarkdown(trimmed)}

    `); }); closeList(); closeCode(); return html.filter(Boolean).join('\n'); }; const exportCAD = async (format) => { try { const taskId = route.params.taskId; const result = await apiRequest('/api/export-mold', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ task_id: taskId, scheme_id: selectedScheme.value?.scheme_id || state.selectedSchemeId, formats: [format], components: ['cavity', 'core', 'parting_surface'] }) }); if (result.status === 'success' && result.data.files) { for (const file of result.data.files) { const relativePath = file.relative_path || (file.filepath ? file.filepath.replace(/\\/g, '/').split('/').slice(-2).join('/') : ''); const downloadUrl = file.download_path || `/api/export-download/${encodeURI(relativePath)}?task_id=${encodeURIComponent(taskId)}`; await downloadWithAuth(downloadUrl, file.filename); } addNotification(`已导出 ${result.data.files.length} 个 ${format.toUpperCase()} 文件`, 'success'); } else if (result.data.errors && result.data.errors.length > 0) { addNotification(`导出失败: ${result.data.errors[0]}`, 'error'); } } catch (e) { addNotification(`导出失败: ${e.message}`, 'error'); } }; async function downloadWithAuth(url, filename) { const headers = {}; if (appState.token) { headers['Authorization'] = `Bearer ${appState.token}`; } const response = await fetch(url, { headers }); if (!response.ok) { throw new Error(`下载失败 (${response.status}): 无法下载,需要授权`); } const blob = await response.blob(); const blobUrl = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = blobUrl; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(blobUrl); } const generateCamPlan = async () => { try { state.camLoading = true; state.camError = ''; const taskId = route.params.taskId; const payload = { task_id: taskId, scheme_id: selectedScheme.value?.scheme_id, mold_steel: state.camForm.mold_steel, surface_quality: state.camForm.surface_quality, controller: state.camForm.controller, include_gcode: state.camForm.include_gcode }; const result = await apiRequest('/api/cam/plan', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); state.camPlan = result?.data || null; if (result?.cam_preferences) { state.camForm.mold_steel = result.cam_preferences.mold_steel || state.camForm.mold_steel; state.camForm.surface_quality = result.cam_preferences.surface_quality || state.camForm.surface_quality; state.camForm.controller = result.cam_preferences.controller || state.camForm.controller; state.camForm.include_gcode = Boolean(result.cam_preferences.include_gcode); } addNotification('CAM 计划生成完成', 'success'); } catch (e) { state.camPlan = null; state.camError = e.message || 'CAM 计划生成失败'; addNotification(`CAM 计划生成失败: ${state.camError}`, 'error'); } finally { state.camLoading = false; } }; return { state, candidateSchemes, hasSingleScheme, selectedScheme, selectedCavityData, selectedKeyInfo, selectedHtmlFile, selectedDfmViolations, selectedInjectionSystem, sideActionAiAdvice, hasVisibleLlmReport, dfmLevelSummary, llmReportHtml, stageTimingEntries, getWallThicknessSummary, countFeaturesByTypes, getUndercutCount, formatStageName, formatTiming, primaryRecommendation, sortedRecommendations, selectedDfmCount, selectedCavityCount, selectedRiskLabel, resultAnchorLinks, formatFileSize, formatDateTime, formatNumber, getPriorityText, exportCAD, generateCamPlan, camSteelOptions, camSurfaceOptions, camControllerOptions, selectScheme, onPreviewLoad, onPreviewError, formatSchemeDirection }; }, template: `

    分析结果

    加载中...

    {{ state.error }}

    {{ state.task.filename }}

    {{ state.task.status }}
    {{ anchor.label }}

    推荐方案

    {{ selectedScheme?.title || selectedScheme?.scheme_id || '方案待定' }}
    方案说明 {{ selectedScheme?.summary || selectedScheme?.reason || '基于当前几何与制造约束自动推荐' }}
    分型方向 {{ formatSchemeDirection(selectedScheme) }}
    模具结构 {{ selectedScheme?.mold_structure_type === 'two_half_cavity' ? '两板半腔(无独立模芯)' : '型腔 + 模芯' }}
    分型面位置 {{ selectedScheme?.offset_label || '中面' }}
    型腔数 {{ selectedCavityCount }} 腔
    预计成型周期 {{ selectedInjectionSystem?.overall_assessment?.estimated_cycle_time || 'N/A' }} s

    备选方案

    {{ candidateSchemes.length - 1 }} 个备选
    点击方案名称可切换查看详情与 3D 预览

    方案概览

    单一方案
    详细方案说明 {{ selectedScheme?.summary || selectedScheme?.reason || '当前仅生成 1 套可用分模方案,已在推荐方案区域展示核心参数。' }}

    任务信息

    文件大小 {{ formatFileSize(state.task.file_size) }}
    分析时间 {{ formatDateTime(state.task.completed_at) }}
    状态 {{ state.task.status }}

    几何概览

    顶点数 {{ formatNumber(state.task.mesh_summary.vertex_count) }}
    面数 {{ formatNumber(state.task.mesh_summary.face_count) }}
    边数 {{ formatNumber(state.task.geometry_data?.topology?.edge_count || state.task.geometry_data?.topology?.edges || state.task.mesh_summary?.edge_count || 'N/A') }}
    体积 {{ formatNumber(state.task.geometry_data.volume) }} mm³
    表面积 {{ formatNumber(state.task.geometry_data.surface_area) }} mm²

    3D 预览

    重点区域
    预览加载失败
    HTML 已生成但加载异常,请检查该链接是否可访问:{{ selectedHtmlFile }}

    3D 预览

    未生成
    预览未生成
    当前任务没有返回 HTML 预览链接,属于未生成状态。

    候选分模方案

    {{ scheme.title || scheme.scheme_id }}

    总分 {{ formatNumber(scheme.score) }}
    方案说明 {{ scheme.summary || scheme.reason || '暂无说明' }}
    分型方向 {{ formatSchemeDirection(scheme) }}
    模具结构 {{ scheme.mold_structure_type === 'two_half_cavity' ? '两板半腔(无独立模芯)' : '型腔 + 模芯' }}
    分型面位置 {{ scheme.offset_label || '中面' }}
    可制造性 {{ formatNumber(scheme.score_breakdown?.manufacturability || 0) }}
    分型质量 {{ formatNumber(scheme.score_breakdown?.parting_quality || 0) }}
    风险得分 {{ formatNumber(scheme.score_breakdown?.risk || 0) }}
    当前仅生成 1 套可用分模方案
    方案对比
    方案 方向 位置 总分 法向匹配 可制造性 分型质量 倒扣数 锁模力
    {{ scheme.title || scheme.scheme_id }} {{ formatSchemeDirection(scheme) }} {{ scheme.offset_label || '中面' }} {{ formatNumber(scheme.score) }} {{ formatNumber(scheme.normal_alignment_score || 0) }} {{ formatNumber(scheme.score_breakdown?.manufacturability || 0) }} {{ formatNumber(scheme.score_breakdown?.parting_quality || 0) }} {{ scheme.key_info?.quality_considerations?.undercut_count || 0 }} {{ scheme.cavity_data?.manufacturing_info?.estimated_clamping_force || 'N/A' }}

    AI 倒扣与抽芯分析

    {{ sideActionAiAdvice.statusLabel }} 未生成

    AI 结论

    判断结果 {{ sideActionAiAdvice.conclusion }}
    推荐方案 {{ sideActionAiAdvice.mechanismLabel }}
    置信度 {{ Math.round(sideActionAiAdvice.confidence * 100) }}%
    分析来源 {{ sideActionAiAdvice.source === 'ai' ? 'AI 模型' : '规则回退' }}
    {{ sideActionAiAdvice.summary || '当前任务未返回更多摘要。' }}

    判断依据

    {{ reason }}

    标准化建议

    {{ advice }}

    人工复核项

    {{ item }}
    AI 结论未生成
    当前任务未生成倒扣与抽芯 AI 分析,请检查模型配置或重新运行分析。

    CAM 与 CNC 加工

    计划 | 导出

    CAM 参数

    模具钢
    表面质量
    控制器
    包含G代码
    CAM 计划生成失败
    {{ state.camError }}
    尚未生成 CAM 计划
    点击上方“生成 CAM 计划”后,将返回工序计划、刀具建议与制造风险提示。

    LLM 设计报告

    ` }; const InventoryView = { setup() { const router = useRouter(); const route = useRoute(); const deliveryDateInput = ref(null); const expectedDateInput = ref(null); const deliveryDateNativeInput = ref(null); const expectedDateNativeInput = ref(null); const state = reactive({ activeTab: 'dashboard', backendDbReady: true, backendDbMessage: '', productCategory: 'finished', dashboard: null, financeSummary: null, financePeriod: { year: new Date().getFullYear(), quarter: '' }, financeTransactions: [], receivables: [], payables: [], customerFinanceStatement: [], supplierFinanceStatement: [], customerProductStatement: [], supplierProductStatement: [], products: [], materials: [], finishedProducts: [], purchaseOrders: [], purchaseWarehouseId: null, purchaseReceiveItems: [], productionOrders: [], productionPlan: null, productionWarehouseId: null, suppliers: [], customers: [], warehouses: [], inventory: [], movements: [], loading: false, showModal: false, modalType: '', editingItem: null, productBomItems: [], materialConsumptionItems: [], showMaterialConsumptionModal: false, consumedMaterials: [], restockItems: [], showRestockModal: false, form: {} }); const parseDateTimeLocal = (text) => { if (!text) return null; const raw = String(text).trim(); const normalized = raw.replace('T', ' ').slice(0, 16); const m = normalized.match(/^(\d{4})-(\d{2})-(\d{2})\s(\d{2}):(\d{2})$/); if (!m) return null; const year = Number(m[1]); const month = Number(m[2]); const day = Number(m[3]); const hour = Number(m[4]); const minute = Number(m[5]); if (!Number.isFinite(year + month + day + hour + minute)) return null; return new Date(year, month - 1, day, hour, minute, 0); }; const toPickerValue = (value) => { if (!value) return ''; const raw = String(value).trim(); if (/^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}/.test(raw)) return raw.slice(0, 16); if (raw.includes('T')) return raw.replace('T', ' ').slice(0, 16); const dt = new Date(raw); if (!Number.isFinite(dt.getTime())) return ''; const pad = (n) => String(n).padStart(2, '0'); return `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}`; }; const toApiDateTime = (value) => { if (!value) return null; const text = String(value).trim(); // 提取日期部分,忽略时间部分 if (text.includes('T')) { return text.split('T')[0]; } if (text.includes(' ')) { return text.split(' ')[0]; } // 如果是只有日期部分的格式 (yyyy-MM-dd) if (text.length === 10) { return text; // 直接返回日期格式,后端 Pydantic 会自动处理 } // 如果是日期对象 if (value instanceof Date) { const year = value.getFullYear(); const month = String(value.getMonth() + 1).padStart(2, '0'); const day = String(value.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } return text; }; const toNativeValue = (value) => { if (!value) return ''; const text = String(value).trim(); // 如果是只有日期部分的格式 (yyyy-MM-dd) if (text.length === 10 && !text.includes('T') && !text.includes(' ')) { return text; // 直接返回日期格式 } // 处理带时间的格式 const isoText = text.replace(' ', 'T'); return isoText.length >= 16 ? isoText.slice(0, 16) : isoText; }; const fromNativeValue = (value) => { if (!value) return ''; const text = String(value).trim(); // 如果是只有日期部分的格式 (yyyy-MM-dd) if (text.length === 10 && !text.includes('T') && !text.includes(' ')) { return text; // 直接返回日期格式 } // 处理带时间的格式 return text.replace('T', ' ').slice(0, 16); }; let deliveryPicker = null; let expectedPicker = null; const destroyPickers = () => { if (deliveryPicker) { deliveryPicker.destroy(); deliveryPicker = null; } if (expectedPicker) { expectedPicker.destroy(); expectedPicker = null; } }; const initPickers = () => { destroyPickers(); if (typeof AirDatepicker !== 'function') return; if (state.modalType === 'salesOrder' && deliveryDateInput.value) { deliveryPicker = new AirDatepicker(deliveryDateInput.value, { timepicker: false, autoClose: true, zIndex: 2005, dateFormat: 'yyyy-MM-dd', onSelect: ({ formattedDate }) => { state.form.delivery_date = formattedDate || ''; state.form.delivery_date_native = toNativeValue(formattedDate || ''); } }); const initial = parseDateTimeLocal(state.form.delivery_date); if (initial) deliveryPicker.selectDate(initial, { silent: true }); } if (state.modalType === 'purchaseOrder' && expectedDateInput.value) { expectedPicker = new AirDatepicker(expectedDateInput.value, { timepicker: false, autoClose: true, zIndex: 2005, dateFormat: 'yyyy-MM-dd', onSelect: ({ formattedDate }) => { state.form.expected_date = formattedDate || ''; state.form.expected_date_native = toNativeValue(formattedDate || ''); } }); const initial = parseDateTimeLocal(state.form.expected_date); if (initial) expectedPicker.selectDate(initial, { silent: true }); } }; const openDateTimePicker = (pickerKind) => { if (pickerKind === 'delivery' && deliveryPicker) { deliveryPicker.show(); return; } if (pickerKind === 'expected' && expectedPicker) { expectedPicker.show(); return; } const nativeInput = pickerKind === 'delivery' ? deliveryDateNativeInput.value : expectedDateNativeInput.value; if (!nativeInput) return; if (typeof nativeInput.showPicker === 'function') { nativeInput.showPicker(); return; } nativeInput.focus(); nativeInput.click(); }; const getMovementTypeLabel = (movementType) => { const movementLabelMap = { in: '其他入库', out: '其他出库', adjust: '库存调整', purchase_in: '采购入库', return_from_production: '生产退料入库', outsource_return: '外协回库', finish_in: '完工入库', issue_to_production: '生产领料出库', outsource_send: '外协发料出库', shipment_out: '销售出库', scrap_out: '报废出库' }; return movementLabelMap[movementType] || movementType; }; const getMovementBadgeClass = (movementType) => { if (['purchase_in', 'return_from_production', 'outsource_return', 'finish_in', 'in'].includes(movementType)) { return 'badge-success'; } if (['issue_to_production', 'outsource_send', 'shipment_out', 'scrap_out', 'out'].includes(movementType)) { return 'badge-error'; } return 'badge-warning'; }; const getPurchaseOrderStatusLabel = (status) => { const statusMap = { draft: '已下单', pending: '已下单', received: '已收货', paid: '已付款' }; return statusMap[status] || status; }; const isPurchaseOrderLocked = (status) => { return ['received', 'paid'].includes(status); }; const loadDashboard = async () => { state.loading = true; try { state.dashboard = await apiRequest('/api/dashboard'); } catch (e) { handleApiError(e, '加载仪表盘'); } finally { state.loading = false; } }; const loadFinishedProducts = async () => { state.loading = true; try { state.finishedProducts = await apiRequest('/api/products?item_type=finished&limit=100'); } catch (e) { handleApiError(e, '加载成品'); } finally { state.loading = false; } }; const loadProducts = async () => { // Compatibility wrapper if needed, or just load finished products await loadFinishedProducts(); }; const loadMaterials = async () => { state.loading = true; try { state.materials = await apiRequest('/api/products?item_type=material&limit=100'); } catch (e) { handleApiError(e, '加载物料'); } finally { state.loading = false; } }; const loadWarehouses = async () => { state.loading = true; try { state.warehouses = await apiRequest('/api/warehouses'); } catch (e) { handleApiError(e, '加载仓库'); } finally { state.loading = false; } }; const ensureStockBaseData = async () => { if (!state.materials.length) { await loadMaterials(); } if (!state.warehouses.length) { await loadWarehouses(); } if (!state.warehouses.length) { try { await apiRequest('/api/warehouses', { method: 'POST', body: JSON.stringify({ name: '默认仓库' }) }); await loadWarehouses(); addNotification('已自动创建默认仓库', 'success'); } catch (e) { handleApiError(e, '自动创建默认仓库'); } } }; const loadSuppliers = async () => { state.loading = true; try { state.suppliers = await apiRequest('/api/suppliers'); } catch (e) { handleApiError(e, '加载供应商'); } finally { state.loading = false; } }; const loadProductionOrders = async () => { state.loading = true; try { const [orders, warehouses] = await Promise.all([ apiRequest('/api/sales-orders?limit=100'), apiRequest('/api/warehouses') ]); state.productionOrders = orders?.items || []; state.warehouses = warehouses || []; if (!state.productionWarehouseId) { state.productionWarehouseId = state.warehouses.find(w => w.is_default)?.id || state.warehouses[0]?.id || null; } } catch (e) { handleApiError(e, '加载按单生产数据'); } finally { state.loading = false; } }; const loadPurchaseOrders = async () => { state.loading = true; try { const [orders, warehouses] = await Promise.all([ apiRequest('/api/purchase-orders?limit=100'), apiRequest('/api/warehouses') ]); state.purchaseOrders = orders?.items || []; state.warehouses = warehouses || []; if (!state.purchaseWarehouseId) { state.purchaseWarehouseId = state.warehouses.find(w => w.is_default)?.id || state.warehouses[0]?.id || null; } } catch (e) { handleApiError(e, '加载采购订单'); } finally { state.loading = false; } }; const loadCustomers = async () => { state.loading = true; try { state.customers = await apiRequest('/api/customers'); } catch (e) { handleApiError(e, '加载客户'); } finally { state.loading = false; } }; const loadInventory = async () => { state.loading = true; try { const data = await apiRequest('/api/inventory'); state.inventory = data?.items || []; } catch (e) { handleApiError(e, '加载库存'); } finally { state.loading = false; } }; const loadMovements = async () => { state.loading = true; try { const data = await apiRequest('/api/stock-movements'); state.movements = data?.items || []; } catch (e) { handleApiError(e, '加载变动记录'); } finally { state.loading = false; } }; const loadFinance = async () => { state.loading = true; try { const selectedYear = Number(state.financePeriod.year) || new Date().getFullYear(); const selectedQuarter = state.financePeriod.quarter ? Number(state.financePeriod.quarter) : null; const periodQuery = selectedQuarter ? `year=${selectedYear}&quarter=${selectedQuarter}` : `year=${selectedYear}`; const [ summary, transactions, receivables, payables, customerStatement, supplierStatement, customerProductStatement, supplierProductStatement ] = await Promise.all([ apiRequest(`/api/finance/summary?${periodQuery}`), apiRequest(`/api/finance/transactions?status=confirmed&limit=20&${periodQuery}`), apiRequest(`/api/finance/receivables?limit=20&${periodQuery}`), apiRequest(`/api/finance/payables?limit=20&${periodQuery}`), apiRequest(`/api/finance/partner-statement/customer?${periodQuery}`), apiRequest(`/api/finance/partner-statement/supplier?${periodQuery}`), apiRequest(`/api/finance/partner-product-statement/customer?${periodQuery}`), apiRequest(`/api/finance/partner-product-statement/supplier?${periodQuery}`) ]); state.financeSummary = summary; state.financeTransactions = transactions; state.receivables = receivables; state.payables = payables; state.customerFinanceStatement = customerStatement.items || []; state.supplierFinanceStatement = supplierStatement.items || []; state.customerProductStatement = customerProductStatement.items || []; state.supplierProductStatement = supplierProductStatement.items || []; } catch (e) { handleApiError(e, '加载财务数据'); } finally { state.loading = false; } }; const refreshFinanceByPeriod = () => { if (state.activeTab === 'finance') { loadFinance(); } }; const switchTab = (tab) => { state.activeTab = tab; if (!state.backendDbReady) { return; } switch (tab) { case 'dashboard': loadDashboard(); break; case 'sales_orders': loadProductionOrders(); break; case 'products': loadFinishedProducts(); break; case 'materials': loadMaterials(); break; case 'purchases': loadPurchaseOrders(); break; case 'inventory': loadInventory(); break; case 'customers': loadCustomers(); break; case 'suppliers': loadSuppliers(); break; case 'finance': loadFinance(); break; case 'movements': loadMovements(); break; } }; const menuGroups = [ { key: 'overview', title: '概览', items: [ { key: 'dashboard', label: '仪表盘' } ] }, { key: 'sales', title: '销售', items: [ { key: 'sales_orders', label: '销售订单管理' } ] }, { key: 'purchase', title: '采购', items: [ { key: 'purchases', label: '采购订单管理' } ] }, { key: 'product', title: '产品', items: [ { key: 'products', label: '成品管理' }, { key: 'materials', label: '物料管理' } ] }, { key: 'partner', title: '往来单位', items: [ { key: 'customers', label: '客户管理' }, { key: 'suppliers', label: '供应商管理' } ] }, { key: 'warehouse', title: '仓库', items: [ { key: 'inventory', label: '库存管理' }, { key: 'movements', label: '库存变动记录' } ] }, { key: 'finance', title: '财务', items: [ { key: 'finance', label: '财务概览' } ] } ]; const openGroups = reactive( Object.fromEntries(menuGroups.map(g => [g.key, true])) ); const toggleGroup = (groupKey) => { openGroups[groupKey] = !openGroups[groupKey]; }; const activeMenu = computed(() => { for (const group of menuGroups) { const item = group.items.find(i => i.key === state.activeTab); if (item) { return { group, item }; } } return null; }); const handleMenuClick = (itemKey) => { switchTab(itemKey); }; const switchProductCategory = (category) => { state.productCategory = category; }; const checkBackendHealth = async () => { try { const resp = await fetch('/health', { method: 'GET' }); if (!resp.ok) { state.backendDbReady = false; state.backendDbMessage = '后端服务异常,暂无法加载业务数据'; return; } const health = await resp.json().catch(() => null); if (health && health.database_connected === false) { state.backendDbReady = false; state.backendDbMessage = '数据库未连接,当前仅可浏览界面,业务数据暂不可用'; return; } state.backendDbReady = true; state.backendDbMessage = ''; } catch { state.backendDbReady = false; state.backendDbMessage = '无法连接后端服务'; } }; const openCreateProduct = (itemType) => { state.modalType = 'product'; state.editingItem = null; state.form = { item_type: itemType, unit: itemType === 'material' ? 'kg' : '件', min_stock: 0, max_stock: 1000, cost_price: 0, sale_price: 0 }; state.showModal = true; }; const openModal = async (type, item = null) => { state.modalType = type; state.editingItem = item; if (item) { if (type === 'salesOrder') { // 检查订单状态,如果是已收款状态,则禁止编辑 if (item.status === 'paid') { addNotification('已收款的销售订单禁止修改', 'warning'); state.modalType = null; state.editingItem = null; return; } await loadCustomers(); await loadFinishedProducts(); const detail = await apiRequest(`/api/sales-orders/${item.id}`); // 加载已消耗的物料 const movements = await apiRequest(`/api/stock-movements?reference_type=sales_order&reference_id=${item.id}&movement_type=consumption`); state.consumedMaterials = (movements || []).map(movement => ({ material_name: movement.product_name || movement.product_sku || '未知物料', quantity: Math.abs(movement.quantity), unit_price: movement.unit_price || 0, amount: movement.total_amount || 0 })); state.form = { customer_id: detail.customer_id, delivery_date: toPickerValue(detail.delivery_date), delivery_date_native: toNativeValue(toPickerValue(detail.delivery_date)), remark: detail.remark || '', items: (detail.items || []).map(line => ({ mode: line.product_id ? 'existing' : 'new', product_id: line.product_id, product_sku: '', product_name: '', quantity: line.quantity, unit_price: line.unit_price, remark: line.remark || '' })) }; } else if (type === 'purchaseOrder') { await loadSuppliers(); await loadMaterials(); const detail = await apiRequest(`/api/purchase-orders/${item.id}`); state.form = { supplier_id: detail.supplier_id, expected_date: toPickerValue(detail.expected_date), expected_date_native: toNativeValue(toPickerValue(detail.expected_date)), remark: detail.remark || '', items: (detail.items || []).map(line => ({ product_id: line.product_id, quantity: line.quantity, remark: line.remark || '' })) }; } else if (type === 'purchaseReceive') { await loadWarehouses(); const detail = await apiRequest(`/api/purchase-orders/${item.id}`); state.purchaseReceiveItems = (detail.items || []) .map(line => ({ item_id: line.id, material_label: `${line.product_sku || line.product_id} - ${line.product_name || ''}`.trim(), remaining_quantity: Math.max((line.quantity || 0) - (line.received_quantity || 0), 0), receive_quantity: Math.max((line.quantity || 0) - (line.received_quantity || 0), 0) })) .filter(line => line.remaining_quantity > 0); state.form = { warehouse_id: state.purchaseWarehouseId || state.warehouses.find(w => w.is_default)?.id || state.warehouses[0]?.id || null, remark: '' }; } else if (type === 'product') { state.form = { ...item }; if (item.item_type === 'finished') { await loadMaterials(); const bom = await apiRequest(`/api/products/${item.id}/materials`); state.productBomItems = (bom.items || []).map(bomItem => ({ material_id: bomItem.material_id, quantity: bomItem.quantity })); } } else { state.form = { ...item }; } } else { state.form = {}; if (type === 'inventoryItem') { await ensureStockBaseData(); state.form = { product_id: state.materials[0]?.id || null, warehouse_id: state.warehouses.find(w => w.is_default)?.id || state.warehouses[0]?.id || null, quantity: 0, locked_quantity: 0, batch_number: '', location: '' }; } if (type === 'salesOrder') { state.form = { customer_id: null, delivery_date: '', delivery_date_native: '', remark: '', items: [] }; } if (type === 'purchaseOrder') { state.form = { supplier_id: null, expected_date: '', expected_date_native: '', remark: '', items: [] }; } if (type === 'product') { state.form = { item_type: 'finished', unit: '件', min_stock: 0, max_stock: 1000, cost_price: 0, sale_price: 0 }; } if (type === 'salesOrder') { await loadCustomers(); await loadFinishedProducts(); state.form = { customer_id: state.customers[0]?.id || null, delivery_date: '', remark: '', items: [{ mode: 'new', product_id: null, product_sku: '', product_name: '', quantity: 1, unit_price: 0, remark: '' }] }; } if (type === 'purchaseOrder') { await loadSuppliers(); await loadMaterials(); state.form = { supplier_id: state.suppliers[0]?.id || null, expected_date: '', remark: '', items: [{ product_id: state.materials[0]?.id || null, quantity: 1, remark: '' }] }; } } state.showModal = true; nextTick(() => { initPickers(); }); }; const openMaterialConsumptionModal = async () => { // 确保是在销售订单编辑或新增页面 if (state.modalType !== 'salesOrder') { addNotification('请先打开销售订单编辑页面', 'warning'); return; } // 加载物料和仓库数据 await loadMaterials(); await loadWarehouses(); // 初始化物料消耗列表 state.materialConsumptionItems = []; // 打开物料消耗模态框 state.showMaterialConsumptionModal = true; }; const addMaterialConsumptionItem = () => { state.materialConsumptionItems.push({ material_id: state.materials[0]?.id || null, quantity: 1, remark: '' }); }; const removeMaterialConsumptionItem = (index) => { state.materialConsumptionItems.splice(index, 1); }; const saveMaterialConsumption = async () => { // 确保是在销售订单页面 if (state.modalType !== 'salesOrder') { addNotification('请先打开销售订单编辑页面', 'warning'); return; } // 如果是新增订单,先保存订单再添加物料消耗 if (!state.editingItem) { // 关闭物料消耗模态框 state.showMaterialConsumptionModal = false; await saveSalesOrder(); return; } // 验证物料消耗项 for (const [i, item] of state.materialConsumptionItems.entries()) { const idx = i + 1; if (!item.material_id) { addNotification(`第 ${idx} 行:请选择物料`, 'warning'); return; } if (!Number.isFinite(item.quantity) || item.quantity <= 0) { addNotification(`第 ${idx} 行:数量必须大于 0`, 'warning'); return; } } try { // 调用 API 保存物料消耗 const result = await apiRequest(`/api/sales-orders/${state.editingItem.id}/consume-materials`, { method: 'POST', body: JSON.stringify({ items: state.materialConsumptionItems.map(item => ({ material_id: item.material_id, quantity: item.quantity, remark: item.remark })) }) }); addNotification('物料消耗记录保存成功', 'success'); // 关闭模态框 state.showMaterialConsumptionModal = false; // 刷新订单详情 const detail = await apiRequest(`/api/sales-orders/${state.editingItem.id}`); state.form = { ...state.form, actual_material_cost: detail.actual_material_cost }; // 刷新已消耗的物料 const movements = await apiRequest(`/api/stock-movements?reference_type=sales_order&reference_id=${state.editingItem.id}&movement_type=consumption`); state.consumedMaterials = (movements || []).map(movement => ({ material_name: movement.product_name || movement.product_sku || '未知物料', quantity: Math.abs(movement.quantity), unit_price: movement.unit_price || 0, amount: movement.total_amount || 0 })); } catch (e) { handleApiError(e, '保存物料消耗'); } }; const openRestockModal = async () => { // 加载物料和供应商数据 await loadMaterials(); await loadSuppliers(); // 初始化补货列表 state.restockItems = []; // 打开补货模态框 state.showRestockModal = true; }; const addRestockItem = () => { state.restockItems.push({ material_id: state.materials[0]?.id || null, quantity: 1, unit_price: 0, remark: '' }); }; const removeRestockItem = (index) => { state.restockItems.splice(index, 1); }; const saveRestock = async () => { // 验证补货项 if (!state.restockItems || state.restockItems.length === 0) { addNotification('请至少添加一个补货物料', 'warning'); return; } for (const [i, item] of state.restockItems.entries()) { const idx = i + 1; if (!item.material_id) { addNotification(`第 ${idx} 行:请选择物料`, 'warning'); return; } if (!Number.isFinite(item.quantity) || item.quantity <= 0) { addNotification(`第 ${idx} 行:数量必须大于 0`, 'warning'); return; } if (!Number.isFinite(item.unit_price) || item.unit_price < 0) { addNotification(`第 ${idx} 行:单价必须大于等于 0`, 'warning'); return; } } try { // 创建采购订单 const payload = { supplier_id: state.suppliers[0]?.id || null, expected_date: new Date().toISOString().split('T')[0], remark: '物料补货', items: state.restockItems.map(item => ({ product_id: item.material_id, quantity: item.quantity, remark: item.remark })) }; const result = await apiRequest('/api/purchase-orders', { method: 'POST', body: JSON.stringify(payload) }); addNotification('采购订单创建成功', 'success'); // 关闭补货模态框 state.showRestockModal = false; // 清空补货列表 state.restockItems = []; // 刷新采购订单列表 loadPurchaseOrders(); } catch (e) { handleApiError(e, '保存补货订单'); } }; const closeModal = () => { state.showModal = false; state.modalType = ''; state.editingItem = null; state.productBomItems = []; state.purchaseReceiveItems = []; state.form = {}; destroyPickers(); }; const saveProduct = async () => { try { if (state.editingItem) { await apiRequest(`/api/products/${state.editingItem.id}`, { method: 'PUT', body: JSON.stringify(state.form) }); if (state.form.item_type === 'finished' && state.productBomItems.length > 0) { await apiRequest(`/api/products/${state.editingItem.id}/materials`, { method: 'PUT', body: JSON.stringify({ items: state.productBomItems }) }); } addNotification('产品更新成功', 'success'); } else { await apiRequest('/api/products', { method: 'POST', body: JSON.stringify(state.form) }); addNotification('产品创建成功', 'success'); } closeModal(); loadProducts(); loadMaterials(); } catch (e) { handleApiError(e, '保存产品'); } }; const deleteProduct = async (id) => { if (!confirm('确定要删除这个产品吗?')) return; try { await apiRequest(`/api/products/${id}`, { method: 'DELETE' }); addNotification('产品已删除', 'success'); loadProducts(); loadMaterials(); } catch (e) { handleApiError(e, '删除产品'); } }; const addBomItem = () => { state.productBomItems.push({ material_id: state.materials[0]?.id || null, quantity: 1 }); }; const removeBomItem = (idx) => { state.productBomItems.splice(idx, 1); }; const saveSupplier = async () => { try { if (state.editingItem) { await apiRequest(`/api/suppliers/${state.editingItem.id}`, { method: 'PUT', body: JSON.stringify(state.form) }); addNotification('供应商更新成功', 'success'); } else { await apiRequest('/api/suppliers', { method: 'POST', body: JSON.stringify(state.form) }); addNotification('供应商创建成功', 'success'); } closeModal(); loadSuppliers(); } catch (e) { handleApiError(e, '保存供应商'); } }; const deleteSupplier = async (id) => { if (!confirm('确定要删除这个供应商吗?')) return; try { await apiRequest(`/api/suppliers/${id}`, { method: 'DELETE' }); addNotification('供应商已删除', 'success'); loadSuppliers(); } catch (e) { handleApiError(e, '删除供应商'); } }; const saveCustomer = async () => { try { if (state.editingItem) { await apiRequest(`/api/customers/${state.editingItem.id}`, { method: 'PUT', body: JSON.stringify(state.form) }); addNotification('客户更新成功', 'success'); } else { await apiRequest('/api/customers', { method: 'POST', body: JSON.stringify(state.form) }); addNotification('客户创建成功', 'success'); } closeModal(); loadCustomers(); } catch (e) { handleApiError(e, '保存客户'); } }; const deleteCustomer = async (id) => { if (!confirm('确定要删除这个客户吗?')) return; try { await apiRequest(`/api/customers/${id}`, { method: 'DELETE' }); addNotification('客户已删除', 'success'); loadCustomers(); } catch (e) { handleApiError(e, '删除客户'); } }; const saveInventoryItem = async () => { try { if (state.editingItem) { await apiRequest(`/api/inventory/${state.editingItem.id}`, { method: 'PUT', body: JSON.stringify({ quantity: state.form.quantity, locked_quantity: state.form.locked_quantity, batch_number: state.form.batch_number, location: state.form.location }) }); addNotification('物料库存更新成功', 'success'); } else { await apiRequest('/api/inventory', { method: 'POST', body: JSON.stringify(state.form) }); addNotification('物料库存创建成功', 'success'); } closeModal(); loadInventory(); } catch (e) { handleApiError(e, '保存物料库存'); } }; const deleteInventoryItem = async (id) => { if (!confirm('确定要删除这个物料库存记录吗?')) return; try { await apiRequest(`/api/inventory/${id}`, { method: 'DELETE' }); addNotification('物料库存已删除', 'success'); loadInventory(); } catch (e) { handleApiError(e, '删除物料库存'); } }; const addSalesOrderItem = () => { state.form.items = state.form.items || []; state.form.items.push({ mode: 'new', product_id: null, product_sku: '', product_name: '', quantity: 1, unit_price: 0, remark: '' }); }; const setSalesOrderLineMode = (line, mode) => { line.mode = mode; if (mode === 'new') { line.product_id = null; line.product_sku = ''; line.product_name = ''; } else { line.product_sku = ''; line.product_name = ''; if (!line.product_id) { line.product_id = state.finishedProducts[0]?.id || null; } } }; const removeSalesOrderItem = (index) => { state.form.items.splice(index, 1); }; const saveSalesOrder = async () => { try { if (!state.form.customer_id) { addNotification('请选择客户', 'warning'); return; } if (!state.form.items || !state.form.items.length) { addNotification('请至少添加一个成品明细', 'warning'); return; } for (const [i, item] of state.form.items.entries()) { const idx = i + 1; if (item.mode === 'existing') { if (!item.product_id) { addNotification(`第 ${idx} 行:请选择模具`, 'warning'); return; } } else { if (!item.product_sku || !String(item.product_sku).trim()) { addNotification(`第 ${idx} 行:请输入模具SKU`, 'warning'); return; } if (!item.product_name || !String(item.product_name).trim()) { addNotification(`第 ${idx} 行:请输入模具名称`, 'warning'); return; } } if (!Number.isFinite(item.quantity) || item.quantity <= 0) { addNotification(`第 ${idx} 行:数量必须大于 0`, 'warning'); return; } if (!Number.isFinite(item.unit_price) || item.unit_price < 0) { addNotification(`第 ${idx} 行:单价必须大于等于 0`, 'warning'); return; } } const payload = { customer_id: state.form.customer_id, delivery_date: toApiDateTime(state.form.delivery_date), remark: state.form.remark, items: state.form.items.map(item => ({ product_id: item.mode === 'existing' ? (item.product_id || null) : null, product_sku: item.mode === 'existing' ? null : (item.product_sku || null), product_name: item.mode === 'existing' ? null : (item.product_name || null), product_category: null, product_unit: '件', quantity: item.quantity, unit_price: item.unit_price, remark: item.remark || '' })) }; let orderId; if (state.editingItem) { const result = await apiRequest(`/api/sales-orders/${state.editingItem.id}`, { method: 'PUT', body: JSON.stringify(payload) }); orderId = state.editingItem.id; addNotification(result.production_status === 'bom_missing' ? '订单已保存,但成品未配置BOM,未扣减物料' : '销售订单更新成功并已自动扣减物料', result.production_status === 'bom_missing' ? 'warning' : 'success'); } else { const result = await apiRequest('/api/sales-orders', { method: 'POST', body: JSON.stringify(payload) }); orderId = result.id; addNotification(result.production_status === 'bom_missing' ? '订单已创建,但成品未配置BOM,未扣减物料' : '销售订单创建成功并已自动扣减物料', result.production_status === 'bom_missing' ? 'warning' : 'success'); } // 如果有物料消耗记录,保存物料消耗 if (state.materialConsumptionItems && state.materialConsumptionItems.length > 0) { try { // 调用 API 保存物料消耗 const result = await apiRequest(`/api/sales-orders/${orderId}/consume-materials`, { method: 'POST', body: JSON.stringify({ items: state.materialConsumptionItems.map(item => ({ material_id: item.material_id, quantity: item.quantity, remark: item.remark })) }) }); addNotification('物料消耗记录保存成功', 'success'); // 清空物料消耗记录 state.materialConsumptionItems = []; } catch (e) { handleApiError(e, '保存物料消耗'); } } closeModal(); loadProductionOrders(); loadFinishedProducts(); loadInventory(); loadMovements(); } catch (e) { handleApiError(e, '保存销售订单'); } }; const updateSalesOrderStatus = async (order, targetStatus) => { try { await apiRequest(`/api/sales-orders/${order.id}/status`, { method: 'PATCH', body: JSON.stringify({ status: targetStatus }) }); addNotification('订单状态已更新', 'success'); loadProductionOrders(); } catch (e) { handleApiError(e, '更新订单状态'); } }; const updatePurchaseOrderStatus = async (order, targetStatus) => { try { await apiRequest(`/api/purchase-orders/${order.id}/status`, { method: 'PATCH', body: JSON.stringify({ status: targetStatus }) }); addNotification('采购订单状态已更新', 'success'); loadPurchaseOrders(); } catch (e) { handleApiError(e, '更新采购订单状态'); } }; const deleteSalesOrder = async (orderId) => { if (!confirm('确定删除这个销售订单吗?系统会自动回补已扣减物料。')) return; try { await apiRequest(`/api/sales-orders/${orderId}`, { method: 'DELETE' }); addNotification('销售订单已删除并回补物料', 'success'); if (state.productionPlan?.sales_order_id === orderId) { state.productionPlan = null; } loadProductionOrders(); loadInventory(); loadMovements(); } catch (e) { handleApiError(e, '删除销售订单'); } }; const addPurchaseOrderItem = () => { state.form.items = state.form.items || []; state.form.items.push({ product_id: state.materials[0]?.id || null, quantity: 1, remark: '' }); }; const removePurchaseOrderItem = (index) => { state.form.items.splice(index, 1); }; const savePurchaseOrder = async () => { try { if (!state.form.supplier_id) { addNotification('请选择供应商', 'warning'); return; } if (!state.form.items || !state.form.items.length) { addNotification('请至少添加一个物料明细', 'warning'); return; } for (const [i, item] of state.form.items.entries()) { const idx = i + 1; if (!item.product_id) { addNotification(`第 ${idx} 行:请选择物料`, 'warning'); return; } if (!Number.isFinite(item.quantity) || item.quantity <= 0) { addNotification(`第 ${idx} 行:数量必须大于 0`, 'warning'); return; } } const payload = { supplier_id: state.form.supplier_id, expected_date: toApiDateTime(state.form.expected_date), remark: state.form.remark, items: state.form.items.map(item => ({ product_id: item.product_id, quantity: item.quantity, remark: item.remark })) }; if (state.editingItem) { await apiRequest(`/api/purchase-orders/${state.editingItem.id}`, { method: 'PUT', body: JSON.stringify(payload) }); addNotification('采购订单更新成功', 'success'); } else { await apiRequest('/api/purchase-orders', { method: 'POST', body: JSON.stringify(payload) }); addNotification('采购订单创建成功', 'success'); } closeModal(); loadPurchaseOrders(); } catch (e) { handleApiError(e, '保存采购订单'); } }; const deletePurchaseOrder = async (orderId) => { if (!confirm('确定删除这个采购订单吗?')) return; try { await apiRequest(`/api/purchase-orders/${orderId}`, { method: 'DELETE' }); addNotification('采购订单已删除', 'success'); loadPurchaseOrders(); } catch (e) { handleApiError(e, '删除采购订单'); } }; const receivePurchaseOrder = async () => { try { if (!state.editingItem?.id) return; const items = (state.purchaseReceiveItems || []) .filter(line => Number(line.receive_quantity) > 0) .map(line => ({ item_id: line.item_id, receive_quantity: Number(line.receive_quantity) })); if (!items.length) { addNotification('请填写本次入库数量', 'warning'); return; } await apiRequest(`/api/purchase-orders/${state.editingItem.id}/receive`, { method: 'POST', body: JSON.stringify({ warehouse_id: state.form.warehouse_id || state.purchaseWarehouseId, items, remark: state.form.remark || '' }) }); addNotification('采购到货入库成功', 'success'); closeModal(); loadPurchaseOrders(); loadInventory(); loadMovements(); } catch (e) { handleApiError(e, '采购到货入库'); } }; onMounted(() => { if (!appState.user) { router.push('/login'); return; } checkBackendHealth().then(() => { if (!state.backendDbReady) { addNotification(state.backendDbMessage || '业务服务不可用', 'warning'); return; } loadDashboard(); }); }); return { state, switchTab, menuGroups, openGroups, toggleGroup, activeMenu, handleMenuClick, formatNumber, formatCurrency, formatDateTime, formatDate, openModal, switchProductCategory, openCreateProduct, closeModal, deliveryDateInput, expectedDateInput, openDateTimePicker, saveProduct, deleteProduct, addBomItem, removeBomItem, saveSupplier, deleteSupplier, saveCustomer, deleteCustomer, saveInventoryItem, deleteInventoryItem, addSalesOrderItem, removeSalesOrderItem, setSalesOrderLineMode, saveSalesOrder, updateSalesOrderStatus, updatePurchaseOrderStatus, deleteSalesOrder, addPurchaseOrderItem, removePurchaseOrderItem, savePurchaseOrder, deletePurchaseOrder, receivePurchaseOrder, loadPurchaseOrders, loadProductionOrders, refreshFinanceByPeriod, getMovementTypeLabel, getMovementBadgeClass, openMaterialConsumptionModal, addMaterialConsumptionItem, removeMaterialConsumptionItem, saveMaterialConsumption, openRestockModal, addRestockItem, removeRestockItem, saveRestock, getPurchaseOrderStatusLabel, isPurchaseOrderLocked }; }, template: `

    进销存管理

    库存、采购、销售管理

    {{ activeMenu?.group?.title || '进销存' }} / {{ activeMenu?.item?.label || '' }}
    提示
    {{ state.backendDbMessage }}
    加载中...
    📦
    {{ 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) }} {{ formatCurrency(product.material_cost || 0) }}
    SKU 名称 分类 单位 成本价 最低库存 操作
    {{ product.sku }} {{ product.name }} {{ product.category || '-' }} {{ product.unit }} {{ formatCurrency(product.cost_price) }} {{ product.min_stock || '-' }}
    SKU 物料 仓库 数量 可用 操作
    {{ item.product_sku }} {{ item.product_name }} {{ item.warehouse_name }} {{ item.quantity }} {{ item.available_quantity }}
    采购单 供应商 状态 订单创建 预计到货 实际到货 实际付款 总金额 已付款 操作
    {{ order.order_no }} {{ order.supplier_name }} {{ getPurchaseOrderStatusLabel(order.status) }} {{ order.created_at ? formatDateTime(order.created_at) : '-' }} {{ order.expected_date ? formatDate(order.expected_date) : '-' }} {{ order.received_date ? formatDateTime(order.received_date) : '-' }} {{ order.paid_date ? formatDateTime(order.paid_date) : '-' }} {{ formatCurrency(order.total_amount || 0) }} {{ formatCurrency(order.paid_amount || 0) }}
    编码 名称 联系人 电话 邮箱 操作
    {{ supplier.code }} {{ supplier.name }} {{ supplier.contact_person || '-' }} {{ supplier.phone || '-' }} {{ supplier.email || '-' }}
    编码 名称 联系人 电话 邮箱 操作
    {{ customer.code }} {{ customer.name }} {{ customer.contact_person || '-' }} {{ customer.phone || '-' }} {{ customer.email || '-' }}
    销售单 客户 订单金额 交付日期 订单创建 实际交付 实际收款 订单状态 操作
    {{ order.order_no }} {{ order.customer_name }} {{ formatCurrency(order.total_amount || 0) }} {{ order.delivery_date ? formatDate(order.delivery_date) : '-' }} {{ order.created_at ? formatDateTime(order.created_at) : '-' }} {{ order.actual_delivery_date ? formatDateTime(order.actual_delivery_date) : '-' }} {{ order.actual_payment_date ? formatDateTime(order.actual_payment_date) : '-' }} {{ order.status === 'manufacturing' ? '制造中' : order.status === 'delivered' ? '已交付' : order.status === 'paid' ? '已收款' : order.status }}
    统计周期:{{ state.financeSummary?.period_label || '-' }}
    🧾
    {{ formatCurrency(state.financeSummary?.receivable_total || 0) }}
    应收总额
    💸
    {{ formatCurrency(state.financeSummary?.payable_total || 0) }}
    应付总额
    💵
    {{ formatCurrency(state.financeSummary?.period_receipt_total || 0) }}
    周期收款
    🏦
    {{ formatCurrency(state.financeSummary?.period_payment_total || 0) }}
    周期付款

    客户账款(周期)

    客户 订单数 流水数 订单金额 订单已收 实收流水 应收余额
    {{ item.partner_name }} {{ item.order_count }} {{ item.transaction_count }} {{ formatCurrency(item.order_total) }} {{ formatCurrency(item.settled_total) }} {{ formatCurrency(item.transaction_total) }} {{ formatCurrency(item.outstanding_total) }}

    供应商账款(周期)

    供应商 订单数 流水数 订单金额 订单已付 实付流水 应付余额
    {{ item.partner_name }} {{ item.order_count }} {{ item.transaction_count }} {{ formatCurrency(item.order_total) }} {{ formatCurrency(item.settled_total) }} {{ formatCurrency(item.transaction_total) }} {{ formatCurrency(item.outstanding_total) }}

    客户-商品追溯(周期)

    客户 SKU 商品 订单数 数量 订单金额 已结款 未结款
    {{ item.partner_name }} {{ item.product_sku || '-' }} {{ item.product_name }} {{ item.order_count }} {{ formatNumber(item.order_quantity) }} {{ formatCurrency(item.order_amount) }} {{ formatCurrency(item.settled_amount) }} {{ formatCurrency(item.outstanding_amount) }}

    供应商-商品追溯(周期)

    供应商 SKU 商品 订单数 数量 订单金额 已结款 未结款
    {{ item.partner_name }} {{ item.product_sku || '-' }} {{ item.product_name }} {{ item.order_count }} {{ formatNumber(item.order_quantity) }} {{ formatCurrency(item.order_amount) }} {{ formatCurrency(item.settled_amount) }} {{ formatCurrency(item.outstanding_amount) }}

    最近财务流水

    单号 类型 往来方 金额 状态 日期
    {{ txn.txn_no }} {{ txn.txn_type === 'receipt' ? '收款' : '付款' }} {{ txn.partner_type === 'customer' ? '客户' : '供应商' }}#{{ txn.partner_id }} {{ formatCurrency(txn.amount) }} {{ txn.status === 'confirmed' ? '已确认' : '已作废' }} {{ formatDateTime(txn.txn_date) }}
    物料 类型 数量 变动前 变动后 时间
    {{ movement.product_name }}{{ movement.product_sku ? ' (' + movement.product_sku + ')' : '' }} {{ getMovementTypeLabel(movement.movement_type) }} {{ movement.quantity }} {{ movement.before_quantity }} {{ movement.after_quantity }} {{ formatDateTime(movement.created_at) }}

    {{ (state.editingItem ? '编辑' : '新增') + (state.modalType === 'product' ? (state.form.item_type === 'finished' ? '成品' : '物料') : state.modalType === 'inventoryItem' ? '物料库存' : state.modalType === 'salesOrder' ? '销售订单' : state.modalType === 'purchaseOrder' ? '采购订单' : state.modalType === 'purchaseReceive' ? '采购到货入库' : state.modalType === 'supplier' ? '供应商' : '客户') }}

    物料 数量 操作
    📅
    📦
    暂无模具
    请点击"添加模具"按钮添加订单明细
    物料名称
    数量
    单价
    金额
    {{ item.material_name }}
    {{ item.quantity }}
    {{ formatCurrency(item.unit_price) }}
    {{ formatCurrency(item.amount) }}
    合计
    {{ formatCurrency(state.editingItem.actual_material_cost) }}
    📦
    暂无物料消耗记录
    点击"消耗物料"按钮添加物料消耗明细
    📅
    📦
    暂无物料
    请点击"添加物料"按钮添加采购明细
    ¥{{ (state.materials.find(m => m.id === line.product_id)?.cost_price || 0).toFixed(2) }}
    ¥{{ ((state.materials.find(m => m.id === line.product_id)?.cost_price || 0) * (line.quantity || 0)).toFixed(2) }}
    物料 明细ID 剩余待入库 本次入库
    {{ line.material_label }} {{ line.item_id }} {{ line.remaining_quantity }}

    添加物料消耗

    📦
    暂无物料
    请点击"添加物料"按钮添加物料消耗明细
    ¥{{ (state.materials.find(m => m.id === line.material_id)?.cost_price || 0).toFixed(2) }}
    ¥{{ ((state.materials.find(m => m.id === line.material_id)?.cost_price || 0) * (line.quantity || 0)).toFixed(2) }}

    物料补货

    补货物料明细

    📦
    请点击"添加物料"按钮添加补货物料明细
    ¥{{ ((line.unit_price || 0) * (line.quantity || 0)).toFixed(2) }}
    ` }; const DesignSystemView = { setup() { const state = reactive({ ui: document.documentElement.dataset.ui || 'v1', theme: document.documentElement.dataset.theme || 'light' }); const tokensPreview = computed(() => { const styles = getComputedStyle(document.documentElement); const keys = [ '--bg-primary', '--bg-secondary', '--text-primary', '--text-secondary', '--border-default', '--primary-500', '--primary-600', '--success-500', '--warning-500', '--danger-500', '--radius-md', '--radius-lg', '--shadow-sm', '--shadow-md', '--duration-fast', '--duration-normal' ]; return keys .map(k => ({ key: k, value: styles.getPropertyValue(k).trim() })) .filter(item => item.value); }); const toggleTheme = () => { const next = state.theme === 'dark' ? 'light' : 'dark'; state.theme = next; document.documentElement.dataset.theme = next; try { localStorage.setItem('gemold_theme', next); } catch (e) {} }; const toggleUi = () => { const next = state.ui === 'v2' ? 'v1' : 'v2'; state.ui = next; document.documentElement.dataset.ui = next; try { localStorage.setItem('gemold_ui_version', next); } catch (e) {} }; return { state, tokensPreview, toggleTheme, toggleUi }; }, template: `

    设计体系

    设计令牌、组件状态与无障碍规范预览

    预览开关
    当前 UI: {{ state.ui }} · 主题: {{ state.theme }}
    Design Tokens(运行时)
    来自 CSS Variables,作为组件与页面的单一事实源
    Token Value Preview
    {{ t.key }} {{ t.value }} -
    组件状态样例
    检查 hover/focus/disabled/loading 等一致性与可见焦点
    ` }; const ReleaseView = { setup() { const state = reactive({ ui: document.documentElement.dataset.ui || 'v1', theme: document.documentElement.dataset.theme || 'light', percent: 0, uid: '' }); const load = () => { try { state.uid = localStorage.getItem('gemold_uid') || ''; } catch (e) { state.uid = ''; } try { const p = Number(localStorage.getItem('gemold_ui_rollout_percent') || 0); state.percent = Number.isFinite(p) ? p : 0; } catch (e) { state.percent = 0; } }; const setUi = (v) => { state.ui = v; try { localStorage.setItem('gemold_ui_version', v); } catch (e) {} document.documentElement.dataset.ui = v; addNotification(`已切换 UI 到 ${v}(刷新后完全生效)`, 'success'); }; const setTheme = (v) => { state.theme = v; try { localStorage.setItem('gemold_theme', v); } catch (e) {} document.documentElement.dataset.theme = v; addNotification(`已切换主题到 ${v}(刷新后完全生效)`, 'success'); }; const savePercent = () => { var p = Number(state.percent); if (!Number.isFinite(p) || p < 0) p = 0; if (p > 100) p = 100; state.percent = p; try { localStorage.setItem('gemold_ui_rollout_percent', String(p)); } catch (e) {} addNotification(`已设置灰度比例为 ${p}%(新用户分桶生效)`, 'success'); }; const rollbackToV1 = () => { try { localStorage.setItem('gemold_ui_version', 'v1'); } catch (e) {} try { localStorage.setItem('gemold_ui_rollout_percent', '0'); } catch (e) {} document.documentElement.dataset.ui = 'v1'; state.ui = 'v1'; state.percent = 0; addNotification('已回滚到 v1(建议刷新页面确认)', 'warning'); }; onMounted(load); return { state, setUi, setTheme, savePercent, rollbackToV1 }; }, template: `

    灰度发布与回滚

    本页仅用于内部控制 UI 灰度与快速回退

    当前状态
    UI: {{ state.ui }} · Theme: {{ state.theme }} · UID: {{ state.uid || '-' }}
    灰度比例
    用于无后端场景的本地灰度演练;生产建议由后端/网关下发
    ` }; 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 }, { path: "/_design-system", component: DesignSystemView }, { path: "/_release", component: ReleaseView } ]; const router = createRouter({ history: createWebHistory(), routes }); router.beforeEach((to, from, next) => { const publicPages = ['/login', '/_design-system', '/_release']; 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");