Vue3中使用LogicFlow实现简单流程图

实现结果

在这里插入图片描述
实现功能:

  1. 拖拽创建节点
  2. 自定义节点/边
  3. 自定义快捷键
  4. 人员选择弹窗
  5. 右侧动态配置组件
  6. 配置项获取/回显
  7. 必填项验证
  8. 历史记录(撤销/恢复)

自定义节点与拖拽创建节点

拖拽节点面板node-panel.vue

<template><div class="node-panel"><divv-for="(item, key) in state.nodePanel":key="key"class="approve-node"@mousedown="dragNode(item)"><div class="node-shape" :class="'node-' + item.type"></div><div class="node-label">{{ item.text }}</div></div></div>
</template><script lang="ts" setup>
import { ILogicFlowNodePanelItem } from "@/types/logic-flow";
import LogicFlow from "@logicflow/core";
import { reactive } from "vue";
const props = defineProps<{ lf?: LogicFlow }>();
const state = reactive({nodePanel: [{type: "approver",text: "用户活动",},{type: "link",text: "连接点",},{type: "review",text: "传阅",},],
});
const dragNode = (item: ILogicFlowNodePanelItem) => {props.lf?.dnd.startDrag({type: item.type,text: item.text,});
};
</script>

自定义节点/边index.ts

