This commit is contained in:
2026-05-14 16:56:18 +08:00
parent d38a4630dd
commit 224330598a
15 changed files with 1151 additions and 277 deletions
+411 -217
View File
@@ -981,8 +981,6 @@ const UsersView = {
const MoldInsightView = {
setup() {
const router = useRouter();
const deliveryDate = ref(null);
const expectedDate = ref(null);
const state = reactive({
selectedFile: null,
selectedMaterial: 'ABS',
@@ -1079,6 +1077,10 @@ const MoldInsightView = {
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", {
@@ -1154,11 +1156,6 @@ const MoldInsightView = {
loadHistory();
});
const isFoamMaterial = (material) => {
const foamMaterials = ['AlSi10Mg', 'AlSi12', 'Pure Al Foam', 'AlSi7Mg'];
return foamMaterials.includes(material);
};
return {
state,
handleFileChange,
@@ -1168,56 +1165,74 @@ const MoldInsightView = {
formatDateTime,
formatNumber,
toggleFileHistory,
viewResult,
isFoamMaterial
viewResult
};
},
template: `
<div class="page-container">
<div class="page-header">
<h1>MoldInsight</h1>
<p>STP 模具几何分析</p>
<h1>注塑模 STP 分析</h1>
<p>上传 STEP/STP 产品件,完成自动分模、工程建议与导出</p>
</div>
<div class="upload-section">
<div
:class="['upload-zone', { 'drag-over': state.dragOver }]"
@dragover.prevent="state.dragOver = true"
@dragleave.prevent="state.dragOver = false"
@drop="handleDrop"
@click="$refs.fileInput.click()"
>
<input
ref="fileInput"
type="file"
accept=".stp,.step"
@change="handleFileChange"
hidden
/>
<div class="upload-icon">📁</div>
<div class="upload-text">
<span class="upload-title">点击选择或拖拽文件</span>
<span class="upload-hint">支持 .stp, .step 格式,最大 100MB</span>
<div class="moldinsight-intro-grid">
<div class="intro-card">
<div class="intro-card-title">输入</div>
<div class="intro-card-text">STEP/STP 产品件,面向注塑模主流程</div>
</div>
<div class="intro-card">
<div class="intro-card-title">输出</div>
<div class="intro-card-text">分模方案、DFM 风险、注塑模系统摘要与 CAD 导出</div>
</div>
<div class="intro-card">
<div class="intro-card-title">目标</div>
<div class="intro-card-text">先确认推荐方案,再进入导出与 CAM 准备</div>
</div>
</div>
<div class="upload-layout">
<div class="upload-main-card">
<h2 class="section-title">1. 上传产品件</h2>
<div
:class="['upload-zone', { 'drag-over': state.dragOver }]"
@dragover.prevent="state.dragOver = true"
@dragleave.prevent="state.dragOver = false"
@drop="handleDrop"
@click="$refs.fileInput.click()"
>
<input
ref="fileInput"
type="file"
accept=".stp,.step"
@change="handleFileChange"
hidden
/>
<div class="upload-icon">📁</div>
<div class="upload-text">
<span class="upload-title">点击选择或拖拽 STP/STEP 文件</span>
<span class="upload-hint">支持注塑模产品件分析,最大 100MB</span>
</div>
</div>
<div v-if="state.selectedFile" class="file-info">
<span class="file-name">{{ state.selectedFile.name }}</span>
<span class="file-size">{{ formatFileSize(state.selectedFile.size) }}</span>
<button class="btn-clear" @click="state.selectedFile = null" title="清除文件">×</button>
</div>
<div v-if="state.error" class="error-message">{{ state.error }}</div>
<div v-if="state.polling" class="progress-bar">
<div class="progress-fill" :style="{ width: state.progress + '%' }"></div>
</div>
</div>
<div v-if="state.selectedFile" class="file-info">
<span class="file-name">{{ state.selectedFile.name }}</span>
<span class="file-size">{{ formatFileSize(state.selectedFile.size) }}</span>
<button class="btn-clear" @click="state.selectedFile = null" title="清除文件">×</button>
</div>
<!-- 材料选择面板 -->
<div v-if="state.selectedFile" class="material-panel">
<div class="panel-header">
<span class="panel-title">📦 材料与参数设置</span>
</div>
<!-- 材料类型选择 -->
<div class="form-group">
<label class="form-label">材料类型</label>
<select v-model="state.selectedMaterial" class="form-select">
<optgroup label="普通塑料">
<div class="upload-side-card">
<h2 class="section-title">2. 注塑模参数</h2>
<div class="material-panel compact-panel">
<div class="form-group">
<label class="form-label">产品材料</label>
<select v-model="state.selectedMaterial" class="form-select">
<option value="ABS">ABS (1.05 g/cm³)</option>
<option value="PP">PP (0.90 g/cm³)</option>
<option value="PE">PE (0.95 g/cm³)</option>
@@ -1226,70 +1241,58 @@ const MoldInsightView = {
<option value="POM">POM (1.41 g/cm³)</option>
<option value="PMMA">PMMA (1.18 g/cm³)</option>
<option value="PBT">PBT (1.31 g/cm³)</option>
</optgroup>
<optgroup label="铝泡沫材料">
<option value="AlSi10Mg">AlSi10Mg (0.45 g/cm³) - 常用铝硅泡沫</option>
<option value="AlSi12">AlSi12 (0.50 g/cm³) - 高强度铝泡沫</option>
<option value="Pure Al Foam">Pure Al Foam (0.35 g/cm³) - 纯铝泡沫</option>
<option value="AlSi7Mg">AlSi7Mg (0.40 g/cm³) - 轻质铝镁泡沫</option>
</optgroup>
</select>
</div>
<!-- 铝泡沫参数(仅在选择泡沫材料时显示) -->
<div v-if="isFoamMaterial(state.selectedMaterial)" class="foam-params">
<div class="param-section">
<span class="param-title">⚙️ 铝泡沫专用参数</span>
<div class="form-row">
<div class="form-group">
<label class="form-label">拔模角 (°)</label>
<input type="range" v-model.number="state.moldParams.draftAngle" min="1" max="10" step="0.5" class="form-range">
<span class="range-value">{{ state.moldParams.draftAngle }}°</span>
</div>
<div class="form-group">
<label class="form-label">收缩率 (%)</label>
<input type="range" v-model.number="state.moldParams.shrinkageRate" min="0.5" max="3.0" step="0.1" class="form-range">
<span class="range-value">{{ state.moldParams.shrinkageRate }}%</span>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">分型精度 (mm)</label>
<input type="range" v-model.number="state.moldParams.partingPrecision" min="0.01" max="1.0" step="0.01" class="form-range">
<span class="range-value">{{ state.moldParams.partingPrecision }} mm</span>
</div>
<div class="form-group">
<label class="form-label">型腔匹配度 (%)</label>
<input type="range" v-model.number="state.moldParams.cavityMatch" min="80" max="100" step="1" class="form-range">
<span class="range-value">{{ state.moldParams.cavityMatch }}%</span>
</div>
</div>
</select>
</div>
<details class="advanced-params">
<summary>高级工艺参数</summary>
<div class="advanced-params-body">
<div class="inline-note">默认值适用于多数注塑件;仅在已知工艺约束时再调整。</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">拔模角 (°)</label>
<input type="range" v-model.number="state.moldParams.draftAngle" min="1" max="10" step="0.5" class="form-range">
<span class="range-value">{{ state.moldParams.draftAngle }}°</span>
</div>
<div class="form-group">
<label class="form-label">收缩率 (%)</label>
<input type="range" v-model.number="state.moldParams.shrinkageRate" min="0.5" max="3.0" step="0.1" class="form-range">
<span class="range-value">{{ state.moldParams.shrinkageRate }}%</span>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">分型精度 (mm)</label>
<input type="range" v-model.number="state.moldParams.partingPrecision" min="0.01" max="1.0" step="0.01" class="form-range">
<span class="range-value">{{ state.moldParams.partingPrecision }} mm</span>
</div>
<div class="form-group">
<label class="form-label">型腔匹配度 (%)</label>
<input type="range" v-model.number="state.moldParams.cavityMatch" min="80" max="100" step="1" class="form-range">
<span class="range-value">{{ state.moldParams.cavityMatch }}%</span>
</div>
</div>
</div>
</details>
</div>
</div>
<div v-if="state.error" class="error-message">{{ state.error }}</div>
<button
v-if="state.selectedFile"
class="btn-primary"
@click="uploadFile"
:disabled="state.uploading || state.polling"
>
{{ state.uploading ? '上传中...' : state.polling ? '分析中...' : '开始分析' }}
</button>
<div v-if="state.polling" class="progress-bar">
<div class="progress-fill" :style="{ width: state.progress + '%' }"></div>
<button
v-if="state.selectedFile"
class="btn-primary upload-submit-btn"
@click="uploadFile"
:disabled="state.uploading || state.polling"
>
{{ state.uploading ? '上传中...' : state.polling ? '分析中...' : '3. 开始注塑模分析' }}
</button>
<div v-else class="inline-note">先选择 STP 文件,再填写材料并开始分析。</div>
</div>
</div>
<div v-if="state.history?.files?.length" class="section">
<h2 class="section-title">分析历史</h2>
<details v-if="state.history?.files?.length" class="history-collapsible section">
<summary class="history-summary">分析历史({{ state.history.files.length }} 个文件)</summary>
<div class="table-container">
<table class="data-table">
<thead>
@@ -1369,7 +1372,7 @@ const MoldInsightView = {
</tbody>
</table>
</div>
</div>
</details>
</div>
`
};
@@ -1457,10 +1460,6 @@ const ResultView = {
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(() => {
@@ -1471,6 +1470,33 @@ const ResultView = {
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 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 selectScheme = (schemeId) => {
state.selectedSchemeId = schemeId;
@@ -1495,6 +1521,40 @@ const ResultView = {
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 exportCAD = async (format) => {
try {
const taskId = route.params.taskId;
@@ -1503,6 +1563,7 @@ const ResultView = {
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']
})
@@ -1586,11 +1647,22 @@ const ResultView = {
selectedKeyInfo,
selectedHtmlFile,
selectedDfmViolations,
selectedInjectionSystem,
stageTimingEntries,
getWallThicknessSummary,
countFeaturesByTypes,
getUndercutCount,
formatStageName,
formatTiming,
primaryRecommendation,
sortedRecommendations,
selectedDfmCount,
selectedCavityCount,
selectedRiskLabel,
formatFileSize,
formatDateTime,
formatNumber,
getPriorityText,
isFoamMaterial,
exportCAD,
generateCamPlan,
camSteelOptions,
@@ -1623,28 +1695,90 @@ const ResultView = {
<span :class="['badge', state.task.status === 'completed' ? 'badge-success' : 'badge-error']">
{{ state.task.status }}
</span>
<div v-if="state.task.status === 'completed'" class="export-buttons">
<button class="btn-sm btn-primary" :disabled="state.camLoading" @click="generateCamPlan()" title="基于当前选中方案生成CAM工艺计划">
{{ state.camLoading ? '生成中...' : '生成 CAM 计划' }}
</button>
<button class="btn-sm btn-primary" @click="exportCAD('step')" title="导出STEP格式(UG/FreeCAD/SolidWorks通用)">
导出 STEP
</button>
<button class="btn-sm btn-secondary" @click="exportCAD('stl')" title="导出STL网格格式(3D打印预览)">
导出 STL
</button>
<button class="btn-sm btn-secondary" @click="exportCAD('iges')" title="导出IGES格式(兼容旧系统)">
导出 IGES
</button>
<button class="btn-sm btn-secondary" @click="exportCAD('brep')" title="导出BRep格式(FreeCAD原生)">
导出 BRep
</button>
</div>
<div class="result-grid result-priority-grid">
<div class="result-card result-card-highlight">
<div class="summary-header">
<h3>推荐方案</h3>
<span class="badge badge-info">{{ selectedScheme?.title || selectedScheme?.scheme_id || '方案待定' }}</span>
</div>
<div class="info-list">
<div class="info-item">
<span class="info-label">分型方向</span>
<span class="info-value">{{ formatSchemeDirection(selectedScheme) }}</span>
</div>
<div class="info-item">
<span class="info-label">分型面位置</span>
<span class="info-value">{{ selectedScheme?.offset_label || '中面' }}</span>
</div>
<div class="info-item">
<span class="info-label">方案总分</span>
<span class="info-value">{{ formatNumber(selectedScheme?.score || 0) }}</span>
</div>
<div class="info-item">
<span class="info-label">方案可信度</span>
<span class="info-value">{{ formatNumber(selectedScheme?.confidence_score || 0) }}</span>
</div>
<div class="info-item">
<span class="info-label">主要理由</span>
<span class="info-value">{{ selectedScheme?.summary || primaryRecommendation?.description || '基于当前几何与制造约束自动推荐' }}</span>
</div>
<div class="info-item">
<span class="info-label">高优先级建议</span>
<span class="info-value">{{ primaryRecommendation?.description || '未发现高优先级工艺建议' }}</span>
</div>
</div>
</div>
<div class="result-card result-card-highlight">
<div class="summary-header">
<h3>关键操作</h3>
<span :class="['badge', selectedRiskLabel === '高风险' ? 'badge-error' : selectedRiskLabel === '中风险' ? 'badge-warning' : 'badge-success']">
{{ selectedRiskLabel }}
</span>
</div>
<div class="info-list">
<div class="info-item">
<span class="info-label">DFM 风险数</span>
<span class="info-value">{{ selectedDfmCount }} 项</span>
</div>
<div class="info-item">
<span class="info-label">推荐型腔数</span>
<span class="info-value">{{ selectedCavityCount }} 腔</span>
</div>
<div class="info-item">
<span class="info-label">预计成型周期</span>
<span class="info-value">{{ selectedInjectionSystem?.overall_assessment?.estimated_cycle_time || 'N/A' }} s</span>
</div>
<div class="info-item">
<span class="info-label">下一步</span>
<span class="info-value">{{ selectedDfmCount ? '先处理 DFM 风险,再确认导出或 CAM' : '可进入 3D 复核、导出与 CAM 准备' }}</span>
</div>
</div>
<div v-if="state.task.status === 'completed'" class="export-buttons export-buttons-block">
<button class="btn-sm btn-primary" @click="exportCAD('step')" title="导出STEP格式(UG/FreeCAD/SolidWorks通用)">
导出 STEP
</button>
<button class="btn-sm btn-secondary" @click="exportCAD('iges')" title="导出IGES格式(兼容旧系统)">
导出 IGES
</button>
<button class="btn-sm btn-secondary" @click="exportCAD('stl')" title="导出STL网格格式(3D打印预览)">
导出 STL
</button>
<button class="btn-sm btn-secondary" @click="exportCAD('brep')" title="导出BRep格式(FreeCAD原生)">
导出 BRep
</button>
<button class="btn-sm btn-primary" :disabled="state.camLoading" @click="generateCamPlan()" title="基于当前选中方案生成CAM工艺计划">
{{ state.camLoading ? '生成中...' : '生成 CAM 计划' }}
</button>
</div>
</div>
</div>
<div class="result-grid">
<div class="result-grid compact-metrics-grid">
<div class="result-card">
<h3>文件信息</h3>
<h3>任务信息</h3>
<div class="info-list">
<div class="info-item">
<span class="info-label">文件大小</span>
@@ -1662,7 +1796,7 @@ const ResultView = {
</div>
<div class="result-card">
<h3>几何数据</h3>
<h3>几何概览</h3>
<div class="info-list">
<div class="info-item" v-if="state.task.mesh_summary">
<span class="info-label">顶点数</span>
@@ -1690,6 +1824,7 @@ const ResultView = {
<div v-if="candidateSchemes.length" class="viewer-section">
<h3>候选分模方案</h3>
<div class="inline-note">先切换并确认推荐方案,再进入后续工程判断、导出与 CAM。</div>
<div :class="['result-grid', hasSingleScheme ? 'single-scheme-grid' : '']">
<div
v-for="scheme in candidateSchemes"
@@ -1765,9 +1900,9 @@ const ResultView = {
</div>
</div>
<div v-if="candidateSchemes.length > 1" class="viewer-section">
<h3>方案对比</h3>
<div class="result-card full-width">
<details v-if="candidateSchemes.length > 1" class="viewer-section diagnostic-panel">
<summary>方案对比</summary>
<div class="result-card full-width diagnostic-panel-body">
<table class="data-table">
<thead>
<tr>
@@ -1801,13 +1936,13 @@ const ResultView = {
</tbody>
</table>
</div>
</div>
</details>
<div v-if="selectedCavityData || selectedKeyInfo" class="viewer-section">
<h3>模具信息</h3>
<h3>工程摘要</h3>
<div class="result-grid">
<div class="result-card">
<h4>基本信息</h4>
<h4>方案与零件</h4>
<div class="info-list">
<div class="info-item">
<span class="info-label">零件名称</span>
@@ -1824,11 +1959,44 @@ const ResultView = {
</div>
</div>
<div class="result-card" v-if="selectedCavityData?.mold_cavities || selectedKeyInfo?.mold_cavities">
<h4>型腔信息</h4>
<h4>型腔与锁模</h4>
<div class="info-list">
<div class="info-item">
<span class="info-label">型腔数量</span>
<span class="info-value">{{ Object.keys(selectedCavityData?.mold_cavities || selectedKeyInfo?.mold_cavities || {}).length }}</span>
<span class="info-value">{{ selectedCavityCount }}</span>
</div>
<div class="info-item">
<span class="info-label">预估锁模力</span>
<span class="info-value">{{ selectedCavityData?.manufacturing_info?.estimated_clamping_force || '自动计算' }}</span>
</div>
</div>
</div>
<div class="result-card" v-if="selectedInjectionSystem">
<h4>注塑模系统</h4>
<div class="info-list">
<div class="info-item">
<span class="info-label">预计成型周期</span>
<span class="info-value">{{ selectedInjectionSystem?.overall_assessment?.estimated_cycle_time || 'N/A' }} s</span>
</div>
<div class="info-item">
<span class="info-label">冷却时间</span>
<span class="info-value">{{ selectedInjectionSystem?.cooling?.cooling_time || 'N/A' }} s</span>
</div>
<div class="info-item">
<span class="info-label">水路数量</span>
<span class="info-value">{{ selectedInjectionSystem?.cooling?.thermal_check?.channel_count || 0 }}</span>
</div>
<div class="info-item">
<span class="info-label">冷却流量</span>
<span class="info-value">{{ selectedInjectionSystem?.cooling?.flow_rate?.flow_rate_lpm || 'N/A' }} L/min</span>
</div>
<div class="info-item">
<span class="info-label">浇口类型</span>
<span class="info-value">{{ selectedInjectionSystem?.gating?.gate_type || 'N/A' }}</span>
</div>
<div class="info-item">
<span class="info-label">流道形式</span>
<span class="info-value">{{ selectedInjectionSystem?.gating?.runner?.type || 'N/A' }}</span>
</div>
</div>
</div>
@@ -1861,8 +2029,31 @@ const ResultView = {
</div>
</div>
<div v-if="selectedHtmlFile" class="viewer-section">
<h3>3D 预览</h3>
<div v-if="state.previewStatus === 'error'" class="inline-alert inline-alert-warning" style="margin-bottom: var(--space-3);">
<div class="inline-alert-title">预览加载失败</div>
<div class="inline-alert-message">HTML 已生成但加载异常,请检查该链接是否可访问:{{ selectedHtmlFile }}</div>
</div>
<iframe
:key="selectedHtmlFile"
:src="selectedHtmlFile"
class="viewer-frame"
@load="onPreviewLoad"
@error="onPreviewError"
></iframe>
</div>
<div v-else class="viewer-section">
<h3>3D 预览</h3>
<div class="inline-alert inline-alert-warning">
<div class="inline-alert-title">预览未生成</div>
<div class="inline-alert-message">当前任务没有返回 HTML 预览链接,属于未生成状态。</div>
</div>
</div>
<div class="viewer-section">
<h3>CAM 准备包</h3>
<h3>导出与 CAM</h3>
<div class="result-card" style="margin-bottom: var(--space-4);">
<h4>CAM 参数</h4>
<div class="info-list">
@@ -1973,62 +2164,9 @@ const ResultView = {
</div>
</template>
</div>
<div v-if="selectedHtmlFile" class="viewer-section">
<h3>3D 预览</h3>
<div v-if="state.previewStatus === 'error'" class="inline-alert inline-alert-warning" style="margin-bottom: var(--space-3);">
<div class="inline-alert-title">预览加载失败</div>
<div class="inline-alert-message">HTML 已生成但加载异常,请检查该链接是否可访问:{{ selectedHtmlFile }}</div>
</div>
<iframe
:key="selectedHtmlFile"
:src="selectedHtmlFile"
class="viewer-frame"
@load="onPreviewLoad"
@error="onPreviewError"
></iframe>
</div>
<div v-else class="viewer-section">
<h3>3D 预览</h3>
<div class="inline-alert inline-alert-warning">
<div class="inline-alert-title">预览未生成</div>
<div class="inline-alert-message">当前任务没有返回 HTML 预览链接,属于未生成状态。</div>
</div>
</div>
<div v-if="state.task.analysis_result" class="viewer-section">
<h3>分析结果详情</h3>
<!-- FreeCAD 验证结果 -->
<div v-if="state.task.verification" class="verification-section">
<div class="verification-summary">
<div class="summary-header">
<h4>FreeCAD 几何验证</h4>
<span :class="['badge', state.task.verification.status === 'passed' ? 'badge-success' : state.task.verification.status === 'failed' ? 'badge-error' : state.task.verification.status === 'error' ? 'badge-warning' : 'badge-info']">
{{ state.task.verification.status === 'passed' ? '验证通过' : state.task.verification.status === 'failed' ? '验证失败' : state.task.verification.status === 'error' ? '验证错误' : state.task.verification.status === 'skipped' ? '已跳过' : '未知' }}
</span>
</div>
<div class="verification-details" v-if="state.task.verification.comparison && (state.task.verification.comparison.volume || state.task.verification.comparison.surface_area)">
<div class="comparison-grid">
<div class="comparison-item" v-if="state.task.verification.comparison.volume">
<span class="comparison-label">体积差异</span>
<span class="comparison-value" :class="{'text-error': state.task.verification.comparison.volume.difference_percent > 1, 'text-success': state.task.verification.comparison.volume.difference_percent <= 1}">
{{ state.task.verification.comparison.volume.difference_percent?.toFixed(4) || 0 }}%
</span>
</div>
<div class="comparison-item" v-if="state.task.verification.comparison.surface_area">
<span class="comparison-label">表面积差异</span>
<span class="comparison-value" :class="{'text-error': state.task.verification.comparison.surface_area.difference_percent > 2, 'text-success': state.task.verification.comparison.surface_area.difference_percent <= 2}">
{{ state.task.verification.comparison.surface_area.difference_percent?.toFixed(4) || 0 }}%
</span>
</div>
</div>
</div>
</div>
</div>
<!-- 分析摘要 -->
<div class="analysis-summary" v-if="state.task.analysis_result.analysis_summary">
<div class="summary-header">
<h4>分析摘要</h4>
@@ -2038,7 +2176,7 @@ const ResultView = {
{{ state.task.analysis_result.analysis_summary }}
</div>
</div>
<div class="result-grid">
<div class="result-card full-width">
<h4>1. 产品特征识别</h4>
@@ -2052,19 +2190,19 @@ const ResultView = {
</div>
<div class="info-item">
<span class="info-label">壁厚分布</span>
<span class="info-value">{{ state.task.analysis_result.detected_features?.find(f => f.feature_type === 'wall_thickness')?.description || '待分析' }}</span>
<span class="info-value">{{ getWallThicknessSummary() }}</span>
</div>
<div class="info-item">
<span class="info-label">加强筋位置和密度</span>
<span class="info-value">{{ state.task.analysis_result.detected_features?.filter(f => f.feature_type === 'rib').length || 0 }} 个加强筋</span>
<span class="info-value">{{ countFeaturesByTypes('rib_structure') || 0 }} 个加强筋</span>
</div>
<div class="info-item">
<span class="info-label">孔洞和凹槽位置</span>
<span class="info-value">{{ state.task.analysis_result.detected_features?.filter(f => f.feature_type === 'hole' || f.feature_type === 'pocket').length || 0 }} 个孔洞/凹槽</span>
<span class="info-label">柱位和功能特征</span>
<span class="info-value">{{ countFeaturesByTypes('boss_feature') || 0 }} 个柱位/功能特征</span>
</div>
<div class="info-item">
<span class="info-label">倒扣区域检测</span>
<span class="info-value">{{ state.task.analysis_result.detected_features?.filter(f => f.feature_type === 'undercut').length || 0 }} 个倒扣区域</span>
<span class="info-value">{{ getUndercutCount() }} 个倒扣区域</span>
</div>
<div class="info-item">
<span class="info-label">对称性分析</span>
@@ -2081,27 +2219,27 @@ const ResultView = {
</div>
<div class="result-card full-width">
<h4>2. 泡沫包装设计决策</h4>
<h4>2. 注塑模设计决策</h4>
<div class="info-list">
<div class="info-item">
<span class="info-label">泡沫厚度建议</span>
<span class="info-value">{{ state.task.analysis_result.design_recommendations?.find(r => r.priority === 'high')?.recommendation || '根据产品重量和脆弱程度自动计算' }}</span>
<span class="info-label">高优先级工艺建议</span>
<span class="info-value">{{ state.task.analysis_result.design_recommendations?.find(r => r.priority === 'high')?.description || '根据几何分析自动生成' }}</span>
</div>
<div class="info-item">
<span class="info-label">加强筋布局</span>
<span class="info-value">基于产品薄弱区域自动布置</span>
<span class="info-value">{{ countFeaturesByTypes('rib_structure') ? '已识别加强筋区域,建议校核厚度和脱模方向' : '当前未识别明显加强筋特征' }}</span>
</div>
<div class="info-item">
<span class="info-label">取手槽位置</span>
<span class="info-value">基于重心位置:{{ state.task.analysis_result.geometry_data?.center_of_mass ? '自动优化' : '手动设置' }}</span>
<span class="info-label">浇口与进胶提示</span>
<span class="info-value">{{ state.task.analysis_result.design_recommendations?.find(r => r.type === 'wall_thickness' || r.type === 'draft_angle')?.reason || '建议结合主分型方向确认浇口位置' }}</span>
</div>
<div class="info-item">
<span class="info-label">通风孔位置</span>
<span class="info-value">防止真空吸附:建议在产品最大平面区域设置通风孔</span>
<span class="info-label">冷却设计提示</span>
<span class="info-value">{{ getWallThicknessSummary() !== '待分析' ? '建议按壁厚分布校核冷却均匀性' : '需先完成壁厚分析后再校核冷却' }}</span>
</div>
<div class="info-item">
<span class="info-label">定位结构设计</span>
<span class="info-value">{{ state.task.analysis_result.detected_features?.filter(f => f.feature_type === '定位').length || 0 }} 个定位特征</span>
<span class="info-value">{{ countFeaturesByTypes('boss_feature') || 0 }} 个柱位/定位相关特征</span>
</div>
<div class="info-item">
<span class="info-label">分型面选择</span>
@@ -2109,13 +2247,10 @@ const ResultView = {
</div>
<!-- 设计建议列表 -->
<div class="recommendations-section" v-if="state.task.analysis_result.design_recommendations?.length">
<div class="recommendations-section" v-if="sortedRecommendations.length">
<h5>详细设计建议</h5>
<div class="recommendations-list">
<div v-for="rec in state.task.analysis_result.design_recommendations.sort((a, b) => {
const priorityOrder = { 'critical': 0, 'high': 1, 'medium': 2, 'low': 3 };
return priorityOrder[a.priority] - priorityOrder[b.priority];
})" :key="rec.type" class="recommendation-item" :class="rec.priority">
<div v-for="rec in sortedRecommendations" :key="rec.type" class="recommendation-item" :class="rec.priority">
<span class="priority-badge" :class="rec.priority">{{ getPriorityText(rec.priority) }}</span>
<span class="recommendation-text">{{ rec.description }}</span>
<span class="recommendation-reason" v-if="rec.reason">({{ rec.reason }})</span>
@@ -2179,6 +2314,65 @@ const ResultView = {
</div>
</div>
</div>
<details class="viewer-section diagnostic-panel" v-if="stageTimingEntries.length || state.task.verification || state.task.llm_report">
<summary>诊断信息</summary>
<div class="diagnostic-panel-body">
<div class="result-card full-width" v-if="stageTimingEntries.length">
<h4>处理耗时</h4>
<table class="data-table">
<thead>
<tr>
<th>阶段</th>
<th>耗时</th>
</tr>
</thead>
<tbody>
<tr v-for="[stage, duration] in stageTimingEntries" :key="'timing-' + stage">
<td>{{ formatStageName(stage) }}</td>
<td>{{ formatTiming(duration) }}</td>
</tr>
</tbody>
</table>
</div>
<div v-if="state.task.verification" class="verification-section">
<div class="verification-summary">
<div class="summary-header">
<h4>FreeCAD 几何验证</h4>
<span :class="['badge', state.task.verification.status === 'passed' ? 'badge-success' : state.task.verification.status === 'failed' ? 'badge-error' : state.task.verification.status === 'error' ? 'badge-warning' : 'badge-info']">
{{ state.task.verification.status === 'passed' ? '验证通过' : state.task.verification.status === 'failed' ? '验证失败' : state.task.verification.status === 'error' ? '验证错误' : state.task.verification.status === 'skipped' ? '已跳过' : '未知' }}
</span>
</div>
<div class="verification-details" v-if="state.task.verification.comparison && (state.task.verification.comparison.volume || state.task.verification.comparison.surface_area)">
<div class="comparison-grid">
<div class="comparison-item" v-if="state.task.verification.comparison.volume">
<span class="comparison-label">体积差异</span>
<span class="comparison-value" :class="{'text-error': state.task.verification.comparison.volume.difference_percent > 1, 'text-success': state.task.verification.comparison.volume.difference_percent <= 1}">
{{ state.task.verification.comparison.volume.difference_percent?.toFixed(4) || 0 }}%
</span>
</div>
<div class="comparison-item" v-if="state.task.verification.comparison.surface_area">
<span class="comparison-label">表面积差异</span>
<span class="comparison-value" :class="{'text-error': state.task.verification.comparison.surface_area.difference_percent > 2, 'text-success': state.task.verification.comparison.surface_area.difference_percent <= 2}">
{{ state.task.verification.comparison.surface_area.difference_percent?.toFixed(4) || 0 }}%
</span>
</div>
</div>
</div>
</div>
</div>
<div class="analysis-summary" v-if="state.task.llm_report">
<div class="summary-header">
<h4>LLM 设计报告</h4>
</div>
<div class="summary-content">
<pre style="white-space: pre-wrap; margin: 0;">{{ state.task.llm_report }}</pre>
</div>
</div>
</div>
</details>
</div>
</div>
`