feat(installation-demo): 工作台顶栏改为目录/制作/分享并新增工具抽屉与制作流程卡
- 标签行:模型→目录、详情→制作;工具从跳转链接改为面板内抽屉,勾选即保存,完整工具箱保留次级入口
This commit is contained in:
parent
614e3dabe2
commit
b0a6cdd1e7
@ -109,6 +109,7 @@ import { useInstallationStepCameraPlayback } from "./hooks/useInstallationStepCa
|
||||
import { useInstallationStepEditor } from "./hooks/useInstallationStepEditor";
|
||||
import { useInstallationStepMaterialThumbnails } from "./hooks/useInstallationStepMaterialThumbnails";
|
||||
import { InstallationInspector } from "./components/InstallationInspector";
|
||||
import { computeInstallationStageReadiness } from "./lib/installationWorkflowReadiness";
|
||||
import { InstallationPreviewStage } from "./components/InstallationPreviewStage";
|
||||
import { InstallationFinishedMiniPreview } from "./components/InstallationFinishedMiniPreview";
|
||||
import { InstallationPartTexturePanel } from "./components/InstallationPartTexturePanel";
|
||||
@ -2005,6 +2006,19 @@ export default function InstallationDemoWorkbenchClient({
|
||||
buildShareInput: buildCurrentShareInput,
|
||||
});
|
||||
|
||||
// 制作流程完成度(工作台体验移植自 V2)
|
||||
const stageReadiness = useMemo(
|
||||
() =>
|
||||
computeInstallationStageReadiness({
|
||||
modelLoaded: Boolean(modelUrl),
|
||||
parts: partCatalog.parts,
|
||||
numberingParts: numberingCatalog.parts,
|
||||
steps: stepEditor.steps,
|
||||
materialCount: Object.keys(nodeMaterials.nodeMaterialConfigs ?? {}).length,
|
||||
}),
|
||||
[modelUrl, partCatalog.parts, numberingCatalog.parts, stepEditor.steps, nodeMaterials.nodeMaterialConfigs],
|
||||
);
|
||||
|
||||
const handleConfirmedQrShare = useCallback(
|
||||
(share: InstallationDemoQrShareDTO) => {
|
||||
setQrShareUrl(share.url);
|
||||
@ -2877,6 +2891,7 @@ export default function InstallationDemoWorkbenchClient({
|
||||
hardwareHingeContext={{ engine: previewEngine, modelNodeNames, parts: numberingCatalog.parts,
|
||||
steps: stepEditor.steps, templates: drawingTools.tools }}
|
||||
activeTab={activeTab}
|
||||
stages={stageReadiness}
|
||||
onTabChange={(nextTab) => {
|
||||
setActiveTab(nextTab);
|
||||
if (nextTab === "share") {
|
||||
|
||||
@ -14,14 +14,17 @@ import type {
|
||||
import { ExplosionEditorPanel } from './ExplosionEditorPanel'
|
||||
import { InstallationDrawingToolSelection } from './InstallationDrawingToolSelection'
|
||||
import { InstallationToolsInventoryBoard } from './InstallationToolsInventoryBoard'
|
||||
import { InstallationWorkflowFooterNav, InstallationWorkflowNavigator } from './InstallationWorkflowNavigator'
|
||||
import { PartCatalogSection } from './PartCatalogSection'
|
||||
import { StepEditingPanel } from './StepEditingPanel'
|
||||
import type { InstallationStageReadiness } from '../lib/installationWorkflowReadiness'
|
||||
import type { InstallationHardwareHingeEditorContext } from './InstallationHardwareHingeEditor'
|
||||
|
||||
interface InstallationDetailsPanelProps {
|
||||
hardwareHingeContext?: InstallationHardwareHingeEditorContext
|
||||
detailView: InstallationDetailView
|
||||
onDetailViewChange: (view: InstallationDetailView) => void
|
||||
stages: InstallationStageReadiness[]
|
||||
canEdit: boolean
|
||||
modelNodeNames: string[]
|
||||
parts: InstallationPartItem[]
|
||||
@ -88,18 +91,6 @@ interface InstallationDetailsPanelProps {
|
||||
partTextureEditor?: ReactNode
|
||||
}
|
||||
|
||||
const DETAIL_STAGE_ITEMS: Array<{
|
||||
value: InstallationDetailView
|
||||
title: string
|
||||
description: string
|
||||
}> = [
|
||||
{ value: 'finished', title: '第一步:成品展示', description: '固定展示产品安装完成后的样子。' },
|
||||
{ value: 'exploded', title: '第二步:爆炸图', description: '固定展示产品拆开的状态。' },
|
||||
{ value: 'numbering', title: '第三步:部件编号', description: '按编号顺序逐组展示,并支持折角引线标注。' },
|
||||
{ value: 'materials', title: '第四步:部件贴图', description: '按板件编号绑定颜色、纹理和方向。' },
|
||||
{ value: 'steps', title: '第五步:安装步骤', description: '进入后续安装步骤与动画设置。' },
|
||||
]
|
||||
|
||||
const NUMBERING_CATEGORY_TABS: Array<{
|
||||
value: InstallationNumberingDisplayCategory
|
||||
label: string
|
||||
@ -204,6 +195,7 @@ export function InstallationDetailsPanel({
|
||||
hardwareHingeContext,
|
||||
detailView,
|
||||
onDetailViewChange,
|
||||
stages,
|
||||
canEdit,
|
||||
modelNodeNames,
|
||||
parts,
|
||||
@ -272,36 +264,9 @@ export function InstallationDetailsPanel({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-3">
|
||||
<section className="mb-3 rounded-[8px] border border-black/[0.06] bg-white">
|
||||
<div className="border-b border-black/[0.06] px-3 py-2">
|
||||
<p className="text-[11px] font-black text-[#2d3335]">详情阶段</p>
|
||||
</div>
|
||||
<div className="grid gap-2 p-3">
|
||||
{DETAIL_STAGE_ITEMS.map((item) => {
|
||||
const active = item.value === detailView
|
||||
return (
|
||||
<button
|
||||
key={item.value}
|
||||
type="button"
|
||||
onClick={() => onDetailViewChange(item.value)}
|
||||
className={[
|
||||
'rounded-[8px] border px-3 py-2 text-left transition',
|
||||
active
|
||||
? 'border-[#2d3335]/30 bg-[#f5f7f8]'
|
||||
: 'border-black/[0.06] bg-white hover:bg-black/[0.02]',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="block text-[12px] font-black text-[#2d3335]">{item.title}</span>
|
||||
<span className="mt-1 block text-[10px] font-semibold text-[#8b9092]">
|
||||
{item.description}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
<InstallationWorkflowNavigator stages={stages} current={detailView} onChange={onDetailViewChange} />
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-3">
|
||||
{detailView === 'numbering' || detailView === 'materials' ? (
|
||||
<div className="mb-3">{finishedPreview}</div>
|
||||
) : null}
|
||||
@ -436,9 +401,9 @@ export function InstallationDetailsPanel({
|
||||
<section className="mt-3 rounded-[8px] border border-[#2d3335]/15 bg-white p-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="text-[12px] font-black text-[#2d3335]">安装图纸</p>
|
||||
<p className="text-[12px] font-black text-[#2d3335]">出图与分享</p>
|
||||
<p className="mt-1 text-[10px] font-semibold leading-4 text-[#8b9092]">
|
||||
先打开固定模板,再手动截取成品、爆炸图和每个动作画面。
|
||||
先打开固定模板,再手动截取成品、爆炸图和每个动作画面;分享入口在顶部「分享」标签。
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@ -452,6 +417,8 @@ export function InstallationDetailsPanel({
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<InstallationWorkflowFooterNav current={detailView} onChange={onDetailViewChange} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import type { ReactNode } from 'react'
|
||||
import { Share2, Wrench } from 'lucide-react'
|
||||
import { ArrowLeft, Share2, Wrench } from 'lucide-react'
|
||||
import type {
|
||||
InstallationDetailView,
|
||||
InstallationDrawingNode,
|
||||
@ -17,14 +18,17 @@ import type {
|
||||
InstallationInspectorTab,
|
||||
} from '../types'
|
||||
import { InstallationDetailsPanel } from './InstallationDetailsPanel'
|
||||
import { InstallationDrawingToolSelection } from './InstallationDrawingToolSelection'
|
||||
import { InstallationSharePanel } from './InstallationSharePanel'
|
||||
import { ProductStructurePanel } from './ProductStructurePanel'
|
||||
import type { InstallationStageReadiness } from '../lib/installationWorkflowReadiness'
|
||||
import type { InstallationHardwareHingeEditorContext } from './InstallationHardwareHingeEditor'
|
||||
|
||||
interface InstallationInspectorProps {
|
||||
hardwareHingeContext?: InstallationHardwareHingeEditorContext
|
||||
activeTab: InstallationInspectorTab
|
||||
onTabChange: (tab: InstallationInspectorTab) => void
|
||||
stages: InstallationStageReadiness[]
|
||||
spus: InstallationSpuNode[]
|
||||
selectedSpu: InstallationSpuNode | null
|
||||
selectedModel: InstallationModelNode | null
|
||||
@ -119,9 +123,10 @@ interface InstallationInspectorProps {
|
||||
partTextureEditor?: ReactNode
|
||||
}
|
||||
|
||||
// 标签名与内容对齐:目录=SPU/型号/图纸树,制作=五阶段流程;工具改为面板内抽屉入口
|
||||
const TABS: Array<{ value: InstallationInspectorTab; label: string }> = [
|
||||
{ value: 'structure', label: '模型' },
|
||||
{ value: 'steps', label: '详情' },
|
||||
{ value: 'structure', label: '目录' },
|
||||
{ value: 'steps', label: '制作' },
|
||||
{ value: 'share', label: '分享' },
|
||||
]
|
||||
|
||||
@ -216,9 +221,11 @@ export function InstallationInspector({
|
||||
stepActionEditor,
|
||||
finishedPreview,
|
||||
partTextureEditor,
|
||||
stages,
|
||||
}: InstallationInspectorProps) {
|
||||
const [toolsDrawerOpen, setToolsDrawerOpen] = useState(false)
|
||||
return (
|
||||
<aside className="flex h-full w-[460px] shrink-0 flex-col border-l border-black/[0.06] bg-white">
|
||||
<aside className="relative flex h-full w-[460px] shrink-0 flex-col border-l border-black/[0.06] bg-white">
|
||||
<div className="border-b border-black/[0.06] px-5 py-4">
|
||||
<p className="text-[17px] font-black text-[#2d3335]">安装演示工作台</p>
|
||||
<p className="mt-1 truncate text-[12px] font-semibold text-[#5a6062]">
|
||||
@ -228,13 +235,6 @@ export function InstallationInspector({
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1 border-b border-black/[0.06] px-4 py-2">
|
||||
<Link
|
||||
href="/installation-demo-workbench/tools"
|
||||
className="inline-flex h-8 items-center gap-1 rounded-[6px] px-3 text-[12px] font-black text-[#5a6062] transition hover:bg-black/[0.04]"
|
||||
>
|
||||
<Wrench size={13} strokeWidth={1.8} />
|
||||
工具
|
||||
</Link>
|
||||
{TABS.map((tab) => (
|
||||
<button
|
||||
key={tab.value}
|
||||
@ -254,8 +254,58 @@ export function InstallationInspector({
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setToolsDrawerOpen(true)}
|
||||
aria-expanded={toolsDrawerOpen}
|
||||
className="ml-auto inline-flex h-8 items-center gap-1 rounded-[6px] px-3 text-[12px] font-black text-[#5a6062] transition hover:bg-black/[0.04]"
|
||||
>
|
||||
<Wrench size={13} strokeWidth={1.8} />
|
||||
工具
|
||||
{selectedDrawingToolIds.length > 0 ? (
|
||||
<span className="rounded-full bg-[#2d3335] px-1.5 text-[10px] font-black leading-4 text-white">
|
||||
{selectedDrawingToolIds.length}
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{toolsDrawerOpen ? (
|
||||
<div className="absolute inset-0 z-20 flex flex-col bg-white">
|
||||
<div className="flex items-center gap-3 border-b border-black/[0.06] px-4 py-3">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="关闭工具抽屉"
|
||||
onClick={() => setToolsDrawerOpen(false)}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-[6px] text-[#5a6062] transition hover:bg-black/[0.04]"
|
||||
>
|
||||
<ArrowLeft size={15} strokeWidth={1.8} />
|
||||
</button>
|
||||
<div className="min-w-0">
|
||||
<p className="text-[13px] font-black text-[#2d3335]">当前图纸工具</p>
|
||||
<p className="mt-0.5 truncate text-[10px] font-semibold text-[#8b9092]">
|
||||
已选 {selectedDrawingToolIds.length} 个 · 勾选立即保存;编辑工具模型去完整工具箱
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/installation-demo-workbench/tools"
|
||||
className="ml-auto shrink-0 rounded-[6px] border border-black/[0.08] px-2.5 py-1.5 text-[11px] font-semibold text-[#2d3335] transition hover:bg-black/[0.03]"
|
||||
>
|
||||
完整工具箱
|
||||
</Link>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-3">
|
||||
<InstallationDrawingToolSelection
|
||||
tools={allTools}
|
||||
selectedToolIds={selectedDrawingToolIds}
|
||||
canEdit={canEdit}
|
||||
loading={drawingToolsLoading}
|
||||
onToggleTool={onToggleDrawingTool}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{catalogMessage || uploadMessage ? (
|
||||
<div className="border-b border-black/[0.06] bg-[#f5f7f8] px-4 py-2 text-[11px] font-semibold text-[#5a6062]">
|
||||
{uploadMessage ?? catalogMessage}
|
||||
@ -284,6 +334,7 @@ export function InstallationInspector({
|
||||
hardwareHingeContext={hardwareHingeContext}
|
||||
detailView={detailView}
|
||||
onDetailViewChange={onDetailViewChange}
|
||||
stages={stages}
|
||||
canEdit={canEdit}
|
||||
modelNodeNames={modelNodeNames}
|
||||
parts={parts}
|
||||
|
||||
@ -0,0 +1,96 @@
|
||||
'use client'
|
||||
|
||||
import { Check, ChevronLeft, ChevronRight, CircleDashed } from 'lucide-react'
|
||||
import type { InstallationDetailView } from '../types'
|
||||
import {
|
||||
findAdjacentStage,
|
||||
type InstallationStageReadiness,
|
||||
} from '../lib/installationWorkflowReadiness'
|
||||
|
||||
const STATUS_LABEL: Record<InstallationStageReadiness['status'], string> = {
|
||||
done: '完成',
|
||||
partial: '有缺项',
|
||||
empty: '未开始',
|
||||
}
|
||||
|
||||
const STATUS_CHIP: Record<InstallationStageReadiness['status'], string> = {
|
||||
done: 'bg-[#e7f0e9] text-[#3a7a4d]',
|
||||
partial: 'bg-[#fbf3df] text-[#9a6b00]',
|
||||
empty: 'bg-black/[0.04] text-[#8b9092]',
|
||||
}
|
||||
|
||||
// 紧凑制作流程条:五个编辑阶段横向扁卡,状态徽标 + 原因一行;自由跳转,不做强制向导。
|
||||
export function InstallationWorkflowNavigator({ stages, current, onChange }: {
|
||||
stages: InstallationStageReadiness[]
|
||||
current: InstallationDetailView
|
||||
onChange: (view: InstallationDetailView) => void
|
||||
}) {
|
||||
const activeStage = stages.find((stage) => stage.key === current)
|
||||
return (
|
||||
<nav aria-label="制作流程" className="border-b border-black/[0.06] px-3 py-3">
|
||||
<div className="grid grid-cols-5 gap-1.5">
|
||||
{stages.map((stage, index) => {
|
||||
const active = stage.key === current
|
||||
return (
|
||||
<button
|
||||
key={stage.key}
|
||||
type="button"
|
||||
onClick={() => onChange(stage.key)}
|
||||
aria-current={active ? 'step' : undefined}
|
||||
title={stage.detail}
|
||||
className={[
|
||||
'flex min-w-0 flex-col gap-1.5 rounded-[8px] border px-2.5 py-2 text-left transition',
|
||||
active
|
||||
? 'border-[#2d3335]/30 bg-[#f5f7f8]'
|
||||
: 'border-black/[0.06] bg-white hover:border-black/[0.12]',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="flex min-w-0 items-center gap-1">
|
||||
<span className={['shrink-0 text-[11px] font-black',
|
||||
active ? 'text-[#2d3335]' : 'text-[#b8bec0]'].join(' ')}>
|
||||
{index + 1}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate text-[12px] font-black text-[#2d3335]">
|
||||
{stage.label}
|
||||
</span>
|
||||
</span>
|
||||
<span className={['inline-flex w-fit items-center gap-1 rounded-full px-1.5 py-0.5 text-[9px] font-semibold leading-none',
|
||||
STATUS_CHIP[stage.status]].join(' ')}>
|
||||
{stage.status === 'done'
|
||||
? <Check size={9} strokeWidth={2.5} />
|
||||
: <CircleDashed size={9} strokeWidth={2} />}
|
||||
{STATUS_LABEL[stage.status]}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{activeStage && activeStage.status !== 'done' ? (
|
||||
<p className="mt-2 truncate text-[10px] font-semibold text-[#8b9092]">
|
||||
{activeStage.label}:{activeStage.detail}
|
||||
</p>
|
||||
) : null}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
// 底部上一步/下一步:只在五个编辑阶段之间切换。
|
||||
export function InstallationWorkflowFooterNav({ current, onChange }: {
|
||||
current: InstallationDetailView
|
||||
onChange: (view: InstallationDetailView) => void
|
||||
}) {
|
||||
const previous = findAdjacentStage(current, -1)
|
||||
const next = findAdjacentStage(current, 1)
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-2 border-t border-black/[0.06] px-3 py-2">
|
||||
<button type="button" disabled={!previous} onClick={() => previous && onChange(previous)}
|
||||
className="inline-flex h-8 items-center gap-1 rounded-md border border-black/[0.08] px-3 text-[12px] font-semibold text-[#2d3335] transition hover:bg-black/[0.03] disabled:opacity-40">
|
||||
<ChevronLeft size={13} />上一步
|
||||
</button>
|
||||
<button type="button" disabled={!next} onClick={() => next && onChange(next)}
|
||||
className="inline-flex h-8 items-center gap-1 rounded-md bg-[#2d3335] px-3 text-[12px] font-semibold text-white transition hover:bg-[#2d3335]/90 disabled:opacity-40">
|
||||
下一步<ChevronRight size={13} />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,143 @@
|
||||
'use client'
|
||||
|
||||
import type { InstallationDetailView } from '../types'
|
||||
|
||||
// 制作流程五阶段的完成度判定:纯函数,输入工作台现有数据,输出每个阶段的状态与原因。
|
||||
// 状态含义:done=满足完成条件;partial=已开始但有缺项;empty=未设置。
|
||||
// 贴图是可选阶段,永远不阻塞流程,只报告设置数量。
|
||||
export type InstallationStageStatus = 'done' | 'partial' | 'empty'
|
||||
|
||||
export interface InstallationStageReadiness {
|
||||
key: InstallationDetailView
|
||||
label: string
|
||||
status: InstallationStageStatus
|
||||
detail: string
|
||||
}
|
||||
|
||||
export interface InstallationStageReadinessInput {
|
||||
modelLoaded: boolean
|
||||
// 爆炸图判定用部件清单(partCatalog.parts,爆炸编辑器直接操作的对象)
|
||||
parts: Array<{
|
||||
offset: number[] | [number, number, number]
|
||||
rotation?: number[] | [number, number, number, number]
|
||||
nodeTransforms?: Record<string, { offset: number[]; rotation: number[] }>
|
||||
}>
|
||||
numberingParts: Array<{
|
||||
nodeNames: string[]
|
||||
}>
|
||||
steps: Array<{ id: string; actions?: unknown[] | undefined }>
|
||||
materialCount: number
|
||||
}
|
||||
|
||||
// 与 ExplosionEditorPanel.hasPartTransform 同口径:偏移或旋转偏离初始值(ε=0.001)即视为已设置变换。
|
||||
// nodeTransforms 只认"非恒等"条目——工作流会自动填充恒等变换,不能算已爆炸。
|
||||
export function hasInstallationPartTransform(part: {
|
||||
offset: number[] | [number, number, number]
|
||||
rotation?: number[] | [number, number, number, number]
|
||||
nodeTransforms?: Record<string, { offset: number[]; rotation: number[] }>
|
||||
}): boolean {
|
||||
const hasBaseOffset = (part.offset ?? []).some((value) => Math.abs(value) > 0.001)
|
||||
const rotation = part.rotation ?? [0, 0, 0, 1]
|
||||
const hasBaseRotation =
|
||||
Math.abs(rotation[0]) > 0.001 ||
|
||||
Math.abs(rotation[1]) > 0.001 ||
|
||||
Math.abs(rotation[2]) > 0.001 ||
|
||||
Math.abs((rotation[3] ?? 1) - 1) > 0.001
|
||||
const hasNodeTransform = Object.values(part.nodeTransforms ?? {}).some((transform) => {
|
||||
const moved = (transform.offset ?? []).some((value) => Math.abs(value) > 0.001)
|
||||
const tRotation = transform.rotation ?? [0, 0, 0, 1]
|
||||
const rotated =
|
||||
Math.abs(tRotation[0]) > 0.001 ||
|
||||
Math.abs(tRotation[1]) > 0.001 ||
|
||||
Math.abs(tRotation[2]) > 0.001 ||
|
||||
Math.abs((tRotation[3] ?? 1) - 1) > 0.001
|
||||
return moved || rotated
|
||||
})
|
||||
return hasBaseOffset || hasBaseRotation || hasNodeTransform
|
||||
}
|
||||
|
||||
export function computeInstallationStageReadiness(
|
||||
input: InstallationStageReadinessInput,
|
||||
): InstallationStageReadiness[] {
|
||||
// 爆炸图完成口径与编辑器一致:任一部件已记录变换即"爆炸状态已记录"(部件可有意留在原位,不要求全部)
|
||||
const explodedCount = input.parts.filter(hasInstallationPartTransform).length
|
||||
const unboundCount = input.numberingParts.filter((part) => part.nodeNames.length === 0).length
|
||||
const stepsWithoutActions = input.steps.filter(
|
||||
(step) => !step.actions || step.actions.length === 0,
|
||||
).length
|
||||
|
||||
return [
|
||||
{
|
||||
key: 'finished',
|
||||
label: '成品展示',
|
||||
status: input.modelLoaded ? 'done' : 'empty',
|
||||
detail: input.modelLoaded ? '模型已加载' : '先上传模型',
|
||||
},
|
||||
{
|
||||
key: 'exploded',
|
||||
label: '爆炸图',
|
||||
status: explodedCount > 0 ? 'done' : 'empty',
|
||||
detail:
|
||||
explodedCount > 0
|
||||
? `爆炸状态已记录(${explodedCount} 项)`
|
||||
: '未设置爆炸位置',
|
||||
},
|
||||
{
|
||||
key: 'numbering',
|
||||
label: '部件编号',
|
||||
status:
|
||||
input.numberingParts.length === 0
|
||||
? 'empty'
|
||||
: unboundCount > 0
|
||||
? 'partial'
|
||||
: 'done',
|
||||
detail:
|
||||
input.numberingParts.length === 0
|
||||
? '还没有编号'
|
||||
: unboundCount > 0
|
||||
? `还有 ${unboundCount} 个编号没有模型节点`
|
||||
: `${input.numberingParts.length} 项`,
|
||||
},
|
||||
{
|
||||
key: 'materials',
|
||||
label: '部件贴图',
|
||||
status: input.materialCount > 0 ? 'done' : 'empty',
|
||||
detail: input.materialCount > 0 ? `${input.materialCount} 项已设置` : '未设置(可选)',
|
||||
},
|
||||
{
|
||||
key: 'steps',
|
||||
label: '安装步骤',
|
||||
status:
|
||||
input.steps.length === 0
|
||||
? 'empty'
|
||||
: stepsWithoutActions > 0
|
||||
? 'partial'
|
||||
: 'done',
|
||||
detail:
|
||||
input.steps.length === 0
|
||||
? '还没有步骤'
|
||||
: stepsWithoutActions > 0
|
||||
? `${input.steps.length} 步 · ${stepsWithoutActions} 步未设置动作`
|
||||
: `${input.steps.length} 步`,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export const INSTALLATION_STAGE_ORDER: InstallationDetailView[] = [
|
||||
'finished',
|
||||
'exploded',
|
||||
'numbering',
|
||||
'materials',
|
||||
'steps',
|
||||
]
|
||||
|
||||
export function findAdjacentStage(
|
||||
current: InstallationDetailView,
|
||||
direction: -1 | 1,
|
||||
): InstallationDetailView | null {
|
||||
const index = INSTALLATION_STAGE_ORDER.indexOf(current)
|
||||
if (index < 0) return null
|
||||
const next = index + direction
|
||||
if (next < 0 || next >= INSTALLATION_STAGE_ORDER.length) return null
|
||||
return INSTALLATION_STAGE_ORDER[next]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user