首页 > 其他分享 >【实战】从零开始打造一个低代码平台——8、全局状态管理2

【实战】从零开始打造一个低代码平台——8、全局状态管理2

时间:2024-10-16 15:47:58浏览次数:9  
标签:实战 控件 widget scrs const state 从零开始 import 全局

文章目录


前言

前一章我们开始引入了全局状态管理,扩展了侧边栏显示屏幕列表,但拖拽到屏幕的组件还没有同步用全局数据。这一章我们把相关的地方都同步用全局数据来管理,实现屏幕切换,还要继续扩展WidgetBar


一、扩展控件

前面章节里的WidgetBar只有一个Button,所以我们偷了点懒,在Canvas绘制组件的时候直接写死了Button。在实际产品里当然不能这么做了,接下来我们进一步扩展控件和WidgetBar,让它看起来更产品化。
我们已经定义了ButtonContainer的数据类型,但对应的控件我们还没有定义。先来完成它们的外观。

// 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>,但对于系统来说就是两个控件,这就够了。至于它们的具体实现不是这一章节的主要内容,我们留待日后再表。

二、定义控件库

上面定义好了ButtonContainer控件,我们还要把控件的数据类型跟对应的控件映射起来,这样我们可以根据组件的控件类型绘出相应的组件。

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里定义了AddScrRemoveScr用来添加/删除屏幕,现在我们要增加一个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传递给targetsource就是我们要拖拽的控件,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)。
然后,通过clientXclientY获取放置的坐标,更新组件的位置。
最后,通过我们前面实现的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 还好看点,你觉得呢?哈哈

标签:实战,控件,widget,scrs,const,state,从零开始,import,全局
From: https://blog.csdn.net/weixin_37760107/article/details/142740739

相关文章

  • C语言手撕实战代码_线索二叉树_先序中序线索二叉树_树的先根遍历_后根遍历_树的度_孩
    文章目录1.设计算法构造一棵先序线索二叉树2.先序线索二叉树的先序遍历算法3.设计算法构造一棵中序线索二叉树4.遍历中序线索二叉树5.树的先根遍历和后根遍历6.树T的叶子结点个数7.计算一棵以孩子兄弟表示的树T的度,该算法的时间复杂度为O(n)8.计算树孩子兄弟链表表示的T......
  • Zabbix进阶实战!将告警推送到Syslog服务器详细教程
    需求场景:用户需要将zabbix产生的告警事件推送给rsyslog服务器,syslog服务器再对事件日志进行分析处理.环境配置信息服务器IP地址ZabbixServer192.168.200.195Rsyslog服务器192.168.200.128*Rsyslog服务器防火墙需要放通UDP/514端口推送脚本需要修......
  • 5大主流方案对比:MySQL千亿级数据线上平滑扩容实战
    在项目初期,我们部署了三个数据库A、B、C,此时数据库的规模可以满足我们的业务需求。为了将数据做到平均分配,我们在Service服务层使用uid%3进行取模分片,从而将数据平均分配到三个数据库中。如图所示:图片后期随着用户量的增加,用户产生的数据信息被源源不断的添加到数据库中......
  • R语言使用caret包构建神经网络模型(Neural Network )构建回归模型实战、通过method参数
    R语言使用caret包构建神经网络模型(NeuralNetwork )构建回归模型实战、通过method参数指定算法名称目录R语言使用caret包构建神经网络模型(NeuralNetwork )构建回归模型、通过method参数指定算法名称 #导入包和库#仿真数据#R语言使用caret包构建随机森林模型(randomfore......
  • 从零开始学机器学习——分类器详解
    首先给大家介绍一个很好用的学习地址:https://cloudstudio.net/columns今天我们将结合第一章节中清洗得到的菜品数据,利用多种分类器对这些数据进行训练,以构建有效的模型。在这个过程中,我会详细讲解每一种分类器的原理及其重要性。尽管这些知识点对于实践来说并不是必须掌握的,因为......
  • C++中如何使用单例模式管理全局变量
    单例模式(SingletonPattern)是一种常用的设计模式,旨在确保一个类只有一个实例,并提供一个全局的访问点。要使用单例模式管理全局变量,可以通过控制类的实例化过程,防止多个对象的创建。这样做不仅可以保证数据一致性,还能避免使用直接的全局变量带来的命名冲突和潜在的多线程安全问题。......
  • MySQL 全局表和表锁
    MySQL的锁可以分为全局表、表级表和行锁三类。全局锁加全局读锁,整个数据库处于只读状态,可以用于数据库备份flushtableswithreadlock利用重复读隔离级别,可以避免加全局锁mysqldump--single-transaction不推荐使用setglobalreadyonly=true使整库处于只读状态rea......
  • 今日指数day8实战拓展多条件综合查询-带SQL分析
    接口说明SQL思路分析:    首先我们需要查询sys_user表中的所有数据,然后根据条件去添加动态SQL去查数据。<selectid="pageQuery"resultType="com.itheima.stock.pojo.entity.SysUser">SELECTssu.*FROMsys_userasssu<where>......
  • 教你从零开始在MaixCam上部署自己本地训练的Yolov5模型(5)- 转换格式并部署为app
    本博客会从一个从未部署过任何环境的电脑上一步步复现如何本地训练自定义模型并成功部署到Maixcam上实现数字识别的功能。文章中会引用到我当时学习是参考到的文章,都会在下面列出来,在此对这些向我提供过帮助的博主表示感谢!!本文中默认读者已经了解过相对应的知识,一些非常......
  • Gateway全局过滤器
    目录全局过滤器GlobalFilter案例定义全局过滤器,拦截并判断用户身份需求步骤自定义过滤器运行效果总结全局过滤器GlobalFilter全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样。区别在于GatewayFilter通过配置定义,处理逻辑是......