Skip to content

112. 实战:酷家乐装修编辑器(十六)

Published:

上节实现了尺寸标注:

2025-06-27 18.04.44.gif

这节我们开始做户型切换、家具的导入和编辑。

image.png

我们把左侧的菜单分为两个列表,一个是户型列表,可以切换户型,一个是家具列表,可以导入家具。

用 antd 的 Segmented 组件:

image.png

改下 components/Menu/index.tsx

image.png

Segmented 的options 里是这两个选项对应的 label 和值。

用一个状态保存 key,Segmented 的onChange 里 setKey

当 key 为户型和家具的时候,分别展示不同的内容。

这里还用到了 @ant-design/icons 的 icon

安装下:

npm install --save @ant-design/icons
import { HomeOutlined, UngroupOutlined } from "@ant-design/icons";
import { Segmented } from "antd";
import { useState } from "react";

function Menu() {

    const [left, setLeft] = useState(0);

    const [key, setKey] = useState('户型');

    return <div className="Menu" style={{left: left}}>
        <Segmented value={key} onChange={setKey} block options={[
            {
                label: <div>
                    <HomeOutlined />
                    <span style={{padding: 10}}>户型</span>
                </div>,
                value: '户型',
            },
            {
                label: <div>
                    <UngroupOutlined />
                    <span style={{padding: 10}}>家具</span>
                </div>,
                value: '家具',
            },
        ]} />
        {
            key === '户型' ? <div>
                111
            </div> : null
        }
        { key === '家具' ? <div>
                222
            </div> : null
        }
        <div className="drawer-bar" onClick={() => {
            setLeft(left === 0 ? -300 : 0);
        }}></div>
    </div>
}

export default Menu;

顺便把样式里的背景色改成白色:

image.png

看下效果:

2025-06-27 21.13.09.gif

然后写一下户型列表:

现在只有两个户型:

house1.png

house2.png

把这两张图放到 public 目录下:

image.png

然后写一下家具列表:

用 antd 的 Card 组件:

image.png

import { Card, Image, Segmented } from "antd";
{
    key === '户型' ? <div>
        <Card
            hoverable
            style={{ width: 200, margin: 20 }}
            cover={<Image
                width={200}
                src="./house1.png"
            />}
        >
            <Meta title="1室1厅0厨0卫" description="" />
        </Card>
        <Card
            hoverable
            style={{ width: 200, margin: 20 }}
            cover={<Image
                width={200}
                src="./house2.png"
            />}
        >
            <Meta title="1室2厅0厨0卫" description="" />
        </Card>
    </div> : null
}

2025-06-27 21.25.51.gif

然后点击的时候清空当前户型数据,设置为新的户型。

酷家乐里也是这样:

2025-06-27 21.27.09.gif

切换户型会清空当前数据。

我们给 store 添加一个方法:

image.png

这里要添加一个 Action 类型,然后 setData 里返回新的 state。

export interface Action {
    setData(data: State['data']): void;
}

const useHouseStore = create<State & Action>((set, get) => {
    return {
        data: data,
        setData(data) {
            set(state => {
                return {
                    ...state,
                    data: data
                }
            })
        }
    }
});

在 Menu 组件里点击户型的时候调用 setData:

image.png

image.png

import data1 from "../../store/house1";
import data2 from "../../store/house2";
const {setData} = useHouseStore();
onClick={() => setData(data1)}
onClick={() => setData(data2)}

试下效果:

2025-06-27 21.40.24.gif

新的户型数据确实设置好了,但之前的场景没清空。

我们处理下:

image.png

image.png

house.name = 'house';

给两个场景的 hosue 加个 name。

然后 data 变化的时候把 house 删掉:

image.png

useEffect(() => {
    const scene1 = scene2DRef.current;
    const scene2 = scene3DRef.current;
    const house1 = scene1?.getObjectByName('house');
    const house2 = scene2?.getObjectByName('house');

    house1?.parent?.remove(house1);
    house2?.parent?.remove(house2);

    house1?.traverse(item => {
        let obj = item as THREE.Mesh;
        if(obj.isMesh) {
            obj.geometry.dispose();
        }
    })
}, [data])

根据 name 查找到 house,把它删掉,并且遍历释放所有 geometry 的 gpu 资源。

试下效果:

2025-06-27 23.12.33.gif

有一个问题,重新渲染之后,模型不会重新加载:

image.png

去掉之前的缓存逻辑,每次重新加载就好了:

image.png

image.png

async function loadWindow() {
    const group = new THREE.Group();
    const loader = new GLTFLoader();
    const gltf = await loader.loadAsync("./window.glb");
    group.add(gltf.scene);
    
    const box = new THREE.Box3();
    box.expandByObject(gltf.scene);

    const size = box.getSize(new THREE.Vector3());
    return {
        model: group,
        size
    };
}

async function loadDoor() {
    const group = new THREE.Group();
    const loader = new GLTFLoader();
    const gltf = await loader.loadAsync("./door.glb");
    group.add(gltf.scene);
    
    const box = new THREE.Box3();
    box.expandByObject(gltf.scene);

    const size = box.getSize(new THREE.Vector3());
    return {
        model: group,
        size
    }
}

2025-06-27 23.18.25.gif

最后我们还要加一个提醒:

image.png

image.png

用 antd 的 PopConfirm 组件来实现。

{
    key === '户型' ? <div>
        <Popconfirm
            title="提醒"
            description="切换户型将清空数据,是否继续?"
            onConfirm={() => setData(data1)}
            okText="是"
            cancelText="否"
        >
            <Card
                hoverable
                style={{ width: 200, margin: 20 }}
                cover={<Image
                    width={200}
                    src="./house1.png"
                />}
            >
                <Meta title="1室1厅0厨0卫" description="" />
            </Card>
        </Popconfirm>

        <Popconfirm
            title="提醒"
            description="切换户型将清空数据,是否继续?"
            onConfirm={() => setData(data2)}
            okText="是"
            cancelText="否"
        >
            <Card
                hoverable
                style={{ width: 200, margin: 20 }}
                cover={<Image
                    width={200}
                    src="./house2.png"
                />}
            >
                <Meta title="1室2厅0厨0卫" description="" />
            </Card>
        </Popconfirm>
    </div> : null
}

试一下:

2025-06-27 23.22.54.gif

这样,户型列表和切换就做好了。

最后我们解决下这个的位置问题:

image.png

把 left 改为 320px:

image.png

试一下:

2025-06-27 23.22.54.gif

案例代码上传了小册仓库

总结

这节我们实现了户型切换。

首先我们用 Segmented、Card 组件实现了户型列表的布局,点击的时候修改 store 里的数据来实现户型切换。

需要注意的是切换户型的时候,要找到之前 house 对象,把它删掉。

户型列表和切换实现后,下节就可以来做家具列表和家具导入了。

评论