/*** @description 注册节点* @export* @param {LogicFlow} lf* @return {*}*/
export function registeNode(lf: ShallowRef<LogicFlow | undefined>) {/*** @description 自定义开始节点*/class StartNode extends CircleNode {getShape() {const { x, y } = this.props.model;const style = this.props.model.getNodeStyle();return h("g", {}, [h("circle", {...style,cx: x,cy: y,r: 30,stroke: "#000",fill: "#000",}),]);}getText() {const { x, y, text } = this.props.model;return h("text",{x: x,y: y,fill: "#fff",textAnchor: "middle",alignmentBaseline: "middle",style: { fontSize: 12 },},text.value);}}class StartNodeModel extends CircleNodeModel {setAttributes() {this.r = 30;this.isSelected = false;}getConnectedTargetRules() {const rules = super.getConnectedTargetRules();const geteWayOnlyAsTarget = {message: "开始节点只能连出,不能连入!",validate: (source?: BaseNodeModel, target?: BaseNodeModel) => {let isValid = true;if (target) {isValid = false;}return isValid;},};rules.push(geteWayOnlyAsTarget);return rules;}getConnectedSourceRules() {const rules = super.getConnectedSourceRules();const onlyOneOutEdge = {message: "开始节点只能连出一条线!",validate: (source?: BaseNodeModel, target?: BaseNodeModel) => {let isValid = true;if (source?.outgoing.edges.length) {isValid = false;}return isValid;},};rules.push(onlyOneOutEdge);return rules;}createId() {return uuidv4();}}lf.value?.register({type: "start",view: StartNode,model: StartNodeModel,});/*** @description 自定义发起节点*/class LaunchNode extends RectNode {getShape() {const { x, y, width, height, radius } = this.props.model;const style = this.props.model.getNodeStyle();return h("g", {}, [h("rect", {...style,x: x - width / 2,y: y - height / 2,rx: radius,ry: radius,width: 120,height: 50,stroke: "#000",fill: "#000",}),]);}getText() {const { x, y, text, width, height } = this.props.model;return h("foreignObject",{x: x - width / 2,y: y - height / 2,className: "foreign-object",style: {width: width,height: height,},},[h("p",{style: {fontSize: 12,width: width,height: height,lineHeight: height + "px",whiteSpace: "nowrap",overflow: "hidden",textOverflow: "ellipsis",textAlign: "center",padding: "0 8px",boxSizing: "border-box",margin: "0",color: "#fff",},},text.value),]);}}class LaunchModel extends RectNodeModel {setAttributes() {this.width = 120;this.height = 50;this.radius = 4;this.isSelected = false;}getConnectedSourceRules() {const rules = super.getConnectedSourceRules();const notAsTarget = {message: "不能连接自己",validate: (source?: BaseNodeModel, target?: BaseNodeModel) => {let isValid = true;if (source?.id === target?.id) {isValid = false;}return isValid;},};rules.push(notAsTarget);return rules;}createId() {return uuidv4();}}lf.value?.register({type: "launch",view: LaunchNode,model: LaunchModel,});/*** @description 自定义审批节点*/class ApproverNode extends RectNode {getShape() {const { x, y, width, height, radius } = this.props.model;const style = this.props.model.getNodeStyle();return h("g", {}, [h("rect", {...style,x: x - width / 2,y: y - height / 2,rx: radius,ry: radius,width: 120,height: 50,stroke: "#facd91",fill: "#facd91",}),]);}getText() {const { x, y, text, width, height } = this.props.model;return h("foreignObject",{x: x - width / 2,y: y - height / 2,className: "foreign-object",style: {width: width,height: height,},},[h("p",{style: {fontSize: 12,width: width,height: height,lineHeight: height + "px",whiteSpace: "nowrap",overflow: "hidden",textOverflow: "ellipsis",textAlign: "center",padding: "0 8px",boxSizing: "border-box",margin: "0",},},text.value),]);}}class ApproverModel extends RectNodeModel {setAttributes() {this.width = 120;this.height = 50;this.radius = 4;this.isSelected = false;}getConnectedSourceRules() {const rules = super.getConnectedSourceRules();const notAsTarget = {message: "不能连接自己",validate: (source?: BaseNodeModel, target?: BaseNodeModel) => {let isValid = true;if (source?.id === target?.id) {isValid = false;}return isValid;},};rules.push(notAsTarget);return rules;}createId() {return uuidv4();}}lf.value?.register({type: "approver",view: ApproverNode,model: ApproverModel,});/*** @description 自定义连接点节点*/class LinkNode extends RectNode {getShape() {const { x, y, width, height, radius } = this.props.model;const style = this.props.model.getNodeStyle();return h("g", {}, [h("rect", {...style,x: x - width / 2,y: y - height / 2,rx: radius,ry: radius,width: 120,height: 50,stroke: "#caf982",fill: "#caf982",}),]);}getText() {const { x, y, text, width, height } = this.props.model;return h("foreignObject",{x: x - width / 2,y: y - height / 2,className: "foreign-object",style: {width: width,height: height,},},[h("p",{style: {fontSize: 12,width: width,height: height,lineHeight: height + "px",whiteSpace: "nowrap",overflow: "hidden",textOverflow: "ellipsis",textAlign: "center",padding: "0 8px",boxSizing: "border-box",margin: "0",},},text.value),]);}}class LinkModel extends RectNodeModel {setAttributes() {this.width = 120;this.height = 50;this.radius = 4;this.isSelected = false;}getConnectedSourceRules() {const rules = super.getConnectedSourceRules();const notAsTarget = {message: "不能连接自己",validate: (source?: BaseNodeModel, target?: BaseNodeModel) => {let isValid = true;if (source?.id === target?.id) {isValid = false;}return isValid;},};rules.push(notAsTarget);return rules;}createId() {return uuidv4();}}lf.value?.register({type: "link",view: LinkNode,model: LinkModel,});/*** @description 自定义传阅节点*/class ReviewNode extends RectNode {getShape() {const { x, y, width, height, radius } = this.props.model;const style = this.props.model.getNodeStyle();return h("g", {}, [h("rect", {...style,x: x - width / 2,y: y - height / 2,rx: radius,ry: radius,width: 120,height: 50,stroke: "#81d3f8",fill: "#81d3f8",}),]);}getText() {const { x, y, text, width, height } = this.props.model;return h("foreignObject",{x: x - width / 2,y: y - height / 2,className: "foreign-object",style: {width: width,height: height,},},[h("p",{style: {fontSize: 12,width: width,height: height,lineHeight: height + "px",whiteSpace: "nowrap",overflow: "hidden",textOverflow: "ellipsis",textAlign: "center",padding: "0 8px",boxSizing: "border-box",margin: "0",},},text.value),]);}}class ReviewModel extends RectNodeModel {setAttributes() {this.width = 120;this.height = 50;this.radius = 4;this.isSelected = false;}getConnectedSourceRules() {const rules = super.getConnectedSourceRules();const notAsTarget = {message: "不能连接自己",validate: (source?: BaseNodeModel, target?: BaseNodeModel) => {let isValid = true;if (source?.id === target?.id) {isValid = false;}return isValid;},};rules.push(notAsTarget);return rules;}createId() {return uuidv4();}}lf.value?.register({type: "review",view: ReviewNode,model: ReviewModel,});/*** @description 结束节点*/class FinishNode extends CircleNode {getShape() {const { x, y } = this.props.model;const style = this.props.model.getNodeStyle();return h("g", {}, [h("circle", {...style,cx: x,cy: y,r: 30,stroke: "#000",fill: "#000",}),]);}getText() {const { x, y, text } = this.props.model;return h("text",{x: x,y: y,fill: "#fff",textAnchor: "middle",alignmentBaseline: "middle",style: { fontSize: 12 },},text.value);}}class FinishModel extends CircleNodeModel {setAttributes() {this.r = 30;this.isSelected = false;}getConnectedSourceRules() {const rules = super.getConnectedSourceRules();const notAsTarget = {message: "终止节点不能作为连线的起点",validate: () => false,};rules.push(notAsTarget);return rules;}createId() {return uuidv4();}}lf.value?.register({type: "end",view: FinishNode,model: FinishModel,});/*** @description 虚线*/class DashedLineModel extends PolylineEdgeModel {getEdgeStyle() {const style = super.getEdgeStyle();style.stroke = "#000";style.strokeDasharray = "3 3";return style;}}lf.value?.register({type: "dashedLine",view: PolylineEdge,model: DashedLineModel,});/*** @description 开始的连线*/class StartPolylineModel extends PolylineEdgeModel {setAttributes() {this.isSelected = false;this.isHitable = false;}}lf.value?.register({type: "startPolyline",view: PolylineEdge,model: StartPolylineModel,});
}

