+ 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: `
+
+
-
`
+ `
+};
+
+const MoldInsightView = {
+ setup() {
+ const router = useRouter();
+ const deliveryDate = ref(null);
+ const expectedDate = ref(null);
+ 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);
+
+ 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 isFoamMaterial = (material) => {
+ const foamMaterials = ['AlSi10Mg', 'AlSi12', 'Pure Al Foam', 'AlSi7Mg'];
+ return foamMaterials.includes(material);
+ };
+
+ return {
+ state,
+ handleFileChange,
+ handleDrop,
+ uploadFile,
+ formatFileSize,
+ formatDateTime,
+ formatNumber,
+ toggleFileHistory,
+ viewResult,
+ isFoamMaterial
+ };
+ },
+ template: `
+
+
+
+
+
+
+
📁
+
+ 点击选择或拖拽文件
+ 支持 .stp, .step 格式,最大 100MB
+
+
+
+
+ {{ state.selectedFile.name }}
+ {{ formatFileSize(state.selectedFile.size) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ state.error }}
+
+
+
+
+
+
+
+
分析历史
+
+
+
+
+ | 文件名 |
+ 上传次数 |
+ 文件大小 |
+ 最新状态 |
+ 最新分析时间 |
+ 操作 |
+
+
+
+
+
+ | {{ 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 isFoamMaterial = (material) => {
+ return material === 'aluminum_foam';
+ };
+
+ 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 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 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,
+ 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,
+ formatFileSize,
+ formatDateTime,
+ formatNumber,
+ getPriorityText,
+ isFoamMaterial,
+ exportCAD,
+ generateCamPlan,
+ camSteelOptions,
+ camSurfaceOptions,
+ camControllerOptions,
+ selectScheme,
+ onPreviewLoad,
+ onPreviewError,
+ formatSchemeDirection
+ };
+ },
+ template: `
+
+
+
+
+
+
+
+
+
+
+
+
文件信息
+
+
+ 文件大小
+ {{ 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²
+
+
+
+
+
+
+
候选分模方案
+
+
+
+
+
+ 分型方向
+ {{ 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' ? '两板半腔(无独立模芯)' : '型腔 + 模芯' }}
+
+
+
+
+
型腔信息
+
+
+ 型腔数量
+ {{ Object.keys(selectedCavityData?.mold_cavities || selectedKeyInfo?.mold_cavities || {}).length }}
+
+
+
+
+
+
+
+
DFM 检查
+
+
未发现显著 DFM 违规项
+
当前选中方案未命中高风险规则,可进入下一步工艺评审。
+
+
+
+
+
+ | 规则 |
+ 级别 |
+ 说明 |
+
+
+
+
+ | {{ item.rule || 'N/A' }} |
+ {{ item.level || 'medium' }} |
+ {{ item.message || 'N/A' }} |
+
+
+
+
+
+
+
+
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) }} |
+
+
+
+
+
+
+
+
+
+
3D 预览
+
+
预览加载失败
+
HTML 已生成但加载异常,请检查该链接是否可访问:{{ selectedHtmlFile }}
+
+
+
+
+
+
3D 预览
+
+
预览未生成
+
当前任务没有返回 HTML 预览链接,属于未生成状态。
+
+
+
+
+
分析结果详情
+
+
+
+
+
+
+
+
+ 体积差异
+
+ {{ state.task.verification.comparison.volume.difference_percent?.toFixed(4) || 0 }}%
+
+
+
+ 表面积差异
+
+ {{ state.task.verification.comparison.surface_area.difference_percent?.toFixed(4) || 0 }}%
+
+
+
+
+
+
+
+
+
+
+
+ {{ state.task.analysis_result.analysis_summary }}
+
+
+
+
+
+
1. 产品特征识别
+
+
+ 整体轮廓和尺寸比例
+ {{ 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' }}
+
+
+ 壁厚分布
+ {{ state.task.analysis_result.detected_features?.find(f => f.feature_type === 'wall_thickness')?.description || '待分析' }}
+
+
+ 加强筋位置和密度
+ {{ state.task.analysis_result.detected_features?.filter(f => f.feature_type === 'rib').length || 0 }} 个加强筋
+
+
+ 孔洞和凹槽位置
+ {{ state.task.analysis_result.detected_features?.filter(f => f.feature_type === 'hole' || f.feature_type === 'pocket').length || 0 }} 个孔洞/凹槽
+
+
+ 倒扣区域检测
+ {{ state.task.analysis_result.detected_features?.filter(f => f.feature_type === 'undercut').length || 0 }} 个倒扣区域
+
+
+ 对称性分析
+ {{ 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' }}
+
+
+
+
+
+
2. 泡沫包装设计决策
+
+
+ 泡沫厚度建议
+ {{ state.task.analysis_result.design_recommendations?.find(r => r.priority === 'high')?.recommendation || '根据产品重量和脆弱程度自动计算' }}
+
+
+ 加强筋布局
+ 基于产品薄弱区域自动布置
+
+
+ 取手槽位置
+ 基于重心位置:{{ state.task.analysis_result.geometry_data?.center_of_mass ? '自动优化' : '手动设置' }}
+
+
+ 通风孔位置
+ 防止真空吸附:建议在产品最大平面区域设置通风孔
+
+
+ 定位结构设计
+ {{ state.task.analysis_result.detected_features?.filter(f => f.feature_type === '定位').length || 0 }} 个定位特征
+
+
+ 分型面选择
+ 基于产品几何:自动推荐最优分型面
+
+
+
+
+
详细设计建议
+
+
+ {{ getPriorityText(rec.priority) }}
+ {{ rec.description }}
+ ({{ rec.reason }})
+
+
+
+
+
+
+
+
3. 模具工程决策
+
+
+ 型腔数量建议
+ {{ selectedCavityData?.mold_cavities ? Object.keys(selectedCavityData.mold_cavities).length : 1 }} 腔
+
+
+ 模架尺寸
+ {{ selectedCavityData?.manufacturing_info?.estimated_mold_size ?
+ (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' : '基于泡沫外形自动计算' }}
+
+
+ 预估锁模力
+ {{ selectedCavityData?.manufacturing_info?.estimated_clamping_force || '自动计算' }}
+
+
+ 顶出系统
+ 顶针顶出(自动布局)
+
+
+ 冷却水路设计
+ {{ state.task.analysis_result.geometry_data?.volume > 1000000 ? '需要冷却水路' : '自然冷却' }}
+
+
+ 材料选择
+ {{ state.task.analysis_result.quality_metrics?.volume ? '根据产量和精度自动推荐' : '7075 铝合金' }}
+
+
+ 体积利用率
+ {{ (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 }} 条
+
+
+
+
+
+
+
+ `
+};
+
+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 || [];
+ 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 || [];
+ 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 {
+ state.inventory = await apiRequest('/api/inventory');
+ } catch (e) {
+ handleApiError(e, '加载库存');
+ } finally {
+ state.loading = false;
+ }
+ };
+
+ const loadMovements = async () => {
+ state.loading = true;
+ try {
+ state.movements = await apiRequest('/api/stock-movements');
+ } 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 }} |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
统计周期:{{ 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) }} |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
📦
+
暂无物料
+
请点击"添加物料"按钮添加物料消耗明细
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `
+};
+
+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: '/login', component: LoginPage },
- { path: '/dashboard', component: DashboardPage, meta: { requiresAuth: true } },
- { path: '/:pathMatch(.*)*', redirect: '/dashboard' },
+ { 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: createWebHashHistory(), routes });
+const router = createRouter({
+ history: createWebHistory(),
+ routes
+});
+
router.beforeEach((to, from, next) => {
- if (to.meta.requiresAuth && !store.user) return next('/login');
- if (to.path === '/login' && store.user) return next('/dashboard');
+ 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({ template: '
' });
+const app = createApp(App);
app.use(router);
-app.mount('#app');
+app.mount("#app");