使用低代码实战开发页面(下)——低代码知识点详解(7)

前言

前面已经简单的实现了CRUD中查询和新增功能,这一篇我们来实现一下编辑和删除功能,不过我在做demo的时候,发现了一些问题,框架做了一些改造,所以这一期拖的时间有点长。

体外话

有些倔友对低代码存在的意思表示好奇,这里结合公司里的低代码平台说明一下。

往期回顾

《保姆级》低代码知识点详解(一)

低代码事件绑定和组件联动------低代码知识点详解(二)

低代码动态属性和在线执行脚本------低代码知识点详解(三)

低代码在线加载远程组件------低代码知识点详解(四)

低代码可视化逻辑编排------低代码知识点详解(五)

使用低代码实战开发页面(上)------低代码知识点详解(六)

页面持久化

前面有人和我说,希望刷新后配置还在,这就需要把数据存到localstorage中。因为我们是使用zustand库做状态存储的,而zustand库实现数据持久化是非常简单的,只需要使用一个中间件就行了,看下面变量持久化例子。

ts 复制代码
import {create} from 'zustand';
import {createJSONStorage, persist} from 'zustand/middleware';
export interface Variable {
  /**
   * 变量名
   */
  name: string;
  /**
   * 默认值
   */
  defaultValue: string;
  /**
   * 备注
   */
  remark: string;
}

interface State {
  variables: Variable[];
}

interface Action {
  /**
   * 添加组件
   * @param component 组件属性
   * @param parentId 上级组件id
   * @returns
   */
  setVariables: (variables: Variable[]) => void;
}

export const useVariablesStore = create(
  persist<State & Action>(
    (set) => ({
      variables: [],
      setVariables: (variables) => set({variables}),
    }),
    {
      name: 'variables',
      storage: createJSONStorage(() => localStorage),
    }
  )
);

components持久化和这个一样,代码就不展示了。

实现删除组件功能

如果一不小心拖错了组件,希望可以给删除掉,下面来实现删除组件功能。删除按钮加在选中组件遮罩的左上角。

动态算出删除按钮的问题

渲染删除按钮

通过绝对定位把按钮设置到左上角

实现删除组件方法

递归查找出当前组件的父组件,然后从父组件children中移除当前要删除的组件。

效果展示

通过当前组件选择父组件

当组件嵌套比较多的时候,因为容器组件可能被遮挡,就无法选中了。现在我们来实现一下,选中一个组件后,可以切换到它的上级组件。

获取当前组件所有上级,然后使用antd中Dropdown组件渲染。

动态隐藏组件

这个功能主要是用来实现组件联动的。有三个按钮,两个按钮来控制另外一个按钮的显示和隐藏,下面我们来实现一下这个功能。

因为这个功能所有组件都是通用的,所以给提出来,变成公共配置。如下图:

所有组件默认是显示的,可以直接切换隐藏,也可以绑定变量来控制隐藏。

先封装一个带变量选择的Switch切换框,和前面封装input类似,代码如下:

tsx 复制代码
// src/editor/common/setting-form-item/switch.tsx
import { SettingOutlined } from '@ant-design/icons';
import { Switch } from 'antd';
import { useState } from 'react';
import SelectVariableModal from '../select-variable-modal';

interface Value {
  type: 'static' | 'variable';
  value: any;
}

interface Props {
  value?: Value,
  onChange?: (value: Value) => void;
}

const SettingFormItemSwitch: React.FC<Props> = ({ value, onChange }) => {

  const [visible, setVisible] = useState(false);

  function valueChange(checked: any) {
    onChange && onChange({
      type: 'static',
      value: checked,
    });
  }

  function select(record: any) {
    onChange && onChange({
      type: 'variable',
      value: record.name,
    });

    setVisible(false);
  }

  return (
    <div className='flex gap-[8px]'>
      <Switch
        disabled={value?.type === 'variable'}
        checked={(value?.type === 'static' || !value) ? value?.value : ''}
        onChange={valueChange}
        checkedChildren="隐藏"
        unCheckedChildren="显示"
      />
      <SettingOutlined
        onClick={() => { setVisible(true) }}
        className='cursor-pointer'
        style={{ color: value?.type === 'variable' ? 'blue' : '' }}
      />
      <SelectVariableModal
        open={visible}
        onCancel={() => { setVisible(false) }}
        onSelect={select}
      />
    </div>
  )
}


export default SettingFormItemSwitch;

在渲染的时候,根据配置判断是否渲染组件,如果是隐藏,则返回null,不渲染组件。

效果展示

表格支持添加操作列

因为编辑和删除是表格行上的操作,所以表格单元格需要支持自定义操作。

因为这个配置比较麻烦,所以组件需要自定义setter,在渲染setter的地方判断一下,如果是组件就去渲染,如果是数组使用公共属性组件去渲染。

table-column配置代码如下:

