feat: brand HiCAD production sheets
This commit is contained in:
parent
7435e7b0f0
commit
d93b2e8ea7
@ -90,6 +90,8 @@ test.describe('HiCAD manufacturing extension', () => {
|
|||||||
projectName: '<工作台>',
|
projectName: '<工作台>',
|
||||||
generatedAt: '2026-07-27 20:00',
|
generatedAt: '2026-07-27 20:00',
|
||||||
overallDimensions: { x: 120, y: 60, z: 92 },
|
overallDimensions: { x: 120, y: 60, z: 92 },
|
||||||
|
brandName: '由壹点AI',
|
||||||
|
brandLogoUrl: '/api/favicon?surface=hicad',
|
||||||
snapshotDataUrl: 'data:image/png;base64,abc',
|
snapshotDataUrl: 'data:image/png;base64,abc',
|
||||||
bomRows: rows,
|
bomRows: rows,
|
||||||
})
|
})
|
||||||
@ -101,6 +103,8 @@ test.describe('HiCAD manufacturing extension', () => {
|
|||||||
expect(html).toContain('右视图')
|
expect(html).toContain('右视图')
|
||||||
expect(html).toContain('120 mm')
|
expect(html).toContain('120 mm')
|
||||||
expect(html).toContain('配件明细表(BOM)')
|
expect(html).toContain('配件明细表(BOM)')
|
||||||
|
expect(html).toContain('由壹点AI')
|
||||||
|
expect(html).toContain('/api/favicon?surface=hicad')
|
||||||
expect(html).toContain('@page { size: A3 landscape;')
|
expect(html).toContain('@page { size: A3 landscape;')
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -112,6 +116,9 @@ test.describe('HiCAD manufacturing extension', () => {
|
|||||||
projectName: '工作台生产图',
|
projectName: '工作台生产图',
|
||||||
generatedAt: '2026-07-27 20:00',
|
generatedAt: '2026-07-27 20:00',
|
||||||
overallDimensions: { x: 120, y: 60, z: 92 },
|
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,
|
bomRows: rows,
|
||||||
notes:
|
notes:
|
||||||
'材料、表面处理、公差、连接方式和检验标准须在投产前由工程人员复核。',
|
'材料、表面处理、公差、连接方式和检验标准须在投产前由工程人员复核。',
|
||||||
@ -125,11 +132,13 @@ test.describe('HiCAD manufacturing extension', () => {
|
|||||||
clientWidth: document.documentElement.clientWidth,
|
clientWidth: document.documentElement.clientWidth,
|
||||||
tableCount: document.querySelectorAll('table').length,
|
tableCount: document.querySelectorAll('table').length,
|
||||||
dimensionViewCount: document.querySelectorAll('.dimension-view').length,
|
dimensionViewCount: document.querySelectorAll('.dimension-view').length,
|
||||||
|
brandCount: document.querySelectorAll('.brand-name').length,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
expect(layout.scrollWidth).toBeLessThanOrEqual(layout.clientWidth)
|
expect(layout.scrollWidth).toBeLessThanOrEqual(layout.clientWidth)
|
||||||
expect(layout.tableCount).toBe(1)
|
expect(layout.tableCount).toBe(1)
|
||||||
expect(layout.dimensionViewCount).toBe(3)
|
expect(layout.dimensionViewCount).toBe(3)
|
||||||
|
expect(layout.brandCount).toBe(1)
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: testInfo.outputPath('engineering-sheet.png'),
|
path: testInfo.outputPath('engineering-sheet.png'),
|
||||||
fullPage: true,
|
fullPage: true,
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { useSingletons } from '@src/lib/boot'
|
|||||||
import { isModelingResponse } from '@src/lib/kcSdkGuards'
|
import { isModelingResponse } from '@src/lib/kcSdkGuards'
|
||||||
import { engineStreamZoomToFit, engineViewIsometric } from '@src/lib/utils'
|
import { engineStreamZoomToFit, engineViewIsometric } from '@src/lib/utils'
|
||||||
import { uuidv4 } 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 toast from 'react-hot-toast'
|
||||||
import {
|
import {
|
||||||
buildExplodeOffsets,
|
buildExplodeOffsets,
|
||||||
@ -31,6 +31,11 @@ type BomOverrides = Record<
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
||||||
|
type BrandSettings = {
|
||||||
|
name: string
|
||||||
|
logoUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
function downloadText(filename: string, text: string, type: string) {
|
function downloadText(filename: string, text: string, type: string) {
|
||||||
const blob = new Blob([text], { type })
|
const blob = new Blob([text], { type })
|
||||||
const url = URL.createObjectURL(blob)
|
const url = URL.createObjectURL(blob)
|
||||||
@ -116,12 +121,48 @@ export function HicadManufacturingPanel({
|
|||||||
const [snapshot, setSnapshot] = useState<string>()
|
const [snapshot, setSnapshot] = useState<string>()
|
||||||
const [exploded, setExploded] = useState(false)
|
const [exploded, setExploded] = useState(false)
|
||||||
const [overrides, setOverrides] = useState<BomOverrides>({})
|
const [overrides, setOverrides] = useState<BomOverrides>({})
|
||||||
|
const [brand, setBrand] = useState<BrandSettings>({
|
||||||
|
name: '由壹点AI',
|
||||||
|
logoUrl: '/api/favicon?surface=hicad',
|
||||||
|
})
|
||||||
const [notes, setNotes] = useState(
|
const [notes, setNotes] = useState(
|
||||||
'材料、表面处理、公差、连接方式和检验标准须在投产前由工程人员复核。'
|
'材料、表面处理、公差、连接方式和检验标准须在投产前由工程人员复核。'
|
||||||
)
|
)
|
||||||
const explodeOffsetsRef = useRef<ExplodeOffset[]>([])
|
const explodeOffsetsRef = useRef<ExplodeOffset[]>([])
|
||||||
const resolvedProjectName = projectName || 'HiCAD 模型'
|
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 sendModelingCommand = async (cmd: ModelingCmd) => {
|
||||||
const response = await kclManager.engineCommandManager.sendSceneCommand({
|
const response = await kclManager.engineCommandManager.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
@ -387,6 +428,8 @@ export function HicadManufacturingPanel({
|
|||||||
projectName: resolvedProjectName,
|
projectName: resolvedProjectName,
|
||||||
generatedAt: new Date().toLocaleString('zh-CN'),
|
generatedAt: new Date().toLocaleString('zh-CN'),
|
||||||
overallDimensions: overall.dimensions,
|
overallDimensions: overall.dimensions,
|
||||||
|
brandName: brand.name,
|
||||||
|
brandLogoUrl: brand.logoUrl,
|
||||||
snapshotDataUrl: snapshot,
|
snapshotDataUrl: snapshot,
|
||||||
bomRows,
|
bomRows,
|
||||||
notes,
|
notes,
|
||||||
|
|||||||
26
vendor/zoo-modeling-app/manufacturing.ts
vendored
26
vendor/zoo-modeling-app/manufacturing.ts
vendored
@ -35,6 +35,8 @@ export type EngineeringSheetInput = {
|
|||||||
projectName: string
|
projectName: string
|
||||||
generatedAt: string
|
generatedAt: string
|
||||||
overallDimensions: Point3d
|
overallDimensions: Point3d
|
||||||
|
brandName?: string
|
||||||
|
brandLogoUrl?: string
|
||||||
snapshotDataUrl?: string
|
snapshotDataUrl?: string
|
||||||
bomRows: BomRow[]
|
bomRows: BomRow[]
|
||||||
notes?: string
|
notes?: string
|
||||||
@ -328,6 +330,10 @@ export function createEngineeringSheetHtml(input: EngineeringSheetInput) {
|
|||||||
const snapshot = input.snapshotDataUrl
|
const snapshot = input.snapshotDataUrl
|
||||||
? `<img class="snapshot" src="${escapeHtml(input.snapshotDataUrl)}" alt="爆炸图"/>`
|
? `<img class="snapshot" src="${escapeHtml(input.snapshotDataUrl)}" alt="爆炸图"/>`
|
||||||
: '<div class="snapshot-empty">暂无爆炸图快照</div>'
|
: '<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>
|
return `<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
@ -338,13 +344,20 @@ export function createEngineeringSheetHtml(input: EngineeringSheetInput) {
|
|||||||
@page { size: A3 landscape; margin: 12mm; }
|
@page { size: A3 landscape; margin: 12mm; }
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
body { margin: 0; color: #111827; font-family: Arial, "Microsoft YaHei", sans-serif; }
|
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; }
|
h1 { margin: 0; font-size: 24px; }
|
||||||
h2 { margin: 0 0 8px; font-size: 16px; }
|
h2 { margin: 0 0 8px; font-size: 16px; }
|
||||||
p { margin: 4px 0; }
|
p { margin: 4px 0; }
|
||||||
.meta { color: #4b5563; font-size: 12px; text-align: right; }
|
.meta { color: #4b5563; font-size: 12px; text-align: right; }
|
||||||
.sheet-grid { display: grid; grid-template-columns: 1.05fr 1fr; gap: 16px; margin-top: 14px; }
|
.sheet-grid { display: grid; grid-template-columns: 1.05fr 1fr; gap: 16px; margin-top: 14px; }
|
||||||
section { break-inside: avoid; }
|
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 { 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; }
|
.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; }
|
.views { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
|
||||||
@ -362,7 +375,11 @@ export function createEngineeringSheetHtml(input: EngineeringSheetInput) {
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<div>
|
<div class="brand">
|
||||||
|
${brandLogo}
|
||||||
|
<span class="brand-name">${brandName}</span>
|
||||||
|
</div>
|
||||||
|
<div class="title">
|
||||||
<h1>${escapeHtml(input.projectName)} · 生产工艺图</h1>
|
<h1>${escapeHtml(input.projectName)} · 生产工艺图</h1>
|
||||||
<p>总体尺寸:${formatDimension(dimensions.x)} × ${formatDimension(dimensions.y)} × ${formatDimension(dimensions.z)}</p>
|
<p>总体尺寸:${formatDimension(dimensions.x)} × ${formatDimension(dimensions.y)} × ${formatDimension(dimensions.z)}</p>
|
||||||
</div>
|
</div>
|
||||||
@ -371,7 +388,10 @@ export function createEngineeringSheetHtml(input: EngineeringSheetInput) {
|
|||||||
<main class="sheet-grid">
|
<main class="sheet-grid">
|
||||||
<section>
|
<section>
|
||||||
<h2>装配爆炸图</h2>
|
<h2>装配爆炸图</h2>
|
||||||
${snapshot}
|
<div class="snapshot-wrap">
|
||||||
|
<div class="snapshot-brand">${brandLogo}<span>${brandName}</span></div>
|
||||||
|
${snapshot}
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h2>总体三视图与尺寸</h2>
|
<h2>总体三视图与尺寸</h2>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user