文章目录
前言
前一章我们开始引入了全局状态管理,扩展了侧边栏显示屏幕列表,但拖拽到屏幕的组件还没有同步用全局数据。这一章我们把相关的地方都同步用全局数据来管理,实现屏幕切换,还要继续扩展WidgetBar
。
一、扩展控件
前面章节里的WidgetBar
只有一个Button
,所以我们偷了点懒,在Canvas
绘制组件的时候直接写死了Button
。在实际产品里当然不能这么做了,接下来我们进一步扩展控件和WidgetBar
,让它看起来更产品化。
我们已经定义了Button
和Container
的数据类型,但对应的控件我们还没有定义。先来完成它们的外观。
// Button Widget
import { WidgetProps } from '.';
export const ButtonWidget: React.FC<WidgetProps> = ({
className,
left,
top,
width,
height,
}) => {
return (
<div
className={`absolute left-3 top-3 w-12 bg-gray-700 rounded-md flex flex-col justify-center items-center p-2 space-y-2 ${
className ?? ""
}`}
style={{ left, top, width, height }}
>
Button
</div>
);
};
// Container Widget
import { WidgetProps } from '.';
export const ContainerWidget: React.FC<WidgetProps> = ({ className, left, top, width, height }) => {
return (
<div
className={`absolute left-3 top-3 w-12 bg-gray-700 rounded-md flex flex-col justify-center items-center p-2 space-y-2 ${
className?? ""
}`}
style={{
left, top, width, height
}}
>Container</div>
);
};
嗯?这两个控件有什么差别吗?本质上没差别,就是一个普通的<div>
,但对于系统来说就是两个控件,这就够了。至于它们的具体实现不是这一章节的主要内容,我们留待日后再表。
二、定义控件库
上面定义好了Button
和Container
控件,我们还要把控件的数据类型跟对应的控件映射起来,这样我们可以根据组件的控件类型绘出相应的组件。
import { ButtonWidget } from './btn-widget';
import { ContainerWidget } from './cont-widget';
export interface WidgetProps {
className?: string;
}
interface WidgetLib {
[key: string]: { icon: string, widget: React.FC<WidgetProps> };
}
export const widgetLib: WidgetLib = {
btn: { icon: 'icon-anniu', widget: ButtonWidget },
cont: { icon: 'icon-rongqi', widget: ContainerWidget },
}
WidgetLib
是一个Map类型,key
是控件名称,对应控件数据里的type
,这里我把Button
的控件数据再列出来对比看下:
export interface Button extends Widget {
type: 'btn'; // 对应`WidgetLib`里的`key`
text: string; // `Button`的属性
}
好,这样我们就定义好了控件数据,控件外观和它们的映射关系。
二、扩展WidgetStore
前面我们只在WidgetStore
里定义了AddScr
和RemoveScr
用来添加/删除屏幕,现在我们要增加一个AddWidget
,当拖拽控件到画布上时,通过AddWidget
来添加一个组件到当前的屏幕里。另外,我们增加currScrId
用来标记当前处于哪个屏幕,和setCurrScrId
来变更当前的屏幕,当用户点击不同屏幕时做切换。对了,还有一个init
用作初始化操作,谁家还没个初始化呢。如果用户每次都需要手动创建第一个屏幕,那就直接帮他完成这一步吧。
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
import { Scr } from '../types/widget';
import { createScr } from '../utils/widget';
import { Widget } from '../types/widget';
export interface WidgetState {
scrs: Scr[];
currScrId?: string;
}
export interface WidgetActions {
init: () => void;
addScr: (scr: Scr) => void;
removeScr: (scrId: string) => void;
setCurrScrId: (scrId: string) => void;
addWidget: (widget: Widget) => void;
}
export const useWidgetStore = create(
immer<WidgetState & WidgetActions>((set) => ({
scrs: [],
init: () => {
set((state) => {
state.scrs = [createScr()];
state.currScrId = state.scrs[0].id;
});
},
addScr: (scr: Scr) => {
set((state) => {
state.scrs = [...state.scrs, scr];
});
},
removeScr: (scrId: string) => {
set((state) => {
if (state.scrs.length > 1) {
state.scrs = state.scrs.filter((s) => s.id !== scrId);
}
if (state.scrs.length > 0) {
state.currScrId = state.scrs[0].id;
}
});
},
setCurrScrId: (scrId: string) => {
set((state) => {
state.currScrId = scrId;
});
},
addWidget: (widget: Widget) => {
set((state) => {
const scr = state.scrs.find((s) => s.id === state.currScrId);
if (scr) {
scr.children = [...scr.children, widget];
}
});
},
}))
)
代码比较直观,就不做详细解释了,addWidget
直接把组件添加到当前屏幕的children
里就行。那嵌套怎么办?不着急,我们一步一步来。在实现Container
的时候会来说这个问题。
三、扩展WidgetBar
我们前面定义了控件库,但还没有反应到WigetBar
上。我们先把控件库定义的控件都显示到WidgetBar
上。
import { widgetLib } from './widget';
interface WidgetBarProps {
className?: string;
}
export const WidgetBar: React.FC<WidgetBarProps> = ({ className }) => {
return (
<div
className={`absolute left-3 top-3 w-12 bg-gray-700 rounded-md flex flex-col justify-center items-center p-2 space-y-2 ${
className ?? ""
}`}
>{
Object.keys(widgetLib).map((type, index) => {
return (
<i
key={index}
className={`iconfont ${widgetLib[type].icon} hover:text-orange-500`}
draggable
onDragStart={(e) => {
e.dataTransfer?.setData("widget", type);
}}
/>
);
})
}
</div>
);
};
代码和盘托出了,这里解释一下onDragStart
。拖拽时,我们可以通过e.dataTransfer.setData
来将一些信息从source
传递给target
,source
就是我们要拖拽的控件,target
就是Canvas
。这里,我们将拖拽的控件类型type
传递给画布,这样画布就可以索引控件库,将对应的组件绘制出来。
四、Canvas从全局数据管理获取组件
准备工作都做好了,我们可以开始来改造Canvas
了。
import { createElement, useMemo } from "react";
import { useCurrScr } from "../hooks/useCurrScr";
import { useWidgetStore } from "../stores/widget.store";
import { createBtn, createWidget } from "../utils/widget";
import { widgetLib } from "./widget";
interface CanvasProps {
className?: string;
}
export const Canvas: React.FC<CanvasProps> = ({ className }) => {
const addWidget = useWidgetStore((s) => s.addWidget);
const { currScr } = useCurrScr();
return (
<div
className={`relative ${className ?? ""}`}
onDragOver={(e) => {
e.preventDefault();
}}
onDrop={(e) => {
const type = e.dataTransfer?.getData("widget");
const { clientX, clientY, target } = e;
const targetElement = target as HTMLElement;
const rect = targetElement.getBoundingClientRect();
const widget = createWidget(type);
const left = clientX - (rect?.left ?? 0) - widget.width / 2;
const top = clientY - (rect?.top ?? 0) - widget.height / 2;
widget.left = left;
widget.top = top;
addWidget(widget);
}}
>
{currScr?.children.map((w, index) => {
const { widget } = widgetLib[w.type];
return createElement(widget, {
key: index,
type: w.type,
name: w.name,
id: w.id,
left: w.left,
top: w.top,
width: w.width,
height: w.height,
});
})}
</div>
);
};
Canvas
改造的核心就是onDrop
。
首先,通过e.dataTransfer.getData
获取source
传递过来的控件类型,创建对应的组件(createWidget
)。
然后,通过clientX
和clientY
获取放置的坐标,更新组件的位置。
最后,通过我们前面实现的addWidget
把这个组件添加到WidgetStore
里。
五、SideBar屏幕切换
我们答应过要支持屏幕切换的,这就来了。
import { useCurrScr } from "../hooks/useCurrScr";
import { useWidgetStore } from "../stores/widget.store";
import { createScr } from "../utils/widget";
interface SideBarProps {
className?: string;
}
export const SideBar: React.FC<SideBarProps> = ({ className }) => {
const addScr = useWidgetStore((s) => s.addScr);
const removeScr = useWidgetStore((s) => s.removeScr);
const setCurrScrId = useWidgetStore((s) => s.setCurrScrId);
const { scrs, currScrId } = useCurrScr();
return (
<div className={className ?? ""}>
<div className="flex items-center justify-end p-2 space-x-2">
<i
className="iconfont icon-plus hover:text-orange-500"
onClick={() => {
addScr(createScr());
}}
/>
<i
className="iconfont icon-minus hover:text-red-500"
onClick={() => {
if (currScrId) {
removeScr(currScrId);
}
}}
/>
</div>
<ul className="p-2 space-y-1">
{scrs.map((scr, index) => (
<li
key={index}
className={`${
currScrId === scrs[index].id
? "bg-orange-500"
: "hover:bg-gray-400"
} flex items-center cursor-pointer space-x-2 p-2 rounded-md `}
onClick={() => {
setCurrScrId(scrs[index].id);
}}
>
<i className="iconfont icon-screen" />
<span>{scr.name}</span>
</li>
))}
</ul>
</div>
);
};
这里实现了:
- 添加/删除
- 屏幕切换
- 当前屏幕高亮
- 鼠标移动到某个屏幕改变背景色
代码比较直观,就不详细解释了。
总结
到这一章我们开始有了产品化的基础,引入全局状态来管理所有的组件,支持增加更多的控件,支持状态变更(屏幕切换)。
我怎么感觉我们这个应用的界面比 Anyui 还好看点,你觉得呢?哈哈