tsx 复制代码
// src/editor/components/table-column/setter.tsx

import { EditOutlined, PlusOutlined } from '@ant-design/icons';
import { Button, Drawer, Form, Input, Space, Tooltip } from 'antd';
import { useRef, useState } from 'react';
import { arrayMove } from "react-sortable-hoc";
import CommonSetter from '../../common/common-setter';
import SortableList from '../../common/sortable-list';
import FlowEvent from '../../layouts/flow-event';

const setter = [
  {
    name: 'type',
    label: '类型',
    type: 'select',
    options: [
      {
        label: '文本',
        value: 'text',
      },
      {
        label: '日期',
        value: 'date',
      },
      {
        label: '操作',
        value: 'option',
      },
    ],
  },
  {
    name: 'title',
    label: '标题',
    type: 'input',
  },
  {
    name: 'dataIndex',
    label: '字段',
    type: 'input',
  },
];

function OptionsFormItem({ value = [], onChange }: any) {

  const [open, setOpen] = useState(false);
  const [curItem, setCurItem] = useState<any>({});
  const flowEventRef = useRef<any>();

  function changeHandle(val: string, item: any) {
    item.label = val;
    onChange([...value]);
  }

  function sortEndHanlde({ oldIndex, newIndex }: { oldIndex: number; newIndex: number; }) {
    onChange(arrayMove(value, oldIndex, newIndex));
  }

  function save() {
    const val = flowEventRef.current?.save();
    curItem.event = val;
    onChange([...value]);
    setOpen(false);
    setCurItem({});
  }

  return (
    <div>
      <div className="h-[32px] flex items-center bg-[#f7f8fa] px-[16px] rounded-2px font-semibold">操作</div>
      <div className='w-[100%] p-[20px]'>
        <SortableList
          items={value || []}
          hiddenEdit
          onDelete={(item: any) => {
            const index = value.findIndex((v: any) => v.dataIndex === item.dataIndex);
            value.splice(index, 1);
            onChange([...value]);
          }}
          itemRender={(item: any) => (
            <Space>
              <div
                className='w-[140px] text-[14px] text-[rgb(0,0,0)]'
                style={{ fontFamily: "-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,'Noto Sans',sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol','Noto Color Emoji'" }}
              >
                <Input onChange={(e) => { changeHandle(e.target.value, item) }} value={item.label} />
              </div>
              <Tooltip title="设置点击事件">
                <EditOutlined
                  onClick={() => {
                    setCurItem(item);
                    setOpen(true);
                  }}
                  className="cursor-pointer"
                />
              </Tooltip>
            </Space>
          )}
          useDragHandle
          onSortEnd={sortEndHanlde}
        />
      </div>
      <div
        className='px-[20px]'
      >
        <Button
          icon={(
            <PlusOutlined />
          )}
          block
          type='dashed'
          onClick={() => {
            onChange([...value, { key: new Date().getTime(), label: '' }]);
          }}
        >
          添加
        </Button>
      </div>
      <Drawer
        title="设置事件流"
        width="100vw"
        open={open}
        zIndex={1005}
        onClose={() => { setOpen(false); }}
        extra={(
          <Button
            type='primary'
            onClick={save}
          >
            保存
          </Button>
        )}
        push={false}
        destroyOnClose
        styles={{ body: { padding: 0 } }}
      >
        <FlowEvent flowData={curItem?.event} ref={flowEventRef} />
      </Drawer>
    </div>
  )
}


function Setter() {

  const form = Form.useFormInstance();

  const type = Form.useWatch('type', form);

  return (
    <>
      <CommonSetter setters={setter} />
      {type === 'option' && (
        <Form.Item noStyle name="options">
          <OptionsFormItem />
        </Form.Item>
      )}
    </>
  )
}

export default Setter;

为了支持添加的操作可以拖拽排序,使用了react-sortable-hoc组件,原生组件用起来比较难受,我这里做了个简单的封装,方便后面其他地方使用。

tsx 复制代码
// src/editor/common/sortable-list.tsx

import { DeleteOutlined, EditOutlined, HolderOutlined } from '@ant-design/icons';
import { Space } from 'antd';

import { SortableContainer, SortableElement, SortableHandle } from "react-sortable-hoc";

const DragHandle = SortableHandle(() => <HolderOutlined className='text-[14px] cursor-move' />);

