使用低代码实战开发页面(下)——低代码知识点详解(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...

相关推荐
Apifox7 分钟前
如何在 Apifox 中通过 Runner 运行包含云端数据库连接配置的测试场景
前端·后端·ci/cd
树上有只程序猿35 分钟前
后端思维之高并发处理方案
前端
庸俗今天不摸鱼1 小时前
【万字总结】前端全方位性能优化指南(十)——自适应优化系统、遗传算法调参、Service Worker智能降级方案
前端·性能优化·webassembly
黄毛火烧雪下1 小时前
React Context API 用于在组件树中共享全局状态
前端·javascript·react.js
Apifox1 小时前
如何在 Apifox 中通过 CLI 运行包含云端数据库连接配置的测试场景
前端·后端·程序员
一张假钞2 小时前
Firefox默认在新标签页打开收藏栏链接
前端·firefox
高达可以过山车不行2 小时前
Firefox账号同步书签不一致(火狐浏览器书签同步不一致)
前端·firefox
m0_593758102 小时前
firefox 136.0.4版本离线安装MarkDown插件
前端·firefox
掘金一周2 小时前
金石焕新程 >> 瓜分万元现金大奖征文活动即将回归 | 掘金一周 4.3
前端·人工智能·后端
三翼鸟数字化技术团队2 小时前
Vue自定义指令最佳实践教程
前端·vue.js