diff --git a/static/style.css b/static/style.css index 84301de..d2e7d7d 100644 --- a/static/style.css +++ b/static/style.css @@ -1364,6 +1364,74 @@ optgroup { gap: var(--space-4); } +.mechanism-visual-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); + gap: var(--space-4); + margin-top: var(--space-4); +} + +.mechanism-visual-card { + display: grid; + grid-template-columns: 220px minmax(0, 1fr); + gap: 0; + border: 1px solid var(--border-light); + border-radius: var(--radius-xl); + background: var(--bg-primary); + overflow: hidden; +} + +.mechanism-visual-svg { + background: linear-gradient(180deg, #eff6ff 0%, #f8fafc 100%); + border-right: 1px solid var(--border-light); + display: flex; + align-items: center; + justify-content: center; + min-height: 180px; +} + +.mechanism-svg { + width: 100%; + height: 100%; +} + +.mechanism-visual-body { + padding: var(--space-4); + display: flex; + flex-direction: column; + gap: var(--space-4); +} + +.mechanism-kpis { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--space-3); +} + +.mechanism-kpi { + padding: var(--space-3); + border-radius: var(--radius-lg); + background: var(--bg-secondary); + border: 1px solid var(--border-light); +} + +.mechanism-kpi-label { + display: block; + font-size: var(--text-xs); + color: var(--text-tertiary); + text-transform: uppercase; + letter-spacing: 0.04em; + margin-bottom: var(--space-1); +} + +.mechanism-kpi-value { + display: block; + font-size: var(--text-sm); + font-weight: var(--font-semibold); + color: var(--text-primary); + word-break: break-word; +} + .mechanism-card { border: 1px solid var(--border-light); border-radius: var(--radius-lg); @@ -1392,7 +1460,7 @@ optgroup { } .mechanism-actions { - margin-top: var(--space-4); + margin-top: auto; display: flex; justify-content: flex-end; } @@ -1480,6 +1548,19 @@ optgroup { .info-list { grid-template-columns: 1fr; } + + .mechanism-visual-card { + grid-template-columns: 1fr; + } + + .mechanism-visual-svg { + border-right: 0; + border-bottom: 1px solid var(--border-light); + } + + .mechanism-kpis { + grid-template-columns: 1fr; + } } .info-item { @@ -1586,6 +1667,11 @@ optgroup { background: var(--warning-bg); } +.recommendation-item.critical { + border-left-color: var(--error); + background: #fef2f2; +} + .recommendation-item.medium { border-left-color: var(--info); background: var(--info-bg); @@ -1612,6 +1698,11 @@ optgroup { color: white; } +.priority-badge.critical { + background: var(--error); + color: white; +} + .priority-badge.medium { background: var(--info); color: white; @@ -1628,6 +1719,123 @@ optgroup { color: var(--text-primary); } +.dfm-card-list { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: var(--space-4); +} + +.dfm-card { + border-radius: var(--radius-xl); + border: 1px solid var(--border-light); + padding: var(--space-4); + background: var(--bg-primary); +} + +.dfm-card-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-3); + margin-bottom: var(--space-3); +} + +.dfm-card-message { + font-size: var(--text-sm); + color: var(--text-primary); + line-height: 1.7; +} + +.dfm-card-hint { + margin-top: var(--space-3); + font-size: var(--text-xs); + color: var(--text-tertiary); +} + +.dfm-level-critical { + border-color: #fecaca; + background: #fef2f2; +} + +.dfm-level-high { + border-color: #fde68a; + background: #fff7ed; +} + +.dfm-level-medium { + border-color: #bfdbfe; + background: #eff6ff; +} + +.dfm-level-low { + border-color: #bbf7d0; + background: #f0fdf4; +} + +.markdown-preview { + color: var(--text-secondary); + font-size: var(--text-sm); + line-height: 1.75; +} + +.markdown-preview h1, +.markdown-preview h2, +.markdown-preview h3, +.markdown-preview h4, +.markdown-preview h5, +.markdown-preview h6 { + color: var(--text-primary); + margin: 0 0 var(--space-3); +} + +.markdown-preview p, +.markdown-preview ul, +.markdown-preview ol, +.markdown-preview blockquote, +.markdown-preview pre { + margin: 0 0 var(--space-4); +} + +.markdown-preview ul, +.markdown-preview ol { + padding-left: 1.25rem; +} + +.markdown-preview code { + font-family: var(--font-mono); + background: var(--bg-secondary); + border: 1px solid var(--border-light); + border-radius: var(--radius-sm); + padding: 0.1rem 0.35rem; +} + +.markdown-preview pre { + padding: var(--space-4); + border-radius: var(--radius-lg); + background: var(--gray-900); + overflow-x: auto; +} + +.markdown-preview pre code { + background: transparent; + border: 0; + color: #f8fafc; + padding: 0; +} + +.markdown-preview blockquote { + padding: var(--space-3) var(--space-4); + border-left: 4px solid var(--primary-300); + background: var(--primary-50); + border-radius: var(--radius-md); + color: var(--text-secondary); +} + +.markdown-preview a { + color: var(--primary-700); + text-decoration: underline; +} + .recommendation-reason { font-size: var(--text-xs); color: var(--text-tertiary); diff --git a/static/vue-app.js b/static/vue-app.js index 27c32d4..991a37f 100644 --- a/static/vue-app.js +++ b/static/vue-app.js @@ -1520,6 +1520,24 @@ const ResultView = { const sliderMechanisms = computed(() => selectedSideActions.value?.slider_mechanisms || []); const lifterMechanisms = computed(() => selectedSideActions.value?.lifter_mechanisms || []); const sideActionRecommendations = computed(() => selectedSideActions.value?.recommendations || []); + const allMechanisms = computed(() => { + const sliders = sliderMechanisms.value.map((item, index) => ({ type: 'slider', index, item })); + const lifters = lifterMechanisms.value.map((item, index) => ({ type: 'lifter', index, item })); + return [...sliders, ...lifters]; + }); + const dfmLevelSummary = computed(() => { + const base = { critical: 0, high: 0, medium: 0, low: 0 }; + (selectedDfmViolations.value || []).forEach((item) => { + const level = String(item.level || 'medium').toLowerCase(); + if (base[level] === undefined) { + base.medium += 1; + } else { + base[level] += 1; + } + }); + return base; + }); + const llmReportHtml = computed(() => renderMarkdownToHtml(state.task?.llm_report || '')); const stageTimingEntries = computed(() => { const timings = state.task?.stage_timings || {}; return Object.entries(timings) @@ -1553,8 +1571,7 @@ const ResultView = { { id: 'side-actions', label: '抽芯建议' }, { id: 'dfm-check', label: 'DFM' }, { id: 'export-cam', label: '导出/CAM' }, - { id: 'analysis-details', label: '分析详情' }, - { id: 'diagnostic-info', label: '诊断' } + { id: 'llm-report', label: '设计报告' } ]; const selectScheme = (schemeId) => { @@ -1627,6 +1644,138 @@ const ResultView = { if (type === 'lifter') return '斜顶抽芯'; return type || '机构'; }; + const formatMechanismTypeShort = (type) => type === 'slider' ? '滑块' : type === 'lifter' ? '斜顶' : '机构'; + const normalizeVector = (vector, fallback = [0.8, -0.6, 0.5]) => { + if (!Array.isArray(vector) || vector.length < 3) return fallback; + const values = vector.map(v => Number(v) || 0); + const length = Math.sqrt(values.reduce((acc, val) => acc + val * val, 0)); + if (!length) return fallback; + return values.map(v => v / length); + }; + const mechanismSchematicSvg = (type, item = {}) => { + const direction = normalizeVector(type === 'slider' ? item.slide_direction : item.location); + const location = normalizeVector(item.location, [0.3, 0.2, 0.5]); + const startX = 120 + (location[0] * 18); + const startY = 90 - (location[1] * 16); + const endX = startX + (direction[0] * 42); + const endY = startY - (direction[2] * 28) + (direction[1] * 12); + const mechanismColor = type === 'slider' ? '#2563eb' : '#7c3aed'; + const label = type === 'slider' ? '滑块抽芯' : '斜顶抽芯'; + return ` + + + + + + + + + + + + + + + + + + + + + ${label} + 基于机构方向与位置生成的三维示意 + + `; + }; + const escapeHtml = (value) => String(value || '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + const applyInlineMarkdown = (text) => { + let html = escapeHtml(text); + html = html.replace(/`([^`]+)`/g, '$1'); + html = html.replace(/\*\*([^*]+)\*\*/g, '$1'); + html = html.replace(/\*([^*]+)\*/g, '$1'); + html = html.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '$1'); + return html; + }; + const renderMarkdownToHtml = (markdown) => { + const source = String(markdown || '').replace(/\r\n/g, '\n').trim(); + if (!source) return ''; + const lines = source.split('\n'); + const html = []; + let inList = false; + let inCode = false; + let codeBuffer = []; + const closeList = () => { + if (inList) { + html.push(''); + inList = false; + } + }; + const closeCode = () => { + if (inCode) { + html.push(`
${escapeHtml(codeBuffer.join('\n'))}
`); + inCode = false; + codeBuffer = []; + } + }; + lines.forEach((rawLine) => { + const line = rawLine.trimEnd(); + if (line.startsWith('```')) { + closeList(); + if (inCode) { + closeCode(); + } else { + inCode = true; + codeBuffer = []; + } + return; + } + if (inCode) { + codeBuffer.push(rawLine); + return; + } + const trimmed = line.trim(); + if (!trimmed) { + closeList(); + html.push(''); + return; + } + const headingMatch = trimmed.match(/^(#{1,6})\s+(.*)$/); + if (headingMatch) { + closeList(); + const level = headingMatch[1].length; + html.push(`${applyInlineMarkdown(headingMatch[2])}`); + return; + } + if (/^[-*]\s+/.test(trimmed)) { + if (!inList) { + html.push('