一个全栈流程图应用


前言

距离上一次发布独立应用的第一个版本,我花了一周的时间把 ProcessOn 上的基本功能都实现了下。但最近一个多月工作和生活都比较忙,趁有空把实现的功能梳理下。如果你对我的应用感兴趣,可以通过私信的方式联系我,申请体验软件。目前,我只发布了 Windows 版本的软件包,由于时间和精力有限,其他平台的版本我会在后续考虑开发。

功能概览

  • 清空:一键清除当前画布上的所有内容,方便用户快速开始新的创作。
  • 加粗:将选中的文本设置为加粗样式,突出重点信息。
  • 倾斜:将选中的文本设置为倾斜样式,增加文本的艺术效果。
  • 下划线:为选中的文本添加下划线,增强文本的可读性。
  • 字体颜色:自由更改选中文本的颜色,满足用户的个性化需求。
  • 行高:灵活调整文本的行高,优化文本的排版效果。
  • 文本对齐:设置文本的对齐方式,包括左对齐、居中对齐和右对齐,使文本布局更加整洁美观。
  • 连线宽度:调整连接线的宽度,以适应不同的设计需求。
  • 连线样式:选择连接线的样式,如实线、虚线等,丰富图形的视觉效果。
  • 开始箭头:设置连接线起始端的箭头样式,增强图形的指向性。
  • 结束箭头:设置连接线末端的箭头样式,使图形的逻辑关系更加清晰。
  • 图层:管理图形元素的图层顺序,方便用户对图形进行精确操作。
  • 页面配置:调整页面布局和背景等设置,打造个性化的画布。
  • 调试:进入调试模式,方便开发人员进行开发和测试工作。
  • 基础图形*:在"图形库"中提供矩形、圆形、菱形等基础图形元素,供用户自由选择和使用。
  • 风格:在"风格"选项卡中提供多种预设样式,如颜色主题、边框样式等,用户点击后可一键将样式应用到整个画布。
  • 导出模板:允许用户将当前画布的布局和样式保存为模板,便于后续复用,提高工作效率。
  • 应用模板:支持从模板库加载已保存的模板,并将其应用到当前画布,实现快速设计。

功能实现

接下来,我将挑选几个功能点,详细说明其实现的思路。

风格

  1. 定义基础样式:首先,定义好不同风格的基础样式,包括颜色、边框、填充等。
  2. 应用样式:当用户选择应用某种风格时,系统会自动找出当前项目中的所有画布,并将预设的样式应用到这些画布上。
  3. 保存变更记录:为了方便后续的撤销(undo)和重做(redo)操作,系统会将这些变更记录保存在 step 表中。在项目开发过程中,我们已经统一封装了对应的流程,开发人员只需标记哪些样式被修改过即可。

以下是实现该功能的代码示例:

ts 复制代码
async batchUpdateShapeStyle(projectId: string, shapeIds: string[], newStyle: any) {
    const shapes = await this.stepManager.shapeRep.find({
      where: { projectId, id: In(shapeIds) }
    });
    for (const shape of shapes) {
      // 更新图形样式
      shape.style = {
        ...shape.style,
        ...newStyle,
        arrowStyle: {
          ...(shape.style.arrowStyle || {}),
          ...(newStyle.arrowStyle || {})
        },
      };
      shape.styleChanged = true; // 标识图形修改属性
    }
    await this.updateShapeChanges(shapes); // 批量更新图形,并记录修改记录
    return { success: true, updated: shapes.length };
  }

导出 / 应用模板

根据用户的需求,导出模板可以有两种方式:

  1. 仅保存到模板库

    • 保存模板:将当前图形保存一份到模板库中。
    • 应用模板:当用户需要使用模板时,系统会从模板库中取出对应的图形,并将其应用到当前画布中。
  2. 导出文件

    • 导出文件:由于数据库设计是每个项目单独一个数据库,即每次新建一个项目都会新建一个数据库连接。因此,在导出项目模板文件时,可以将当前项目的数据库文件拷贝一份。如果有定制的配置文件,也可以一并导出。为了方便管理,可以将多个文件压缩成一个文件。
    • 应用模板:当用户需要应用模板时,系统会将压缩文件解压,然后将对应的数据库复制到指定的路径下,连接数据库,最后将模板应用到当前画布中。

基础图形

目前支持以下图形,以下是绘制基础图形的代码示例:

