
前言
距离上一次发布独立应用的第一个版本,我花了一周的时间把 ProcessOn 上的基本功能都实现了下。但最近一个多月工作和生活都比较忙,趁有空把实现的功能梳理下。如果你对我的应用感兴趣,可以通过私信的方式联系我,申请体验软件。目前,我只发布了 Windows 版本的软件包,由于时间和精力有限,其他平台的版本我会在后续考虑开发。
功能概览
- 清空:一键清除当前画布上的所有内容,方便用户快速开始新的创作。
- 加粗:将选中的文本设置为加粗样式,突出重点信息。
- 倾斜:将选中的文本设置为倾斜样式,增加文本的艺术效果。
- 下划线:为选中的文本添加下划线,增强文本的可读性。
- 字体颜色:自由更改选中文本的颜色,满足用户的个性化需求。
- 行高:灵活调整文本的行高,优化文本的排版效果。
- 文本对齐:设置文本的对齐方式,包括左对齐、居中对齐和右对齐,使文本布局更加整洁美观。
- 连线宽度:调整连接线的宽度,以适应不同的设计需求。
- 连线样式:选择连接线的样式,如实线、虚线等,丰富图形的视觉效果。
- 开始箭头:设置连接线起始端的箭头样式,增强图形的指向性。
- 结束箭头:设置连接线末端的箭头样式,使图形的逻辑关系更加清晰。
- 图层:管理图形元素的图层顺序,方便用户对图形进行精确操作。
- 页面配置:调整页面布局和背景等设置,打造个性化的画布。
- 调试:进入调试模式,方便开发人员进行开发和测试工作。
- 基础图形*:在"图形库"中提供矩形、圆形、菱形等基础图形元素,供用户自由选择和使用。
- 风格:在"风格"选项卡中提供多种预设样式,如颜色主题、边框样式等,用户点击后可一键将样式应用到整个画布。
- 导出模板:允许用户将当前画布的布局和样式保存为模板,便于后续复用,提高工作效率。
- 应用模板:支持从模板库加载已保存的模板,并将其应用到当前画布,实现快速设计。
功能实现
接下来,我将挑选几个功能点,详细说明其实现的思路。
风格
- 定义基础样式:首先,定义好不同风格的基础样式,包括颜色、边框、填充等。
- 应用样式:当用户选择应用某种风格时,系统会自动找出当前项目中的所有画布,并将预设的样式应用到这些画布上。
- 保存变更记录:为了方便后续的撤销(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 };
}
导出 / 应用模板
根据用户的需求,导出模板可以有两种方式:
-
仅保存到模板库:
- 保存模板:将当前图形保存一份到模板库中。
- 应用模板:当用户需要使用模板时,系统会从模板库中取出对应的图形,并将其应用到当前画布中。
-
导出文件:
- 导出文件:由于数据库设计是每个项目单独一个数据库,即每次新建一个项目都会新建一个数据库连接。因此,在导出项目模板文件时,可以将当前项目的数据库文件拷贝一份。如果有定制的配置文件,也可以一并导出。为了方便管理,可以将多个文件压缩成一个文件。
- 应用模板:当用户需要应用模板时,系统会将压缩文件解压,然后将对应的数据库复制到指定的路径下,连接数据库,最后将模板应用到当前画布中。
基础图形
目前支持以下图形,以下是绘制基础图形的代码示例:
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;
}
规划
以上就是我对这款应用目前实现的功能的详细梳理和介绍。从基本的文本编辑到复杂的图形操作,从个性化的风格定制到便捷的模板管理。
目前软件使用的人数不多,也没有想好后续的规划。有兴趣的朋友可以留言说说你的想法,也算是独立开发的一个小小尝试吧。