`
+ );
+ 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)