const SortableItem = SortableElement<any>(({
  item,
  onDelete,
  itemRender,
  customItemRender,
  hiddenEdit,
  onEdit,
  hiddenDelete,
  hiddenDrag,
  itemIndex,
  deleteHandle,
  editHandle,
}: any) => {

  function renderDelete() {
    if (deleteHandle) {
      if (deleteHandle(item, itemIndex)) {
        return (
          <DeleteOutlined onClick={onDelete} className="hover:text-[red] cursor-pointer" />
        )
      }
    } else if (!hiddenDelete) {
      return (
        <DeleteOutlined onClick={onDelete} className="hover:text-[red] cursor-pointer" />
      )
    }
  }

  function renderEdit() {
    if (editHandle) {
      if (editHandle(item, itemIndex)) {
        return (
          <EditOutlined className="cursor-pointer" onClick={() => { onEdit(item, itemIndex) }} />
        )
      }
    } else if (!hiddenEdit) {
      return (
        <EditOutlined className="cursor-pointer" onClick={() => { onEdit(item, itemIndex) }} />
      )
    }
  }

  return (
    <div key={item.key} style={{ lineHeight: 1, fontSize: 14 }} className="py-[4px]">
      {customItemRender ? customItemRender(item, itemIndex) : (
        <Space style={{ width: '100%' }}>
          {renderEdit()}
          {itemRender && itemRender(item, itemIndex)}
          {renderDelete()}
          {!hiddenDrag && <DragHandle />}
        </Space>
      )}
    </div>
  )

});

const SortableList = SortableContainer<any>(({
  items,
  onDelete,
  itemRender,
  hiddenEdit,
  hiddenDelete,
  onEdit,
  hiddenDrag,
  customItemRender,
  deleteHandle,
  editHandle,
}: any) => {
  return (
    <div>
      {items.map((value: any, index: number) => (
        <SortableItem
          key={value.key}
          index={index}
          item={value}
          itemIndex={index}
          onDelete={() => {
            onDelete(value, index);
          }}
          itemRender={itemRender}
          hiddenEdit={hiddenEdit}
          hiddenDelete={hiddenDelete}
          onEdit={onEdit}
          hiddenDrag={hiddenDrag}
          customItemRender={customItemRender}
          deleteHandle={deleteHandle}
          editHandle={editHandle}
        />
      ))}
    </div>
  );
});

export default SortableList;

效果展示

改造表格渲染列的方法,增加操作类型判断。

上面代码里_execEventFlow方法,是封装的一个公共执行事件流的方法,需要传入事件流配置,和额外参数就行了,参数后面再说。

预览一下

新增请求接口动作

实现删除功能,需要按钮点击事件绑定删除接口,所以现在我们来实现一下请求接口。

先看一下配置效果

实现请求接口的配置代码,没啥好说的,省略了。

实现请求接口方法,这里随着动作越来越多,prod文件里的代码也越来越多了,所以我这里给单独拆出来了一个文件(src/editor/utils/action.ts),存放动作实现的方法。

这里讲解一下eventDatainitEventData这两个参数。

eventData是当前事件的参数,就是事件流中间事件产生的参数。

initEventData是初始事件的参数,也就是开始事件的参数。

开始事件的eventDatainitEventData是同一个。

这里record对应的就是eventDatainitEventData

上面配置中{initEventData.id}是为了把当前点击行的id传给后端删除当前数据。

增加确认框动作

为了防止误删除,在删除数据的时候,加一个确认操作。

配置和实现比较简单,就不详细说了。

删除功能演示

表单支持设置默认值

因为要实现编辑功能,表单需要支持设置默认值。

表单校验

一般的表格都会有一些校验规则,这里先实现一个简单的必输校验。

form-item配置里添加校验属性

使用antd的form-item组件rules属性实现必输功能

给表单添加校验通过事件

这样可以在校验通过事件中拿到表单的值,调用接口去新增数据或更新数据。

视频演示

表格搜索

新建数据

删除数据

编辑数据

最后

低代码基础知识详解就到这里了,后面会在fluxy-admin中完善前面实现的低代码功能,最终实现一个企业级低代码平台。如果文章对你有帮助,麻烦给个赞吧,谢谢了。

新增的代码放在feature-231121分支

demo体验地址:dbfu.github.io/lowcode-dem...

demo仓库地址:github.com/dbfu/lowcod...

相关推荐
世俗ˊ18 分钟前
CSS入门笔记
前端·css·笔记
子非鱼92118 分钟前
【前端】ES6:Set与Map
前端·javascript·es6
6230_23 分钟前
git使用“保姆级”教程1——简介及配置项设置
前端·git·学习·html·web3·学习方法·改行学it
想退休的搬砖人31 分钟前
vue选项式写法项目案例(购物车)
前端·javascript·vue.js
加勒比海涛1 小时前
HTML 揭秘:HTML 编码快速入门
前端·html
啥子花道1 小时前
Vue3.4 中 v-model 双向数据绑定新玩法详解
前端·javascript·vue.js
麒麟而非淇淋1 小时前
AJAX 入门 day3
前端·javascript·ajax
茶茶只知道学习1 小时前
通过鼠标移动来调整两个盒子的宽度(响应式)
前端·javascript·css
清汤饺子1 小时前
实践指南之网页转PDF
前端·javascript·react.js
蒟蒻的贤1 小时前
Web APIs 第二天
开发语言·前端·javascript