From 43fd3d258abcab311259287ecb714314e80feb50 Mon Sep 17 00:00:00 2001 From: 134 Date: Mon, 27 Jul 2026 22:57:40 +0800 Subject: [PATCH] feat: simplify HiCAD project management --- scripts/vendor-zoo-modeling-app.mjs | 288 +++++++++++++++++- tests/routes/hicad-project-experience.test.ts | 53 ++++ .../HicadProjectExperience.tsx.template | 68 +++++ vendor/zoo-modeling-app/localization.ts | 58 +++- 4 files changed, 465 insertions(+), 2 deletions(-) create mode 100644 tests/routes/hicad-project-experience.test.ts create mode 100644 vendor/zoo-modeling-app/HicadProjectExperience.tsx.template diff --git a/scripts/vendor-zoo-modeling-app.mjs b/scripts/vendor-zoo-modeling-app.mjs index 3046311d0..35d613c6a 100644 --- a/scripts/vendor-zoo-modeling-app.mjs +++ b/scripts/vendor-zoo-modeling-app.mjs @@ -110,7 +110,11 @@ function replaceSource(relativePath, before, after) { if (!after) { return; } - throw new Error(`Official source changed; patch failed for ${relativePath}`); + throw new Error( + `Official source changed; patch failed for ${relativePath}; expected ${JSON.stringify( + before.slice(0, 160) + )}` + ); } writeFileSync(filePath, source.replace(before, after)); } @@ -162,6 +166,10 @@ function patchOfficialSource() { path.join(customizationDir, "FitAllButton.tsx.template"), path.join(sourceDir, "src/components/FitAllButton.tsx") ); + copyFileSync( + path.join(customizationDir, "HicadProjectExperience.tsx.template"), + path.join(sourceDir, "src/components/HicadProjectExperience.tsx") + ); replaceSource( "index.html", ' ', @@ -307,6 +315,284 @@ function patchOfficialSource() { " {displayedName !== user.email && (", " {!isHiCadEmbedded && displayedName !== user.email && (" ); + replaceSource( + "src/components/AppHeader.tsx", + "import ProjectSidebarMenu from '@src/components/ProjectSidebarMenu'\n", + "import { HicadAutosaveStatus } from '@src/components/HicadProjectExperience'\nimport ProjectSidebarMenu from '@src/components/ProjectSidebarMenu'\n" + ); + replaceSource( + "src/components/AppHeader.tsx", + `
+ {children ||`, + `
+ {project && } + {children ||` + ); + replaceSource( + "src/routes/Home.tsx", + "import AppProjectCard from '@src/components/AppProjectCard/AppProjectCard'\n", + "import AppProjectCard from '@src/components/AppProjectCard/AppProjectCard'\nimport { HicadProjectDashboardHeading } from '@src/components/HicadProjectExperience'\n" + ); + replaceSource( + "src/routes/Home.tsx", + ` projectSearchKeybinding={projectSearchKeybinding} + className="col-start-2 -col-end-1"`, + ` projectSearchKeybinding={projectSearchKeybinding} + projectCount={scopedHomeProjectEntries.length} + className="col-start-2 -col-end-1"` + ); + replaceSource( + "src/routes/Home.tsx", + ` projectSearchKeybinding?: string +}`, + ` projectSearchKeybinding?: string + projectCount: number +}` + ); + replaceSource( + "src/routes/Home.tsx", + ` projectSearchKeybinding, + ...rest`, + ` projectSearchKeybinding, + projectCount, + ...rest` + ); + replaceSource( + "src/routes/Home.tsx", + `
+
+ {library && showLibraryBackLink && ( + + All libraries + + )} +

{title}

+
+
`, + `
+ {library && showLibraryBackLink && ( + + 返回全部项目 + + )} + +
` + ); + replaceSource( + "src/routes/Home.tsx", + `
`, + `
` + ); + replaceSource( + "src/routes/Home.tsx", + `
`, + `
` + ); + replaceSource( + "src/components/AppProjectCard/AppProjectCard.tsx", + "import { ContextMenu, ContextMenuItem } from '@src/components/ContextMenu'\n", + "import { ContextMenu, ContextMenuItem } from '@src/components/ContextMenu'\nimport { CustomIcon } from '@src/components/CustomIcon'\n" + ); + replaceSource( + "src/components/AppProjectCard/AppProjectCard.tsx", + ` { + local: 'Local', + 'cloud-only': 'Cloud-only', + syncing: 'Syncing', + synced: 'Synced', + conflicted: 'Conflicted', + }`, + ` { + local: '已保存', + 'cloud-only': '云端项目', + syncing: '保存中', + synced: '已保存', + conflicted: '需要处理', + }` + ); + replaceSource( + "src/components/AppProjectCard/AppProjectCard.tsx", + `function getDisplayedTime(dateTimeMs: number) { + const date = new Date(dateTimeMs) + const startOfToday = new Date() + startOfToday.setHours(0, 0, 0, 0) + return date.getTime() < startOfToday.getTime() + ? date.toLocaleDateString() + : date.toLocaleTimeString() +}`, + `function getDisplayedTime(dateTimeMs: number) { + const date = new Date(dateTimeMs) + const now = new Date() + const isToday = + date.getFullYear() === now.getFullYear() && + date.getMonth() === now.getMonth() && + date.getDate() === now.getDate() + + if (isToday) { + return \`今天 \${date.toLocaleTimeString('zh-CN', { + hour: '2-digit', + minute: '2-digit', + hour12: false, + })}\` + } + + return date.toLocaleDateString('zh-CN', { + year: date.getFullYear() === now.getFullYear() ? undefined : 'numeric', + month: 'short', + day: 'numeric', + }) +}` + ); + replaceSource( + "src/components/AppProjectCard/AppProjectCard.tsx", + ` {project.kclFileCount !== undefined && ( + + {project.kclFileCount}{' '} + file{project.kclFileCount === 1 ? '' : 's'}{' '} + {(project.directoryCount ?? 0) > 0 && ( + <> + {'/ '} + + {project.directoryCount} + {' '} + folder{project.directoryCount === 1 ? '' : 's'} + + )} + + )} + + Edited{' '} + + {displayedProject.modified + ? getDisplayedTime(displayedProject.modified) + : 'never'} + + `, + `
+ + {project.kclFileCount !== undefined ? ( + <> + {project.kclFileCount}{' '} + 个模型文件 + + ) : ( + 'CAD 项目' + )} + + + 最近编辑{' '} + + {displayedProject.modified + ? getDisplayedTime(displayedProject.modified) + : '暂无记录'} + + +
` + ); + replaceSource( + "src/components/AppProjectCard/AppProjectCard.tsx", + ` title="Delete Project"`, + ` title="删除项目"` + ); + replaceSource( + "src/components/AppProjectCard/AppProjectCard.tsx", + `

+ This will permanently delete "{projectName || 'this file'} + ". +

+

+ Are you sure you want to delete "{projectName || 'this file'} + "? This action cannot be undone. +

`, + `

+ 确定删除“{projectName || '此项目'}”吗? +

+

+ 项目及其全部模型文件将被永久删除,此操作无法撤销。 +

` + ); + replaceSource( + "src/components/AppProjectCard/AppProjectCard.tsx", + ` title={projectName} + titleText={projectName}`, + ` title={ + + {projectName} + {canOpen && ( + + 继续设计 + + )} + + } + titleText={projectName}` + ); + replaceSource( + "src/components/AppProjectCard/AppProjectCard.tsx", + ` thumbnailUrl={imageUrl} + badges={badges}`, + ` thumbnailUrl={imageUrl} + thumbnailAlt={projectName ? \`\${projectName} 的模型预览\` : 'CAD 项目预览'} + thumbnailFallback={ +
+ + 暂无模型预览 +
+ } + badges={badges}` + ); + replaceSource( + "src/components/AppProjectCard/AppProjectCard.tsx", + ` Rename project`, + ` 重命名` + ); + replaceSource( + "src/components/AppProjectCard/AppProjectCard.tsx", + ` Delete project`, + ` 删除项目` + ); + replaceSource( + "src/lib/commandBarConfigs/projectsCommandConfig.ts", + ` displayName: \`Create project\`, + description: 'Create a project',`, + ` displayName: \`新建 CAD 项目\`, + description: '输入项目名称即可开始,设计内容会自动保存。',` + ); + replaceSource( + "src/lib/commandBarConfigs/projectsCommandConfig.ts", + ` displayName: 'Title', + required: true,`, + ` displayName: '项目名称', + description: '使用容易识别的名称,例如“五金连接安装板”。', + required: true,` + ); + replaceSource( + "src/lib/commandBarConfigs/projectsCommandConfig.ts", + ` displayName: 'Library', + required: () => createProjectLibraryOptions().length > 1,`, + ` displayName: '保存位置', + required: () => createProjectLibraryOptions().length > 1,` + ); + replaceSource( + "src/lib/commandBarConfigs/projectsCommandConfig.ts", + ` defaultValue: defaultProjectFolderNameSnapshot,`, + ` defaultValue: () => '',` + ); + replaceSource( + "src/components/CommandBar/CommandBarBasicInput.tsx", + ` placeholder="Enter a value"`, + ` placeholder={ + commandBarState.context.selectedCommand?.name === 'Create project' + ? '例如:五金连接安装板' + : '请输入' + }` + ); replaceSource( "src/components/environment/Environment.tsx", " new URL(env().VITE_KITTYCAD_WEBSOCKET_URL ?? ''),", diff --git a/tests/routes/hicad-project-experience.test.ts b/tests/routes/hicad-project-experience.test.ts new file mode 100644 index 000000000..892a279b2 --- /dev/null +++ b/tests/routes/hicad-project-experience.test.ts @@ -0,0 +1,53 @@ +import fs from 'node:fs' +import path from 'node:path' + +import { expect, test } from '@playwright/test' + +const projectRoot = process.cwd() + +test.describe('HiCAD project management experience', () => { + test('vendors the project dashboard and autosave status', () => { + const script = fs.readFileSync( + path.join(projectRoot, 'scripts/vendor-zoo-modeling-app.mjs'), + 'utf8' + ) + const experience = fs.readFileSync( + path.join( + projectRoot, + 'vendor/zoo-modeling-app/HicadProjectExperience.tsx.template' + ), + 'utf8' + ) + + expect(script).toContain('HicadProjectExperience.tsx.template') + expect(script).toContain('projectCount={scopedHomeProjectEntries.length}') + expect(script).toContain('thumbnailFallback') + expect(script).toContain('最近编辑') + expect(experience).toContain('我的 CAD 项目') + expect(experience).toContain('编辑内容会自动保存') + expect(experience).toContain('自动保存已开启') + expect(experience).toContain('data-testid="hicad-create-project"') + expect(experience).toContain('data-testid="hicad-create-from-sample"') + }) + + test('keeps new project creation simple and understandable', () => { + const script = fs.readFileSync( + path.join(projectRoot, 'scripts/vendor-zoo-modeling-app.mjs'), + 'utf8' + ) + const localization = fs.readFileSync( + path.join(projectRoot, 'vendor/zoo-modeling-app/localization.ts'), + 'utf8' + ) + + expect(script).toContain('新建 CAD 项目') + expect(script).toContain("displayName: '项目名称'") + expect(script).toContain('例如:五金连接安装板') + expect(script).toContain("defaultValue: () => ''") + expect(localization).toContain("['Submit', '确认']") + expect(localization).toContain("['Continue', '下一步']") + expect(localization).toContain("['Step back', '返回上一步']") + expect(localization).toContain('还没有项目,点击“新建项目”开始设计。') + expect(localization).toContain("[data-testid='home-sidebar']") + }) +}) diff --git a/vendor/zoo-modeling-app/HicadProjectExperience.tsx.template b/vendor/zoo-modeling-app/HicadProjectExperience.tsx.template new file mode 100644 index 000000000..368ec661c --- /dev/null +++ b/vendor/zoo-modeling-app/HicadProjectExperience.tsx.template @@ -0,0 +1,68 @@ +import { CustomIcon } from '@src/components/CustomIcon' + +export function HicadProjectDashboardHeading({ + projectCount, +}: { + projectCount: number +}) { + const startCreatingProject = () => { + document + .querySelector('[data-testid="home-new-file"]') + ?.click() + } + const startFromSample = () => { + document + .querySelector( + '[data-testid="home-create-from-sample"]' + ) + ?.click() + } + + return ( +
+
+

HiCAD 项目空间

+

我的 CAD 项目

+

+ 共 {projectCount} 个项目,编辑内容会自动保存 +

+
+
+ + +
+
+ ) +} + +export function HicadAutosaveStatus() { + return ( + + + 自动保存已开启 + + ) +} diff --git a/vendor/zoo-modeling-app/localization.ts b/vendor/zoo-modeling-app/localization.ts index 37f02b274..6d08d092e 100644 --- a/vendor/zoo-modeling-app/localization.ts +++ b/vendor/zoo-modeling-app/localization.ts @@ -23,6 +23,8 @@ const translations = new Map([ ['Connected', '已连接'], ['Connecting and setting up scene...', '正在连接并初始化场景...'], ['Create a new project', '新建项目'], + ['Create project', '新建项目'], + ['Create from a sample', '从示例开始'], [ 'Create a gear with 10 teeth and use sensible defaults for everything else...', '创建一个 10 齿齿轮,其余参数使用合理默认值...', @@ -31,6 +33,7 @@ const translations = new Map([ ['Design using traditional point-and-click', '使用传统点击方式设计'], ['Dismiss', '关闭'], ['Edit', '编辑'], + ['Edited', '最近编辑'], ['Export', '导出'], ['Export part', '导出零件'], ['Extrude', '拉伸'], @@ -67,9 +70,28 @@ const translations = new Map([ ['No selection.', '未选择。'], ['Offset Plane', '偏移基准面'], ['Open project', '打开项目'], + ['Project Libraries', '我的 CAD 项目'], + ['Projects', '项目'], ['Project Files', '项目文件'], ['Publish', '发布'], ['Redo', '重做'], + ['Rename project', '重命名'], + ['Delete project', '删除项目'], + ['Search projects', '搜索项目'], + ['Sort by', '排序'], + ['Name', '名称'], + ['Last Modified', '最近编辑'], + ['No projects found', '还没有项目'], + [', ready to make your first one?', ',点击“新建项目”开始设计。'], + [ + 'No projects found, ready to make your first one?', + '还没有项目,点击“新建项目”开始设计。', + ], + ['Submit', '确认'], + ['Continue', '下一步'], + ['Step back', '返回上一步'], + ['Enter a value', '请输入'], + ['Cancel', '取消'], ['Revolve', '旋转'], ['Settings', '设置'], ['Shell', '抽壳'], @@ -158,7 +180,11 @@ function translateElement(element: Element) { for (const attribute of translatableAttributes) { const value = normalizeLookupKey(element.getAttribute(attribute) || '') if (!value) continue - const translated = translations.get(value) + const translated = + translations.get(value) || + (attribute === 'placeholder' && value.startsWith('Search projects') + ? '搜索项目' + : undefined) if (translated) element.setAttribute(attribute, translated) } } @@ -203,6 +229,36 @@ export function installHiCadLocalization() { html[data-hicad-embedded='true'] [data-hicad-official-account-link] { display: none !important; } + html[data-hicad-embedded='true'] [data-testid='home-sidebar'], + html[data-hicad-embedded='true'] [data-testid='home-new-file'], + html[data-hicad-embedded='true'] [data-testid='environment-subdomain-bar'], + html[data-hicad-embedded='true'] [data-testid='cloud-sync-status'] { + display: none !important; + } + html[data-hicad-embedded='true'] .home-layout { + display: block !important; + column-gap: 2rem; + margin-top: clamp(1.5rem, 5vh, 4rem); + } + html[data-hicad-embedded='true'] [data-testid='home-header'] > p:has( + [data-testid='project-directory-settings-link'] + ) { + display: none !important; + } + html[data-hicad-embedded='true'] [data-testid='home-section'] { + margin-top: 1.5rem; + overflow-y: auto; + padding-bottom: 3rem; + } + html[data-hicad-embedded='true'] [data-testid='home-section'] > ul { + align-content: start; + } + html[data-hicad-embedded='true'] [data-testid='project-status-badge'] { + box-shadow: 0 1px 2px rgb(0 0 0 / 8%); + } + html[data-hicad-embedded='true'] [data-testid='project-directory-settings-link'] { + overflow-wrap: anywhere; + } ` document.head.appendChild(style)