feat: brand HiCAD production sheets

This commit is contained in:
134 2026-07-27 19:57:44 +08:00 committed by root
parent 7435e7b0f0
commit d93b2e8ea7
3 changed files with 76 additions and 4 deletions

View File

@ -90,6 +90,8 @@ test.describe('HiCAD manufacturing extension', () => {
projectName: '<工作台>',
generatedAt: '2026-07-27 20:00',
overallDimensions: { x: 120, y: 60, z: 92 },
brandName: '由壹点AI',
brandLogoUrl: '/api/favicon?surface=hicad',
snapshotDataUrl: 'data:image/png;base64,abc',
bomRows: rows,
})
@ -101,6 +103,8 @@ test.describe('HiCAD manufacturing extension', () => {
expect(html).toContain('右视图')
expect(html).toContain('120 mm')
expect(html).toContain('配件明细表(BOM)')
expect(html).toContain('由壹点AI')
expect(html).toContain('/api/favicon?surface=hicad')
expect(html).toContain('@page { size: A3 landscape;')
})
@ -112,6 +116,9 @@ test.describe('HiCAD manufacturing extension', () => {
projectName: '工作台生产图',
generatedAt: '2026-07-27 20:00',
overallDimensions: { x: 120, y: 60, z: 92 },
brandName: '由壹点AI',
brandLogoUrl:
'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="80" height="80"%3E%3Crect width="80" height="80" fill="%23111827"/%3E%3Cpath d="M20 20h40v40H20z" fill="white"/%3E%3C/svg%3E',
bomRows: rows,
notes:
'材料、表面处理、公差、连接方式和检验标准须在投产前由工程人员复核。',
@ -125,11 +132,13 @@ test.describe('HiCAD manufacturing extension', () => {
clientWidth: document.documentElement.clientWidth,
tableCount: document.querySelectorAll('table').length,
dimensionViewCount: document.querySelectorAll('.dimension-view').length,
brandCount: document.querySelectorAll('.brand-name').length,
}))
expect(layout.scrollWidth).toBeLessThanOrEqual(layout.clientWidth)
expect(layout.tableCount).toBe(1)
expect(layout.dimensionViewCount).toBe(3)
expect(layout.brandCount).toBe(1)
await page.screenshot({
path: testInfo.outputPath('engineering-sheet.png'),
fullPage: true,

View File

@ -5,7 +5,7 @@ import { useSingletons } from '@src/lib/boot'
import { isModelingResponse } from '@src/lib/kcSdkGuards'
import { engineStreamZoomToFit, engineViewIsometric } from '@src/lib/utils'
import { uuidv4 } from '@src/lib/utils'
import { useMemo, useRef, useState } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import toast from 'react-hot-toast'
import {
buildExplodeOffsets,
@ -31,6 +31,11 @@ type BomOverrides = Record<
}
>
type BrandSettings = {
name: string
logoUrl: string
}
function downloadText(filename: string, text: string, type: string) {
const blob = new Blob([text], { type })
const url = URL.createObjectURL(blob)
@ -116,12 +121,48 @@ export function HicadManufacturingPanel({
const [snapshot, setSnapshot] = useState<string>()
const [exploded, setExploded] = useState(false)
const [overrides, setOverrides] = useState<BomOverrides>({})
const [brand, setBrand] = useState<BrandSettings>({
name: '由壹点AI',
logoUrl: '/api/favicon?surface=hicad',
})
const [notes, setNotes] = useState(
'材料、表面处理、公差、连接方式和检验标准须在投产前由工程人员复核。'
)
const explodeOffsetsRef = useRef<ExplodeOffset[]>([])
const resolvedProjectName = projectName || 'HiCAD 模型'
useEffect(() => {
let active = true
void fetch('/api/site-settings?scope=chat', {
credentials: 'same-origin',
})
.then((response) => {
if (!response.ok) throw new Error('Unable to load site settings')
return response.json() as Promise<Record<string, unknown>>
})
.then((settings) => {
if (!active) return
const name =
typeof settings.siteName === 'string' && settings.siteName.trim()
? settings.siteName.trim()
: '由壹点AI'
const configuredLogo =
typeof settings.siteLogo === 'string' && settings.siteLogo.trim()
? settings.siteLogo.trim()
: typeof settings.siteLogoDark === 'string' &&
settings.siteLogoDark.trim()
? settings.siteLogoDark.trim()
: '/api/favicon?surface=hicad'
setBrand({ name, logoUrl: configuredLogo })
})
.catch(() => {
// Keep the built-in website branding when settings are unavailable.
})
return () => {
active = false
}
}, [])
const sendModelingCommand = async (cmd: ModelingCmd) => {
const response = await kclManager.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
@ -387,6 +428,8 @@ export function HicadManufacturingPanel({
projectName: resolvedProjectName,
generatedAt: new Date().toLocaleString('zh-CN'),
overallDimensions: overall.dimensions,
brandName: brand.name,
brandLogoUrl: brand.logoUrl,
snapshotDataUrl: snapshot,
bomRows,
notes,

View File

@ -35,6 +35,8 @@ export type EngineeringSheetInput = {
projectName: string
generatedAt: string
overallDimensions: Point3d
brandName?: string
brandLogoUrl?: string
snapshotDataUrl?: string
bomRows: BomRow[]
notes?: string
@ -328,6 +330,10 @@ export function createEngineeringSheetHtml(input: EngineeringSheetInput) {
const snapshot = input.snapshotDataUrl
? `<img class="snapshot" src="${escapeHtml(input.snapshotDataUrl)}" alt="爆炸图"/>`
: '<div class="snapshot-empty">暂无爆炸图快照</div>'
const brandName = escapeHtml(input.brandName || '由壹点AI')
const brandLogo = input.brandLogoUrl
? `<img class="brand-logo" src="${escapeHtml(input.brandLogoUrl)}" alt="${brandName}"/>`
: ''
return `<!doctype html>
<html lang="zh-CN">
@ -338,13 +344,20 @@ export function createEngineeringSheetHtml(input: EngineeringSheetInput) {
@page { size: A3 landscape; margin: 12mm; }
* { box-sizing: border-box; }
body { margin: 0; color: #111827; font-family: Arial, "Microsoft YaHei", sans-serif; }
header { display: flex; align-items: end; justify-content: space-between; border-bottom: 2px solid #111827; padding-bottom: 8px; }
header { display: flex; align-items: end; justify-content: space-between; gap: 24px; border-bottom: 2px solid #111827; padding-bottom: 8px; }
.brand { display: flex; align-items: center; gap: 10px; min-width: 170px; }
.brand-logo { width: 38px; height: 38px; object-fit: contain; }
.brand-name { font-size: 18px; font-weight: 700; white-space: nowrap; }
.title { flex: 1; }
h1 { margin: 0; font-size: 24px; }
h2 { margin: 0 0 8px; font-size: 16px; }
p { margin: 4px 0; }
.meta { color: #4b5563; font-size: 12px; text-align: right; }
.sheet-grid { display: grid; grid-template-columns: 1.05fr 1fr; gap: 16px; margin-top: 14px; }
section { break-inside: avoid; }
.snapshot-wrap { position: relative; }
.snapshot-brand { position: absolute; left: 10px; top: 10px; z-index: 1; display: flex; align-items: center; gap: 6px; padding: 5px 8px; background: rgba(255,255,255,.9); border: 1px solid #d1d5db; font-size: 11px; font-weight: 700; }
.snapshot-brand img { width: 20px; height: 20px; object-fit: contain; }
.snapshot { width: 100%; height: 310px; object-fit: contain; border: 1px solid #9ca3af; }
.snapshot-empty { height: 310px; display: grid; place-items: center; border: 1px dashed #9ca3af; color: #6b7280; }
.views { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
@ -362,7 +375,11 @@ export function createEngineeringSheetHtml(input: EngineeringSheetInput) {
</head>
<body>
<header>
<div>
<div class="brand">
${brandLogo}
<span class="brand-name">${brandName}</span>
</div>
<div class="title">
<h1>${escapeHtml(input.projectName)} · 生产工艺图</h1>
<p>总体尺寸:${formatDimension(dimensions.x)} × ${formatDimension(dimensions.y)} × ${formatDimension(dimensions.z)}</p>
</div>
@ -371,7 +388,10 @@ export function createEngineeringSheetHtml(input: EngineeringSheetInput) {
<main class="sheet-grid">
<section>
<h2>装配爆炸图</h2>
${snapshot}
<div class="snapshot-wrap">
<div class="snapshot-brand">${brandLogo}<span>${brandName}</span></div>
${snapshot}
</div>
</section>
<section>
<h2>总体三视图与尺寸</h2>