注册logicflow并使用自定义节点

<template><div class="logic-flow-container"><div class="logic-flow-header"><el-button type="primary" @click="getData">获取数据</el-button><el-button type="primary" @click="submit">提交</el-button></div><div class="logic-flow-main"><div class="logic-flow" ref="logicFlowRef"></div><Settingclass="logic-flow-setting":data="nodeData!":lf="lf":type="state.settingType"></Setting><NodePanel :lf="lf"></NodePanel></div><!-- 当lf有值 才能注册事件 --><Control v-if="lf" :lf="lf"></Control></div>
</template><script lang="ts">
export default { name: "LogicFlow" };
</script>
<script lang="ts" setup>
import LogicFlow from "@logicflow/core";
import "@logicflow/core/lib/style/index.css";
import "@logicflow/extension/lib/style/index.css";
import { onMounted, reactive, ref, ShallowRef, shallowRef } from "vue";
import NodePanel from "./components/node-panel.vue";
import { registeNode, registerKeyboard, requiredConfig } from "./index";
import { ElMessage } from "element-plus";
import Control from "./components/control.vue";
import Setting from "./components/setting.vue";
import { SettingType } from "@/types/logic-flow";const logicFlowRef = ref<HTMLDivElement>();
const nodeData = ref<LogicFlow.NodeData | LogicFlow.EdgeData>(); // 节点数据
const state = reactive({settingType: "all" as SettingType,
});
const lf = shallowRef<LogicFlow>();const getSettingInfo = (data: LogicFlow.NodeData | LogicFlow.EdgeData) => {switch (data.type) {case "launch":nodeData.value = data;state.settingType = data.type;break;case "approver":nodeData.value = data;state.settingType = data.type;break;case "link":nodeData.value = data;state.settingType = data.type;break;case "review":nodeData.value = data;state.settingType = data.type;break;case "polyline":case "dashedLine":nodeData.value = data;state.settingType = data.type;break;}
};
/*** @description 注册事件*/
const initEvent = (lf: ShallowRef<LogicFlow | undefined>) => {lf.value?.on("blank:click", (e) => {state.settingType = "all";});lf.value?.on("node:mousedown", ({ data }) => {lf.value?.selectElementById(data.id, false);getSettingInfo(data);});lf.value?.on("edge:click", ({ data }) => {lf.value?.selectElementById(data.id, false);getSettingInfo(data);});lf.value?.on("connection:not-allowed", (data) => {ElMessage.error(data.msg);return false;});lf.value?.on("node:dnd-add", ({ data }) => {// 选中节点 更改信息lf.value?.selectElementById(data.id, false);getSettingInfo(data);lf.value?.container.focus(); // 聚焦 能够使用键盘操作});
};
/*** @description 获取数据*/
const getData = () => {console.log(lf.value?.getGraphData());
};
/*** @description 提交 验证数据*/
const submit = () => {const { nodes } = lf.value?.getGraphData() as LogicFlow.GraphData;for (let index = 0; index < nodes.length; index++) {const data = nodes[index];const { properties } = data;// 循环配置项for (const key in properties) {// 数组配置项 判断是否为空if (Array.isArray(properties[key])) {if (requiredConfig[key] && properties[key].length === 0) {return ElMessage.error(`${data.text?.value}节点 ${requiredConfig[key]}`);}} else {// 非数组配置项 判断是否为空if (requiredConfig[key] && !properties[key]) {return ElMessage.error(`${data.text?.value}节点 ${requiredConfig[key]}`);}}}}console.log(lf.value?.getGraphData());
};onMounted(() => {lf.value = new LogicFlow({container: logicFlowRef.value!,grid: true,keyboard: {enabled: true,shortcuts: registerKeyboard(lf, nodeData),},textEdit: false,});registeNode(lf);initEvent(lf);lf.value.render({nodes: [{id: "node_1",type: "start",x: 100,y: 300,properties: {width: 60,height: 60,},text: {x: 100,y: 300,value: "开始",},},{id: "node_2",type: "launch",x: 100,y: 400,properties: {width: 120,height: 50,},text: {x: 100,y: 400,value: "发起流程",},},{id: "node_3",type: "end",x: 100,y: 600,properties: {width: 60,height: 60,},text: {x: 100,y: 600,value: "结束",},},],edges: [{id: "edge_1",type: "startPolyline",sourceNodeId: "node_1",targetNodeId: "node_2",},{id: "edge_2",type: "polyline",sourceNodeId: "node_2",targetNodeId: "node_3",},],});lf.value.translateCenter(); // 将图形移动到画布中央
});
</script>

右侧的配置设置

  1. 通过componentIs实现不同的配置组件
  2. 通过logicflow的setProperties()函数,将配置项注入节点/边的properties对象中,目的是传参和回显的时候方便
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

人员选择组件

正选、反选、回显,可作为一个单独组件使用,目前使用的是el-tree,数据量大时可考虑虚拟树
在这里插入图片描述

<template><MyDialogv-model="state.visible"title="选择人员"width="800px"@close="close"@cancel="close"@submit="submit"><div class="type"><label><span>发起人:</span><el-radio-group v-model="state.type"><el-radio value="1">指定人员</el-radio><el-radio value="2">角色</el-radio></el-radio-group></label></div><div class="panel"><div class="left-panel"><div class="panel-title">人员选择</div><div class="search"><el-inputv-model="state.filterText"style="width: 100%"placeholder="请输入筛选内容"/></div><div class="content"><el-treeref="treeRef":data="state.data"show-checkboxnode-key="key":check-on-click-node="true":filter-node-method="filterNode"@check-change="checkChange"/></div></div><div class="right-panel"><div class="panel-title">已选择</div><div class="content checked-content"><el-tagv-for="tag in state.checkedList":key="tag.key"closabletype="primary"@close="handleClose(tag.key)">{{ tag.label }}</el-tag></div></div></div></MyDialog>
</template><script lang="ts">
export default { name: "ChoosePerson" };
</script>
<script lang="ts" setup>
import { ElTree } from "element-plus";
import { nextTick, reactive, ref, watch } from "vue";
interface Tree {[key: string]: any;
}
const state = reactive({visible: false,type: "1",filterText: "",value: [],data: [{label: "张三",key: "1",},{label: "李四",key: "2",},{label: "王五",key: "3",children: [{label: "王五1",key: "31",},{label: "王五2",key: "32",},],},],checked: [] as string[],checkedList: [] as { label: string; key: string }[],
});
const treeRef = ref<InstanceType<typeof ElTree>>();
const emits = defineEmits(["submit"]);
/*** @description 筛选节点*/
watch(() => state.filterText,(val) => {treeRef.value!.filter(val);}
);
const open = (checked: string[]) => {state.visible = true;nextTick(() => {state.checked = checked;treeRef.value?.setCheckedKeys([...checked], false);});
};
const close = () => {state.visible = false;state.filterText = "";
};
const submit = () => {emits("submit", state.checked, state.checkedList);close();
};
/*** @description 筛选节点*/
const filterNode = (value: string, data: Tree) => {if (!value) return true;return data.label.includes(value);
};
/*** @description 选中节点*/
const checkChange = () => {// 已选的id string[] 用来提交state.checked = treeRef.value?.getCheckedNodes(true, false).map((item) => item.key) as string[];// 已选的对象 {label: string; key: string}[] 用来展示tagstate.checkedList = treeRef.value?.getCheckedNodes(true, false).map((item) => {return {label: item.label,key: item.key,};})!;
};
/*** @description 删除已选人员*/
const handleClose = (key: string) => {state.checkedList = state.checkedList.filter((item) => item.key !== key);treeRef.value?.setCheckedKeys(state.checkedList.map((item) => item.key),false);
};
/*** @description 清空已选人员*/
const clear = () => {state.checkedList = [];state.checked = [];treeRef.value?.setCheckedKeys([], false);
};
defineExpose({open,clear,
});
</script><style lang="scss" scoped>
.type {display: flex;align-items: center;margin-bottom: 20px;span {margin-right: 10px;}label {display: flex;align-items: center;}
}
.panel {width: 100%;display: flex;.left-panel {flex: 1;border: 1px solid #ccc;border-radius: 4px;.search {padding: 6px 10px;}}.right-panel {flex: 1;margin-left: 20px;border: 1px solid #ccc;border-radius: 4px;}.panel-title {padding: 10px 0;font-size: 14px;font-weight: bold;background-color: #f5f5f5;text-align: center;}.content {max-height: 400px;min-height: 200px;overflow: auto;}.checked-content {padding: 6px 10px;.el-tag + .el-tag {margin-left: 10px;}}
}
</style>

自定义快捷键,根据源码改编

/*** @description 注册键盘事件* @export* @param {(ShallowRef<LogicFlow | undefined>)} lf* @param {(Ref<LogicFlow.NodeData | LogicFlow.EdgeData | undefined>)} nodeData* @return {*}*/
export function registerKeyboard(lf: ShallowRef<LogicFlow | undefined>,nodeData: Ref<LogicFlow.NodeData | LogicFlow.EdgeData | undefined>
) {let copyNodes = undefined as LogicFlow.NodeData[] | undefined;let TRANSLATION_DISTANCE = 40;let CHILDREN_TRANSLATION_DISTANCE = 40;const cv = [{keys: ["ctrl + c", "cmd + c"],callback: () => {copyNodes = lf.value?.getSelectElements().nodes;},},{keys: ["ctrl + v", "cmd + v"],callback: () => {const startOrEndNode = copyNodes?.find((node) =>node.type === "start" ||node.type === "end" ||node.type === "launch");if (startOrEndNode) {return true;}if (copyNodes) {lf.value?.clearSelectElements();copyNodes.forEach(function (node) {node.x += TRANSLATION_DISTANCE;node.y += TRANSLATION_DISTANCE;node.text!.x += TRANSLATION_DISTANCE;node.text!.y += TRANSLATION_DISTANCE;return node;});let addElements = lf.value?.addElements({ nodes: copyNodes, edges: [] },CHILDREN_TRANSLATION_DISTANCE);if (!addElements) return true;addElements.nodes.forEach(function (node) {nodeData.value = node.getData();return lf.value?.selectElementById(node.id, true);});CHILDREN_TRANSLATION_DISTANCE =CHILDREN_TRANSLATION_DISTANCE + TRANSLATION_DISTANCE;}return false;},},{keys: ["backspace"],callback: () => {const elements = lf.value?.getSelectElements(true);if (elements) {lf.value?.clearSelectElements();elements.edges.forEach(function (edge) {return edge.id && lf.value?.deleteEdge(edge.id);});elements.nodes.forEach(function (node) {if (node.type === "start" ||node.type === "end" ||node.type === "launch") {return true;}return node.id && lf.value?.deleteNode(node.id);});return false;}},},];return cv;
}

仓库地址
在线预览

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/466666.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Devops业务价值流:软件研发最佳实践

在当今快速迭代的软件开发环境中&#xff0c;DevOps业务价值流已成为推动软件研发高效与质量并重的关键实践。软件研发阶段作为产品生命周期的核心环节&#xff0c;其每一步都承载着将创意转化为现实的重要使命。在历经需求澄清的精准定位、架构设计的宏观规划以及项目初始化的…

wireshark工具使用

复制数据 1.右键展开整帧数据 2.复制“所有可见项目” mark标记数据 标记&#xff1a; 跳转&#xff1a; 保存成文件&#xff1a; 文件–>导出特定分组—>Marked packets only

管理 Elasticsearch 变得更容易了,非常容易!

作者&#xff1a;来自 Elastic Ken Exner Elasticsearch 用户&#xff0c;我们听到了你的心声。管理 Elasticsearch 有时会变得很复杂&#xff0c;面临的挑战包括性能调整、问题检测和资源优化。我们一直致力于简化你的体验。今天&#xff0c;我们宣布了自收购 Opster 以来的一…

深度洞察| 超6亿银发精准流量,40+泛银发群体参与消费三大变化

作者 | NewAgingPro团队 前言 9月24日&#xff0c;AgeClub成立银发流量及场景联盟&#xff08;简称&#xff1a;AgeMCN&#xff09;&#xff0c;助力银发经济高质量发展。 10月11日&#xff0c;AgeClub发布《2024银发流量全景洞察报告》&#xff0c;探索银发流量发展新模式…

Spring Boot——日志介绍和配置

1. 日志的介绍 在前面的学习中&#xff0c;控制台上打印出来的一大堆内容就是日志&#xff0c;可以帮助我们发现问题&#xff0c;分析问题&#xff0c;定位问题&#xff0c;除此之外&#xff0c;日志还可以进行系统的监控&#xff0c;数据采集等 2. 日志的使用 在程序中获取日…

Redis 组网方式入门

文章目录 一、组网方式1. 单实例模式描述优点缺点适用场景 2. 主从复制模式&#xff08;Master-Slave Replication&#xff09;描述优点缺点适用场景基于docker的redis主从复制1. 配置主节点2. 配置从节点3. 查看节点状态4. 验证主从数据同步5. 查看同步进度 3. 哨兵模式&#…

信号-2-信号捕捉

相关概念&#xff1a;递达 未决 / 阻塞 忽略 阻塞 vs 忽略 阻塞&#xff1a; 如果指定信号信号被阻塞&#xff0c; block期间该信号不能被递达&#xff0c;一直在pending表中。知道block被撤销后&#xff0c; 该信号才能递达&#xff0c;递达后对应pending位置置零。 忽…

(蓝桥杯C/C++)——基础算法(下)

目录 一、时空复杂度 1.时间复杂度 2.空间复杂度 3.分析技巧 4.代码示例 二、递归 1.递归的介绍 2.递归如何实现 3.递归和循环的比较 4.代码示例 三、差分 1.差分的原理和特点 2.差分的实现 3.例题讲解 四、枚举 1.枚举算法介绍 2.解空间的类型 3. 循环枚举解…

【极限编程(XP)】

极限编程&#xff08;XP&#xff09;简介 定义与核心价值观&#xff1a;极限编程&#xff08;Extreme Programming&#xff0c;XP&#xff09;是一种轻量级、敏捷的软件开发方法。它强调团队合作、客户参与、持续测试和快速反馈等价值观&#xff0c;旨在提高软件开发的效率和质…

如何编写安全的 Go 代码

原文&#xff1a;Jakub Jarosz - 2024.11.02 在编写 Go 代码时&#xff0c;如何时刻考虑安全性&#xff1f;要在一篇简短的文章中回答这个问题似乎不太可能。因此&#xff0c;我们将把范围缩小到一些具体做法上。 这些实践如果持续应用&#xff0c;将有助于我们编写健壮、安全…

Go八股(Ⅳ)***slice,string,defer***

***slice&#xff0c;string&#xff0c;defer*** 1.slice和arry的区别 arry&#xff1a; Go语言中arry即为数据的一种集合&#xff0c;需要在声明时指定容量和初值&#xff0c;且一旦声明就长度固定&#xff0c;访问时按照索引访问。通过内置函数len可以获取数组中的元素个…

使用 Mac 数据恢复从 iPhoto 图库中恢复照片

我们每个人都会遇到这种情况&#xff1a;在意识到我们不想丢失照片之前&#xff0c;我们会永久删除 iPhoto 图库中的一些照片。永久删除这些照片后&#xff0c;是否可以从 iPhoto 图库中恢复照片&#xff1f;本文将指导您使用免费的 Mac 数据恢复软件从 iPhoto 中恢复照片。 i…

Spark 的介绍与搭建:从理论到实践

目录 一、分布式的思想 &#xff08;一&#xff09;存储 &#xff08;二&#xff09;计算 二、Spark 简介 &#xff08;一&#xff09;发展历程 &#xff08;二&#xff09;Spark 能做什么&#xff1f; &#xff08;三&#xff09;spark 的组成部分 &#xff08;四&…

Spring Boot2(Spring Boot 的Web开发 springMVC 请求处理 参数绑定 常用注解 数据传递 文件上传)

SpringBoot的web开发 静态资源映射规则 总结&#xff1a;只要静态资源放在类路径下&#xff1a; called /static (or /public or /resources or //METAINF/resources 一启动服务器就能访问到静态资源文件 springboot只需要将图片放在 static 下 就可以被访问到了 总结&…

Vue2中使用firefox的pdfjs进行文件文件流预览

文章目录 1.使用场景2. 使用方式1. npm 包下载,[点击查看](https://www.npmjs.com/package/pdfjs-dist)2. 官网下载1. 放到public文件夹下面2. 官网下载地址[点我,进入官网](https://github.com/mozilla/pdf.js/tags?afterv3.3.122) 3. 代码演示4. 图片预览5. 如果遇到跨域或者…

2024软件测试面试热点问题

&#x1f345; 点击文末小卡片 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 大厂面试热点问题 1、测试人员需要何时参加需求分析&#xff1f; 如果条件循序 原则上来说 是越早介入需求分析越好 因为测试人员对需求理解越深刻 对测试工…

C语言 | Leetcode C语言题解之第542题01矩阵

题目&#xff1a; 题解&#xff1a; /*** Return an array of arrays of size *returnSize.* The sizes of the arrays are returned as *returnColumnSizes array.* Note: Both returned array and *columnSizes array must be malloced, assume caller calls free().*/ type…

C++总结

目录 一、面向对象的三大特性 二、引用 2.1 概念 2.2特性 三、类与对象 3.1概念 3.2 类的内容 3.3对象的创建 四、构造函数与析构函数 五、封装 六、继承 6.1概念与基础使用 6.2 继承权限 6.2.1 权限修饰符 6.2.2 继承权限 6.3构造函数 6.3.1 派生类与基类的构造函数关系 6.3.2…

2024 CSS保姆级教程二 - BFC详解

前言 - CSS中的文档流 在介绍BFC之前&#xff0c;需要先给大家介绍一下文档流。​ 我们常说的文档流其实分为定位流、浮动流、普通流三种。​ ​ 1. 绝对定位(Absolute positioning)​ 如果元素的属性 position 为 absolute 或 fixed&#xff0c;它就是一个绝对定位元素。​ 在…

在PHP8内,用Jenssegers MongoDB扩展来实现Laravel与MongoDB的集成

在现代 web 开发中&#xff0c;MongoDB 作为一种流行的 NoSQL 数据库&#xff0c;因其灵活的文档结构和高性能而受到许多开发者的青睐。Laravel&#xff0c;作为一个优雅的 PHP Web 框架&#xff0c;提供了丰富的功能和优雅的代码风格。本文将指导你如何在 Laravel 项目中集成 …