ts 复制代码
const svgPath = computed(() => {
  const shapeKey = props.shape.shapeKey;
//   根据 shapeKey 来绘制不同的图形,图形的大小是根据 bounds 来绘制的,也就是选中图形时外层的矩形框的大小
  switch (shapeKey) {
    case ShapeKey.Block:
      return getRectPath(props.shape.bounds);
    case ShapeKey.Pentagon:
      return getPolygonPath(props.shape.bounds, 5);
    case ShapeKey.Rhombus:
      return getRhombusPath(props.shape.bounds);
    case ShapeKey.Triangle:
      return getTrianglePath(props.shape.bounds);
    case ShapeKey.Ellipse:
      return getEllipsePath(props.shape.bounds);
    case ShapeKey.Circle:
      return getCirclePath(props.shape.bounds);
    case ShapeKey.RightAngle:
      return getRightAnglePath(props.shape.bounds);
    case ShapeKey.Pentagon:
      return getPolygonPath(props.shape.bounds, 5);
    case ShapeKey.Text:
      return '';
    default:
      return getRectPath(props.shape.bounds);
  }
  // 可扩展更多类型
  return props.shape.svgPath || '';
});


// 矩形的绘制方法
function getRectPath(bounds: any): string {
  const { absX, absY, width, height } = bounds;
  return `M ${absX} ${absY} H ${absX + width} V ${absY + height} H ${absX} Z`;
}

// 菱形的绘制方法
function getRhombusPath(bounds: any): string {
  // 菱形:对角线分别与包围盒边重合
  const { absX, absY, width, height } = bounds;
  const cx = absX + width / 2;
  const cy = absY + height / 2;
  return [
    `M ${cx},${absY}`,
    `L ${absX + width},${cy}`,
    `L ${cx},${absY + height}`,
    `L ${absX},${cy}`,
    'Z'
  ].join(' ');
}
// 更多图形的绘制方法......

层级

客户端根据 zIndex 从小到大排序。由于 SVG 中后绘制的图形会在前面绘制的图形之上,因此,层级低的图形需要排序在前面,层级高的图形需要排序在后面。这样就能实现从小到大层级的设置。

以下是实现图层排序的代码示例:

ts 复制代码
const symbols = computed(() => {
  return props.graph.symbols.sort((a, b) => a.zIndex - b.zIndex).filter(childShape => shapeCompManager.get(childShape.subShapeType));
})

当你想要将图形的层级上移一层时,可以使用以下代码:

TS 复制代码
 async moveZIndexUp(dto: { projectId: string; shapeId: string }) {
    // ...
    const shape = shapeMap.get(dto.shapeId); // 当前图形
    // 找到比当前 zIndex 大且最近的 shape
    let minDiff = Infinity;
    let targetShape: ShapeEntity | null = null;
    for (const s of shapes) {
      if (s.id !== shape.id && s.zIndex > shape.zIndex && (s.zIndex - shape.zIndex) < minDiff) {
        minDiff = s.zIndex - shape.zIndex;
        targetShape = s;
      }
    }
    // 交换图形层级
    if (targetShape) {
      const temp = shape.zIndex;
      shape.zIndex = targetShape.zIndex;
      targetShape.zIndex = temp;
      shape.zIndexChanged = true;
      targetShape.zIndexChanged = true;
      await this.updateShapeChanges([shape, targetShape]);
    }
    return shape;
  }

当你想要将图形的层级置顶时,可以使用以下代码

TS 复制代码
 async moveZIndexToTop(dto: { projectId: string; shapeId: string }) {
    // ...
    const shape = shapeMap.get(dto.shapeId);
    if (!shape) throw new Error('未找到指定图形');
    const maxZIndex = Math.max(...shapes.map(s => s.zIndex));
    shape.zIndex = maxZIndex + 1;
    shape.zIndexChanged = true;
    await this.updateShapeChanges([shape]);
    return shape;
  }

规划

以上就是我对这款应用目前实现的功能的详细梳理和介绍。从基本的文本编辑到复杂的图形操作,从个性化的风格定制到便捷的模板管理。

目前软件使用的人数不多,也没有想好后续的规划。有兴趣的朋友可以留言说说你的想法,也算是独立开发的一个小小尝试吧。

相关推荐
我在书社写代码2 天前
Vue 3 + TypeScript + Vite 服务端渲染项目
全栈
闲不住的李先森4 天前
AI 基础调用实现:从原理到代码实现
前端·llm·全栈
EndingCoder7 天前
React 19 与 Next.js:利用最新 React 功能
前端·javascript·后端·react.js·前端框架·全栈·next.js
梦想CAD控件8 天前
(在线CAD插件)网页CAD实现图纸表格智能提取
前端·javascript·全栈
susnm9 天前
最后的最后
rust·全栈
章丸丸10 天前
Tube - tRPC setup
react native·全栈
JefferyXZF12 天前
Next.js Server Actions 详解: 无缝衔接前后端的革命性技术(八)
前端·全栈·next.js
CF14年老兵13 天前
🤯 AI写代码比人类便宜99.9%!但这就是真相吗?
openai·全栈·trae
滕本尊13 天前
从业务到框架:Elpis 企业级应用的 NPM 包抽离实践
前端·全栈