/**
* 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 }}
`,
};
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: `
提示
{{ state.backendMessage }}
`
};
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: `
📦
{{ 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 }}
客户
¥{{ 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() }}
低库存预警
| 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) }} |
|
`
};
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: `
输出
分模方案、DFM 风险、注塑模系统摘要与 CAD 导出
1. 上传产品件
{{ state.selectedFile.name }}
{{ formatFileSize(state.selectedFile.size) }}
{{ state.error }}
2. 注塑模参数
高级工艺参数
默认值适用于多数注塑件;仅在已知工艺约束时再调整。
先选择 STP 文件,再填写材料并开始分析。
分析历史({{ state.history.files.length }} 个文件)
| 文件名 |
上传次数 |
文件大小 |
最新状态 |
最新分析时间 |
操作 |
| {{ file.filename }} |
{{ file.upload_count }} 次
|
{{ formatFileSize(file.file_size) }} |
{{ file.latest_status }}
|
{{ formatDateTime(file.latest_upload_time) }} |
|
| 上传时间 |
文件大小 |
状态 |
体积 (mm³) |
表面积 (mm²) |
重量 (g) |
操作 |
| {{ formatDateTime(record.upload_time) }} |
{{ formatFileSize(record.file_size) }} |
{{ record.status }}
|
{{ record.volume ? formatNumber(record.volume) : '-' }} |
{{ record.surface_area ? formatNumber(record.surface_area) : '-' }} |
{{ record.product_weight ? record.product_weight.toFixed(2) : '-' }} |
|
|
`
};
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 sideActionSummary = computed(() => selectedSideActions.value?.summary || {});
const sliderMechanisms = computed(() => selectedSideActions.value?.slider_mechanisms || []);
const lifterMechanisms = computed(() => selectedSideActions.value?.lifter_mechanisms || []);
const sideActionRecommendations = computed(() => selectedSideActions.value?.recommendations || []);
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: 'engineering-summary', label: '工程摘要' },
{ id: 'side-actions', label: '抽芯建议' },
{ id: 'dfm-check', label: 'DFM' },
{ id: 'preview-3d', label: '预览' },
{ id: 'export-cam', label: '导出/CAM' },
{ id: 'analysis-details', label: '分析详情' },
{ id: 'diagnostic-info', 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 formatVector = (vector) => {
if (!Array.isArray(vector)) return 'N/A';
return `[${vector.map(v => Number(v).toFixed(2)).join(', ')}]`;
};
const formatMechanismSize = (size) => {
if (!size || typeof size !== 'object') return 'N/A';
const keys = Object.keys(size);
return keys.map(key => `${key}:${Number(size[key]).toFixed(1)} mm`).join(' / ');
};
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 downloadUrl = `/api/export-download/${file.filepath.replace(/\\/g, '/').split('/').slice(-2).join('/')}`;
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,
selectedSideActions,
sideActionSummary,
sliderMechanisms,
lifterMechanisms,
sideActionRecommendations,
stageTimingEntries,
getWallThicknessSummary,
countFeaturesByTypes,
getUndercutCount,
formatStageName,
formatTiming,
formatVector,
formatMechanismSize,
primaryRecommendation,
sortedRecommendations,
selectedDfmCount,
selectedCavityCount,
selectedRiskLabel,
resultAnchorLinks,
formatFileSize,
formatDateTime,
formatNumber,
getPriorityText,
exportCAD,
generateCamPlan,
camSteelOptions,
camSurfaceOptions,
camControllerOptions,
selectScheme,
onPreviewLoad,
onPreviewError,
formatSchemeDirection
};
},
template: `
分型方向
{{ formatSchemeDirection(selectedScheme) }}
分型面位置
{{ selectedScheme?.offset_label || '中面' }}
方案总分
{{ formatNumber(selectedScheme?.score || 0) }}
方案可信度
{{ formatNumber(selectedScheme?.confidence_score || 0) }}
主要理由
{{ selectedScheme?.summary || primaryRecommendation?.description || '基于当前几何与制造约束自动推荐' }}
高优先级建议
{{ primaryRecommendation?.description || '未发现高优先级工艺建议' }}
DFM 风险数
{{ selectedDfmCount }} 项
推荐型腔数
{{ selectedCavityCount }} 腔
预计成型周期
{{ selectedInjectionSystem?.overall_assessment?.estimated_cycle_time || 'N/A' }} s
下一步
{{ selectedDfmCount ? '先处理 DFM 风险,再确认导出或 CAM' : '可进入 3D 复核、导出与 CAM 准备' }}
任务信息
文件大小
{{ 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²
候选分模方案
先切换并确认推荐方案,再进入后续工程判断、导出与 CAM。
分型方向
{{ formatSchemeDirection(scheme) }}
模具结构
{{ scheme.mold_structure_type === 'two_half_cavity' ? '两板半腔(无独立模芯)' : '型腔 + 模芯' }}
分型面位置
{{ scheme.offset_label || '中面' }}
候选优先级
{{ formatNumber(scheme.priority_score || 0) }}
法向匹配度
{{ formatNumber(scheme.normal_alignment_score || 0) }}
方法
{{ scheme.method || 'rule_based' }}
方案说明
{{ scheme.summary || scheme.reason || '暂无说明' }}
可制造性
{{ formatNumber(scheme.score_breakdown?.manufacturability || 0) }}
分型质量
{{ formatNumber(scheme.score_breakdown?.parting_quality || 0) }}
风险得分
{{ formatNumber(scheme.score_breakdown?.risk || 0) }}
DFM 违规项
{{ scheme.dfm_violation_count || 0 }}
方案可信度
{{ formatNumber(scheme.confidence_score || 0) }}
回退标记
{{ scheme.is_fallback ? '是' : '否' }}
回退原因
{{ scheme.fallback_reason }}
当前仅生成 1 套可用分模方案
方案对比
| 方案 |
方向 |
位置 |
总分 |
可信度 |
回退 |
法向匹配 |
可制造性 |
分型质量 |
倒扣数 |
锁模力 |
| {{ scheme.title || scheme.scheme_id }} |
{{ formatSchemeDirection(scheme) }} |
{{ scheme.offset_label || '中面' }} |
{{ formatNumber(scheme.score) }} |
{{ formatNumber(scheme.confidence_score || 0) }} |
{{ scheme.is_fallback ? '是' : '否' }} |
{{ 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' }} |
工程摘要
方案与零件
零件名称
{{ selectedCavityData?.metadata?.part_name || selectedKeyInfo?.metadata?.part_name || 'N/A' }}
材料
{{ selectedCavityData?.metadata?.material || selectedKeyInfo?.metadata?.material || 'N/A' }}
结构判定
{{ selectedScheme?.mold_structure_type === 'two_half_cavity' ? '两板半腔(无独立模芯)' : '型腔 + 模芯' }}
型腔与锁模
型腔数量
{{ selectedCavityCount }}
预估锁模力
{{ selectedCavityData?.manufacturing_info?.estimated_clamping_force || '自动计算' }}
注塑模系统
预计成型周期
{{ selectedInjectionSystem?.overall_assessment?.estimated_cycle_time || 'N/A' }} s
冷却时间
{{ selectedInjectionSystem?.cooling?.cooling_time || 'N/A' }} s
水路数量
{{ selectedInjectionSystem?.cooling?.thermal_check?.channel_count || 0 }}
冷却流量
{{ selectedInjectionSystem?.cooling?.flow_rate?.flow_rate_lpm || 'N/A' }} L/min
浇口类型
{{ selectedInjectionSystem?.gating?.gate_type || 'N/A' }}
流道形式
{{ selectedInjectionSystem?.gating?.runner?.type || 'N/A' }}
倒扣与抽芯建议
总体摘要
倒扣面总数
{{ sideActionSummary.total_undercut_faces || getUndercutCount() }}
滑块数量
{{ sideActionSummary.total_slider_count || sliderMechanisms.length }}
斜顶数量
{{ sideActionSummary.total_lifter_count || lifterMechanisms.length }}
机构复杂度
{{ sideActionSummary.complexity || 'N/A' }}
液压抽芯
{{ sideActionSummary.has_hydraulic ? '需要' : '暂不需要' }}
建议动作
{{ sideActionRecommendations[0] || '当前方案未返回额外抽芯建议' }}
滑块抽芯
滑块 {{ idx + 1 }}
抽芯方向
{{ formatVector(item.slide_direction) }}
抽芯行程
{{ item.slide_stroke || 'N/A' }} mm
抽芯角度
{{ item.slide_angle || 'N/A' }}°
驱动方式
{{ item.actuation || 'N/A' }}
导滑方式
{{ item.guide_type || 'N/A' }}
锁紧方式
{{ item.locking_mechanism || 'N/A' }}
机构尺寸
{{ formatMechanismSize(item.block_size) }}
机构位置
{{ formatVector(item.location) }}
斜顶抽芯
斜顶 {{ idx + 1 }}
斜顶角度
{{ item.lifter_angle || 'N/A' }}°
斜顶行程
{{ item.lifter_stroke || 'N/A' }} mm
回位方式
{{ item.return_mechanism || 'N/A' }}
导向方式
{{ item.guide_type || 'N/A' }}
机构尺寸
{{ formatMechanismSize(item.block_size) }}
机构位置
{{ formatVector(item.location) }}
DFM 检查
未发现显著 DFM 违规项
当前选中方案未命中高风险规则,可进入下一步工艺评审。
| 规则 |
级别 |
说明 |
| {{ item.rule || 'N/A' }} |
{{ item.level || 'medium' }} |
{{ item.message || 'N/A' }} |
3D 预览
预览加载失败
HTML 已生成但加载异常,请检查该链接是否可访问:{{ selectedHtmlFile }}
3D 预览
预览未生成
当前任务没有返回 HTML 预览链接,属于未生成状态。
导出与 CAM
CAM 参数
模具钢
表面质量
控制器
包含G代码
CAM 计划生成失败
{{ state.camError }}
尚未生成 CAM 计划
点击上方“生成 CAM 计划”后,将返回工序计划、刀具建议与制造风险提示。
计划摘要
方案ID
{{ state.camPlan.scheme_id || 'N/A' }}
总工序数
{{ state.camPlan.summary?.total_operations || 0 }}
预计总时长
{{ formatNumber(state.camPlan.summary?.total_estimated_time_min || 0) }} min
方案可信度
{{ formatNumber(state.camPlan.confidence?.score || 0) }}
刀具建议
粗加工刀具
{{ state.camPlan.tooling_suggestion?.roughing_tool?.tool_id || 'N/A' }}
精加工刀具
{{ state.camPlan.tooling_suggestion?.finishing_tool?.tool_id || 'N/A' }}
工序计划
| 序号 |
工序 |
刀具 |
主轴转速 |
进给率 |
预计时长(min) |
| {{ op.seq }} |
{{ op.operation }} |
{{ op.tool_id || 'N/A' }} |
{{ formatNumber(op.spindle_speed_rpm || 0) }} |
{{ formatNumber(op.feed_rate_mm_min || 0) }} |
{{ formatNumber(op.estimated_time_min || 0) }} |
分析结果详情
{{ state.task.analysis_result.analysis_summary }}
几何与特征摘要
整体轮廓和尺寸比例
{{ state.task.analysis_result?.geometry_data?.bounding_box?.dimensions ?
(state.task.analysis_result.geometry_data.bounding_box.dimensions[0]?.toFixed(1) || 0) + ' × ' +
(state.task.analysis_result.geometry_data.bounding_box.dimensions[1]?.toFixed(1) || 0) + ' × ' +
(state.task.analysis_result.geometry_data.bounding_box.dimensions[2]?.toFixed(1) || 0) + ' mm' : 'N/A' }}
壁厚分布
{{ getWallThicknessSummary() }}
加强筋位置和密度
{{ countFeaturesByTypes('rib_structure') || 0 }} 个加强筋
柱位和功能特征
{{ countFeaturesByTypes('boss_feature') || 0 }} 个柱位/功能特征
倒扣区域检测
{{ getUndercutCount() }} 个倒扣区域
对称性分析
{{ state.task.analysis_result.geometry_data?.symmetry || '非对称' }}
重心位置
{{ state.task.analysis_result.geometry_data?.center_of_mass ?
'(' + (state.task.analysis_result.geometry_data.center_of_mass[0]?.toFixed(2) || 0) + ', ' +
(state.task.analysis_result.geometry_data.center_of_mass[1]?.toFixed(2) || 0) + ', ' +
(state.task.analysis_result.geometry_data.center_of_mass[2]?.toFixed(2) || 0) + ') mm' : 'N/A' }}
工程建议与质量指标
高优先级工艺建议
{{ state.task.analysis_result.design_recommendations?.find(r => r.priority === 'high')?.description || '根据几何分析自动生成' }}
加强筋布局
{{ countFeaturesByTypes('rib_structure') ? '已识别加强筋区域,建议校核厚度和脱模方向' : '当前未识别明显加强筋特征' }}
浇口与进胶提示
{{ state.task.analysis_result.design_recommendations?.find(r => r.type === 'wall_thickness' || r.type === 'draft_angle')?.reason || '建议结合主分型方向确认浇口位置' }}
冷却设计提示
{{ getWallThicknessSummary() !== '待分析' ? '建议按壁厚分布校核冷却均匀性' : '需先完成壁厚分析后再校核冷却' }}
定位结构设计
{{ countFeaturesByTypes('boss_feature') || 0 }} 个柱位/定位相关特征
分型面选择
基于产品几何:自动推荐最优分型面
模架尺寸
{{ (selectedCavityData.manufacturing_info.estimated_mold_size.length || 0) + ' × ' + (selectedCavityData.manufacturing_info.estimated_mold_size.width || 0) + ' × ' + (selectedCavityData.manufacturing_info.estimated_mold_size.height || 0) + ' mm' }}
体积利用率
{{ (state.task.analysis_result.quality_metrics.volume_utilization * 100)?.toFixed(1) || 'N/A' }}%
拓扑复杂度
{{ state.task.analysis_result.quality_metrics.topology_complexity?.toFixed(2) || 'N/A' }}
壁厚均匀性
{{ (state.task.analysis_result.quality_metrics.wall_uniformity * 100)?.toFixed(1) || 'N/A' }}%
翘曲风险
{{ selectedKeyInfo.quality_considerations.warpage_risk || '低风险' }}
潜在焊缝线
{{ selectedKeyInfo.quality_considerations.potential_weld_lines || 0 }} 条
详细设计建议
{{ getPriorityText(rec.priority) }}
{{ rec.description }}
({{ rec.reason }})
诊断信息
处理耗时
| 阶段 |
耗时 |
| {{ formatStageName(stage) }} |
{{ formatTiming(duration) }} |
体积差异
{{ state.task.verification.comparison.volume.difference_percent?.toFixed(4) || 0 }}%
表面积差异
{{ state.task.verification.comparison.surface_area.difference_percent?.toFixed(4) || 0 }}%
{{ state.task.llm_report }}
`
};
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: `
提示
{{ 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 }} |
|
🧾
{{ 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) }} |
📦
暂无物料
请点击"添加物料"按钮添加物料消耗明细
`
};
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: `
| Token |
Value |
Preview |
{{ t.key }} |
{{ t.value }} |
-
|
`
};
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: `
`
};
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");