讲讲项目里的仪表盘编辑器(二)
应用场景
正常来说,编辑器应用场景应该包括:
- 编辑器-预览
- 编辑器
- 最终运行时
怎么去设计

上一篇推文,我们已经大概了解了编辑器场景。接下来,我们来看预览时的设计

编辑器-预览
点击预览按钮,执行以下逻辑:
/** @name 预览 **/async handlePreview() {...// 打开抽屉组件,并往里面放置运行时模块createDrawer(h => h(DashboardRuntime, { props: { dashboard: this.form } }),{title: '预览仪表盘',width: 'calc(100vw - 200px)',},);}
也就是说:

所以我们直接关注运行时表现
运行时设计
<template><HabitContext :habitKey="habitKey" @init="habitContextInit"><!-- loading框 --><a-spin v-if="loading" /><divv-else:style="styleCSSVariable"><background :background="themeBackground" :class="$style.background"><grid-layout v-bind="layoutProps"><dashboard-itemv-for="field in fields":key="field.pkId"/></grid-layout></background></div></HabitContext>
</template>
这里套了一层HabitContext框架,是用来应用和记录用户习惯的(后面讲)。a-spin是加载层。紧接着和设计器差不多,局部变量样式集里面套了个背景框架和grid-layout布局。
我们再看看dashboard-item的实现:
<template><grid-itemv-bind="layout"static>...</grid-item>
</template>
这里通过v-bind动态传入grid-item的属性(也就是拣选出来的x/y/w/h这些)。同时用static固定gird-item,使其无法缩放、拖动、被其他元素影响。
<template><grid-itemv-bind="layout"static><divv-if="showChart">...</div><!-- 没权限显示占位图 --><divv-elsestyle="height: 100%; width: 100%; display: flex; flex-direction: column"><div><span :style="titleCss">{{ field.name }}</span></div></div></grid-item>
</template>
这里就是简单的做了一个占位
<template><grid-itemv-bind="layout"static><divv-if="showChart"><div :class="$style.action"><template v-for="action in actions"><a-tooltip:key="action.key"placement="bottom":mouseLeaveDelay="0":title="action.name"><x-icon:type="action.icon"@click="execAction(action)"/></a-tooltip></template></div><component:is="component":field="field"/></div><!-- 没权限显示占位图 --><divv-elsestyle="height: 100%; width: 100%; display: flex; flex-direction: column"><div><span :style="titleCss">{{ field.name }}</span></div></div></grid-item>
</template>
浮层按钮还有具体的图表组件
数据流设计
到这里,我们已经看完了编辑器功能的大概设计。接下来该写写这套系统最核心的部分,数据流设计了。
创建一个仪表盘编辑器

点下新增按钮后,我们传入一些系统参数【应用id,功能类别等等,在这里我们并不需要关注】储存新建仪表盘在系统的位置和属性。

在接口储存完这些系统信息后,跳转到仪表盘页面进行最为关键的仪表盘初始化数据生成。
async handleAddForm(category) {// 弹窗让填写名称、图标等基础信息const result = await GroupForm.createModal({data: { parentId: this.groupId, appId: this.appId, category },},{title: this.getCategoryName(category),width: '427px',},);// 调用接口保存const formId = await add(result);this.$message.success(this.$t('common.tips.add'));// 保存完毕后跳转到页面switch (category) {case FormCategoryType.DASHBOARD:return this.$router.push(`/dashboard-design/${formId}`);...default:return this.$router.push(`/form-design/${formId}/form`);}}
这里是通过vue-router进行跳转。这里也简单贴出路由代码
import DashboardDesign from '@/views/dashboard-design';const DashboardDesignRoutes = [{path: '/dashboard-design/:id',component: DashboardDesign,},...
];export default DashboardDesignRoutes;
到这里结束,一个仪表盘编辑器已经创建完毕了。它只存储了系统数据,没有仪表盘的初始数据。而当我们进入仪表盘编辑器页面的时候,完成有效编辑之后,才会以正式数据存储下来。
当然这里指的是前端数据,后端还是会根据我们穿进去的系统参数生成一份默认的接口向的仪表盘数据模板(比如默认权限、默认刷新时间上面的)
进入仪表盘编辑器页面
先通过后端接口,拿到当前仪表盘编辑器id的接口数据
@formDesignModule.Action init;
async created() {...await this.init(this.formId).then(() => {...}
}
大概长这样,记录一些系统信息或默认属性 。这里的init是vuex的action操作。为了是把数据保存到前端本地。更多关于本项目的vuex方法请看我另外一篇文章的介绍
讲讲项目里的状态存储器vuex_AI3D_WebEngineer的博客-CSDN博客
https://blog.csdn.net/weixin_42274805/article/details/133237271?spm=1001.2014.3001.5501
看看这个init的actions做了什么?
actions: {async init({ commit }, formId) {const form = await getFormData(formId);commit('saveForm', form);}
}
mutations: {saveForm(state, data) {state.form = data;...state.loading = false;state.changed = false;}
}
这里是通过调用接口获取当前仪表盘的数据,并把它存到当前的formDesignModule,也就是formDesign这个命名空间的仓库里。
仪表盘编辑页面的状态管理器
我们刚刚看到了代码,在编辑页面created里我们执行了init。其实就是非显示地获取数据。吧获取数据的过程从页面隐式地放到了状态管理器里的actions里面。并通过state返回关注的数据。这样子无论我们在仪表盘功能里怎么去跳转页面,都不需要再重新调用接口了,而是直接从仓库里拿。
@formDesignModule.State form;@formDesignModule.Action init;@formDesignModule.State loading;@formDesignModule.State selectedField;@formDesignModule.Getter fields;@formDesignModule.Mutation updateSelectedField;@formDesignModule.Mutation selectField;@formDesignModule.Mutation updateSetting;@formDesignModule.Mutation saveForm;@formDesignModule.Mutation updateDashboardConfig;@formDesignModule.Action save;
大概有这些属性和方法来完成编辑器的功能实现。看看就行了。紧接着我们来讲其中一些实现
点击添加组件到仪表盘
有两种添加方法:
① 点击组件按钮添加
② 拖拽组件添加

点击组件
handleClickAdd() {...// 初始化layoutconst layout = getDashboardLayoutByType(type);const layoutList = ensureArray(this.$refs.container.layout);layout.x = (layoutList.length * 2) % 60;layout.y = layoutList.length + 60;field.widget.layout = layout;// 初始化风格field = this.initFieldStyle(field);
}
getDashboardLayoutByType是根据你点击的组件生成默认的组件layout数据。比如图片组件定义的默认layout是:
export function getDashboardLayoutByType(type) {const layout = getDashboardControlMeta(type, 'layout');return { x: 0, y: 0, ...(typeof layout === 'function' ? layout() : layout) };
}

这时返回了一个初始化的layout即{w:30,h:15,minH:7,x:0,y:0}。
const layoutList = ensureArray(this.$refs.container.layout);这里是直接获取设计器组件里面的layout属性(它的data值)。这个layout目前是个空数组(因为是新建的仪表盘,里面没有组件)。
layout.x = (layoutList.length * 2) % 60;
layout.y = layoutList.length + 60;
field.widget.layout = layout;// 更新布局
this.$refs.container.syncLayout();
很好理解啦,我们吧初始化layout的横纵坐标调整到它应该在的位置上,并吧这个调整过的layout信息存储到新增组件的布局属性里(替换掉初始化layout)。讲讲为什么这么计算:

可以看到实例中这两个组件的x/y值并不像上面这个逻辑计算出来的。 如果按照上面那个逻辑计算出来,则应该是{x:0,y:60...}和{x:2,y:61...}。其实这个计算过程是为了保证第n+1个组件的x和y一定大于第n个。从而避免重叠出错,而至于精准的layout数据,是借助vue-layout-grid插件行自适应生成。具体怎么做,我们看代码:
/** @name 同步layout **/syncLayout() {this.layout = ensureArray(this.fields).map(field => ({...field.widget.layout,i: field.pkId,}));}
<grid-layoutref="layout":class="$style.layout":layout.sync="layout"
>...
</grid-layout>
很多人看到这里就要骂了,骗人,你这不是啥都没干?只是把layout重新赋值了一遍。让我们改下代码看看:
/** @name 同步layout **/async syncLayout() {this.layout = ensureArray(this.fields).map(field => ({...field.widget.layout,i: field.pkId,}));console.log(this.layout);await this.$nextTick();console.log(this.layout);}
第一个输出:
[{h: 10,w: 12,x: 0,y: 0},{h: 20,w: 60,x: 2,y: 61}
]
第二个输出:
[
{h: 10,w: 12,x: 0,y: 0,i: "39b19b29-c8ef-4fd3-8604-d7e168196ae6"
},
{h: 20,w: 60,x: 2,y: 10,i: "5d684834-26bd-4d35-b7ff-36d8de9d903e"
},
]
可以看到此时this.layout已经变了。这是因为<grid-layout>已经自适应了布局。
由此,我们的保存仪表盘布局方法也呼之欲出了:
save() {// 拿到同步后的this.layoutconst layout = this.$refs.container.layout;// 生成组件id和layout信息的映射表const layoutMap = generateMap(layout, 'i', item =>pick(item, 'x', 'y', 'w', 'h'),);...
}
先看到这里,这里要生成一份类似于:'amdous123623': {w:10,h:20,x:0,y:0...}这样的映射表,是整个仪表盘布局的储存并不是直接存储类似于girdLayout的这种数组,而是由一个个组件自身的layout属性(甚至无视组件排序)拣选出来生成this.layout。也就是说仪表盘的存储结构为Array<field>这样的。
save(fields) {const layout = this.$refs.container.layout;const layoutMap = generateMap(layout, 'i', item =>pick(item, 'x', 'y', 'w', 'h'),);this.privateUpdateFields((fields || this.fields).map(field => {if (!layoutMap[field.pkId]) return field;return {...field,widget: {...field.widget,layout:{...field.widget.layout,...layoutMap[field.pkId],}},};}),);}
拖拽添加组件到仪表盘
前面我们已经讲了拖拽添加组件的思路,和预防错位或重叠的处理。现在来讲讲具体代码实现。
之前讲过了,在control-list.vue也就是左边的组件列表拖拽出组件,触发@dragstart方法,同时往设计器里传入dragType。设计器里根据dragType找对对应的组件初始化layout
@Watch('dragType')handleDragTypeChange(type) {this.isInChildCom = false; // 重新拖动需要重置if (type) {this.dragLayout = {i: 'drag',...getDashboardLayoutByType(type),};} else {this.dragLayout = null;}}
假设此时拖拽元素已经拖拽到在设计器(也就是gird-layout)上面。触发@dragover.native="handleDrag"
handleDrag(ev) {if (this.isInChildCom) return; // 进入子元素范围则无需触发ev.preventDefault();this._handleDrag(ev);
}
@throttle(100)
_handleDrag(ev) {if (!this.dragType || !this.$el) return;if (this.dragContext.clientX === ev.clientX &&this.dragContext.clientY === ev.clientY)return;this.dragContext.clientX = ev.clientX;this.dragContext.clientY = ev.clientY;this.updateInside(ev);this.updateDrag(ev);
}
_handleDrag每100秒记录一次拖拽元素的位置,当拖拽元素发生变动时,更新设计器视图。
updateInside(ev) {if (!this.dragType || !this.$el) return;const rect = this.$el.getBoundingClientRect();const errorRate = 10;const inside =ev.clientX > rect.left + errorRate &&ev.clientX < rect.right - errorRate &&ev.clientY > rect.top + errorRate &&ev.clientY < rect.bottom - errorRate;if (inside && this.dragLayoutIndex === -1) {this.layout.push(this.dragLayout);}if (!inside && this.dragLayoutIndex !== -1) {this.layout.splice(this.dragLayoutIndex, 1);}}
这里是获取设计器边界的位置属性(errorRate为误差范围,你可以理解为设计器有padding),判断拖拽元素是否在设计器边界内,如果是,就往layout里面加入它(重复则不加入),如果已经超出设计器,则移除。

我们往编辑器拖拽移动,可以看到这个虚线框会一直跟随变动,可能你们就要问了,上面的代码里dragLayout一但被添加进layout,那么dragLayoutIndex就不会是-1,也就是说layout里面的dragLayout不会改变(x或y)。那这个虚框是怎么还在移动的?
其实啊,这个虚框并不由layout里的数据决定。而是由vue-grid-layout这个插件负责渲染的。在拖动的时候,this.layout是不会变的。我们只需要每100毫秒记录一次拖拽元素的当前位置this.dragLayout,直到放置生效之后,用this.dragLayout去覆盖this.layout里面的那个被拖动元素。
所以updateDrag是为了更新this.dragLayout。通过clientY/X换算成vue-grid-layout的x,y
const dragRef = this.getDragRef();if (!this.dragType || !dragRef) return;const rect = this.$el.getBoundingClientRect();const dragging = {top: this.dragContext.clientY - rect.top,left: this.dragContext.clientX - rect.left,};dragRef.dragging = dragging;const newLayout = dragRef.calcXY(dragging.top, dragging.left);this.dragLayout.x = newLayout.x;this.dragLayout.y = newLayout.y;}
getDragRef() {// vue-grid-layout默认在$children内存在一个组件实例了, 其实每次拖动直接取最后一个实例应该就可以了return this.$refs.layout.$children[this.$refs.layout.$children.length - 1];
}
当我们放手时,触发 <grid-layout>组件上的drop事件,我们来看看@drop.native="handleDrop"的handleDrop方法
async handleDrop() {if (this.isInChildCom) return; // 进入子元素范围则无需触发if (!this.dragType) return;...
}
重叠和空类型直接当做无效动作处理
async handleDrop() {if (this.isInChildCom) return; // 进入子元素范围则无需触发if (!this.dragType) return;try {...}catch (e) {this.layout.splice(this.dragLayoutIndex, 1);throw e;}finally {this.$emit('update:dragType', null);}
}
这个try catch我们之前已经讲过了。try里面的逻辑也很简单
try {let field = createDashboardField(this.dragType);...field.widget.layout = pick(this.dragLayout, 'x', 'y', 'w', 'h');...// 更新布局this.layout.splice(this.dragLayoutIndex, 1, {...field.widget.layout,i: field.pkId,});// 提交数据存储this.$emit('add', field);
}
拖拽移动组件位置
由插件处理,会自动更新到this.layout
放大缩小组件
由插件处理,会自动更新到this.layout
删除组件
async handleDelete(pkId) {const cloneFields = deepClone(this.fields);// 摘除删除的组件数据this.updateFields(cloneFields.filter(field => {return field.pkId !== pkId;}),);await this.$nextTick();this.$refs.container.syncLayout();
}
/** @name 同步layout **/async syncLayout() {this.layout = ensureArray(this.fields).map(field => ({...field.widget.layout,i: field.pkId,}));await this.$nextTick();}
额外讲一下选中组件对组件进行修改
当我们选中组件的时候,需要在vuex里登记一下当前的选中状态
<grid-itemv-for="layoutItem in layout"...@mousedown.native="handlePointerDown"@mouseup.native="handlePointerUp($event, layoutItem.i)"
>...
</grid-item>
加了一些位置判断,以防这个组件位置出错或已经不在布局里
/** @name 鼠标设备按下与抬起事件处理 **/_pointerContext = null;handlePointerDown(ev) {this._pointerContext = {x: ev.clientX,y: ev.clientY,};}handlePointerUp(ev, pkId) {if (!this._pointerContext || !this.fieldMap[pkId]) return;const { x, y } = this._pointerContext;if (x !== ev.clientX || y !== ev.clientY) return;this.selectField(this.fieldMap[pkId]);}@formDesignModule.Mutation selectField;
再来看看仓库的代码
// fromdesign.jsselectField(state, field) {state.selectedField = field;},
如果当前组件的内容或属性发送变更,则执行
commit('selectField', newField);
相关文章:
讲讲项目里的仪表盘编辑器(二)
应用场景 正常来说,编辑器应用场景应该包括: 编辑器-预览 编辑器 最终运行时 怎么去设计 上一篇推文,我们已经大概了解了编辑器场景。接下来,我们来看预览时的设计 编辑器-预览 点击预览按钮,执行以…...
文心一言 VS 讯飞星火 VS chatgpt (102)-- 算法导论9.3 8题
八、用go语言,设 X[1…n]和 Y[1…n]为两个数组,每个都包含n个有序的元素。请设计一个 O(lgn)时间的算法来找出数组 X和Y中所有 2n 个元素的中位数。 文心一言: 要在 O(lgn) 时间内找出两个有序数组 X 和 Y 中所有元素的中位数,可…...
axios添加缓存请求,防止多次请求,单页面多个同一组件造成多次请求解决方案
在axios中,添加 const cacheMap {};响应拦截添加 try {// 删除缓存const api res.config.url.replace(process.env.VUE_APP_BASE_API, "");if (cacheMap.hasOwnProperty(api)) {delete cacheMap[api];}} catch (err) {}创建两个请求方法 /*** Get缓存…...
Java包装类与自动拆箱装箱
有的时候博客内容会有变动,首发博客是最新的,其他博客地址可能会未同步,认准https://blog.zysicyj.top 首发博客地址[1] 面试题手册[2] 系列文章地址[3] 1. 什么是 Java 包装类和自动拆箱装箱? Java 中的基本数据类型(如 int、cha…...
基于SpringBoot网上超市的设计与实现【附万字文档(LW)和搭建文档】
主要功能 前台登录: 注册用户:用户名、密码、姓名、联系电话 用户: ①首页、商品信息推荐、商品资讯、查看更多 ②商品信息、商品详情、评论、点我收藏、添加购物车、立即购买 ③个人中心、余额、点我充值、更新信息、我的订单、我的地址、我…...
二、C++项目:仿muduo库实现并发服务器之时间轮的设计
文章目录 一、为什么要设计时间轮?(一)简单的秒级定时任务实现:(二)Linux提供给我们的定时器:1.原型2.例子 二、时间轮(一)思想(一)代码 一、为什…...
计算机竞赛 深度学习OCR中文识别 - opencv python
文章目录 0 前言1 课题背景2 实现效果3 文本区域检测网络-CTPN4 文本识别网络-CRNN5 最后 0 前言 🔥 优质竞赛项目系列,今天要分享的是 🚩 **基于深度学习OCR中文识别系统 ** 该项目较为新颖,适合作为竞赛课题方向,…...
蓝桥等考Python组别五级003
第一部分:选择题 1、Python L5 (15分) 表达式“a >= b”等价于下面哪个表达式?( ) a > b and a == ba > b or a == ba < b and a == ba < b or a > b正确答案:B 2、Python L5 (15分) 当x是偶数时,下面哪个表达式的值一定是True?( …...
学之思项目第一天-完成项目搭建
一、前端 拉下前端代码执行 npm i 然后执行npm run serve就行了 二、后端 搭建父子模块 因为这个涉及到前台以及后台管理所以使用父子模块 并且放置一个公共模块,放置公共的依赖以及公共的代码 2.1 搭建父子工程 这个可以使用直接一个个的maven模块ÿ…...
pandas--->CSV / JSON
csv CSV(Comma-Separated Values,逗号分隔值,有时也称为字符分隔值,因为分隔字符也可以不是逗号),其文件以纯文本形式存储表格数据(数字和文本)。 CSV 是一种通用的、相对简单的文…...
LeetCode算法二叉树—116. 填充每个节点的下一个右侧节点指针
目录 116. 填充每个节点的下一个右侧节点指针 题解: 代码: 运行结果: 给定一个 完美二叉树 ,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下: struct Node {int val;Node *left;N…...
二、2023.9.28.C++基础endC++内存end.2
文章目录 17、说说new和malloc的区别,各自底层实现原理。18、 说说const和define的区别。19、 说说C中函数指针和指针函数的区别?20、 说说const int *a, int const *a, const int a, int *const a, const int *const a分别是什么,有什么特点…...
DevSecOps 将会嵌入 DevOps
通常人们在一个项目行将结束时才会考虑到安全,这么做会导致很多问题;将安全融入到DevOps的工作流中已产生了积极结果。 DevSecOps:安全正当时 一直以来,开发人员在构建软件时认为功能需求优先于安全。虽然安全编码实践起着重要作…...
不同管径地下管线的地质雷达响应特征分析
不同管径地下管线的地质雷达响应特征分析 前言 以混凝土管线为例,建立了不同管径的城市地下管线模型,进行二维地质雷达正演模拟,分析不同管径管线的地质雷达响应特征。 文章目录 不同管径地下管线的地质雷达响应特征分析前言1、管径50cm2、…...
【接口测试学习】白盒测试 接口测试 自动化测试
一、什么是白盒测试 白盒测试是一种测试策略,这种策略允许我们检查程序的内部结构,对程序的逻辑结构进行检查,从中获取测试数据。白盒测试的对象基本是源程序,所以它又称为结构测试或逻辑驱动测试,白盒测试方法一般分为…...
7.网络原理之TCP_IP(下)
文章目录 4.传输层重点协议4.1TCP协议4.1.1TCP协议段格式4.1.2TCP原理4.1.2.1确认应答机制 ACK(安全机制)4.1.2.2超时重传机制(安全机制)4.1.2.3连接管理机制(安全机制)4.1.2.4滑动窗口(效率机制…...
Docker Dockerfile解析
Dockerfile是什么 Dockerfile是用来构建Docker镜像的文本文件,是由一条条构建镜像所需的指令和参数构成的脚本。 官网:Dockerfile reference | Docker Docs 构建三步骤: 编写Dockerfile文件docker build命令构建镜像docker run依镜像运行容…...
浏览器从输入URL到页面展示这个过程中都经历了什么
一. URL输入 URL是统一资源定位符,用于定位互联网上的资源,俗称网址。我们在地址栏输入网址后敲下回车,浏览器会对输入的信息进行以下判断: 1. 检查输入的内容是否是一个合法的URL连接 2. 如果合法的话,则会判断URL…...
2023-09-22 monetdb-事务管理-乐观并发控制-记录
摘要: 2023-09-22 monetdb-事务管理-记录 相关文档: Transaction Management | MonetDB Docs https://en.wikipedia.org/wiki/Optimistic_concurrency_control monetdb事务管理: MonetDB/SQL 支持以 START TRANSACTION 标记并以 COMMIT 或 ROLLBACK 关闭的多语句事务方案。如果…...
蓝桥等考Python组别四级008
第一部分:选择题 1、Python L4 (15分) 字符“D”的ASCII码值比字符“F”的ASCII码值小( )。 1234正确答案:B 2、Python L4 (15分) 下面的Python变量名正…...
基于大模型的 UI 自动化系统
基于大模型的 UI 自动化系统 下面是一个完整的 Python 系统,利用大模型实现智能 UI 自动化,结合计算机视觉和自然语言处理技术,实现"看屏操作"的能力。 系统架构设计 #mermaid-svg-2gn2GRvh5WCP2ktF {font-family:"trebuchet ms",verdana,arial,sans-…...
利用ngx_stream_return_module构建简易 TCP/UDP 响应网关
一、模块概述 ngx_stream_return_module 提供了一个极简的指令: return <value>;在收到客户端连接后,立即将 <value> 写回并关闭连接。<value> 支持内嵌文本和内置变量(如 $time_iso8601、$remote_addr 等)&a…...
DeepSeek 赋能智慧能源:微电网优化调度的智能革新路径
目录 一、智慧能源微电网优化调度概述1.1 智慧能源微电网概念1.2 优化调度的重要性1.3 目前面临的挑战 二、DeepSeek 技术探秘2.1 DeepSeek 技术原理2.2 DeepSeek 独特优势2.3 DeepSeek 在 AI 领域地位 三、DeepSeek 在微电网优化调度中的应用剖析3.1 数据处理与分析3.2 预测与…...
页面渲染流程与性能优化
页面渲染流程与性能优化详解(完整版) 一、现代浏览器渲染流程(详细说明) 1. 构建DOM树 浏览器接收到HTML文档后,会逐步解析并构建DOM(Document Object Model)树。具体过程如下: (…...
如何在最短时间内提升打ctf(web)的水平?
刚刚刷完2遍 bugku 的 web 题,前来答题。 每个人对刷题理解是不同,有的人是看了writeup就等于刷了,有的人是收藏了writeup就等于刷了,有的人是跟着writeup做了一遍就等于刷了,还有的人是独立思考做了一遍就等于刷了。…...
安宝特方案丨船舶智造的“AR+AI+作业标准化管理解决方案”(装配)
船舶制造装配管理现状:装配工作依赖人工经验,装配工人凭借长期实践积累的操作技巧完成零部件组装。企业通常制定了装配作业指导书,但在实际执行中,工人对指导书的理解和遵循程度参差不齐。 船舶装配过程中的挑战与需求 挑战 (1…...
基于Java+MySQL实现(GUI)客户管理系统
客户资料管理系统的设计与实现 第一章 需求分析 1.1 需求总体介绍 本项目为了方便维护客户信息为了方便维护客户信息,对客户进行统一管理,可以把所有客户信息录入系统,进行维护和统计功能。可通过文件的方式保存相关录入数据,对…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...
深入浅出深度学习基础:从感知机到全连接神经网络的核心原理与应用
文章目录 前言一、感知机 (Perceptron)1.1 基础介绍1.1.1 感知机是什么?1.1.2 感知机的工作原理 1.2 感知机的简单应用:基本逻辑门1.2.1 逻辑与 (Logic AND)1.2.2 逻辑或 (Logic OR)1.2.3 逻辑与非 (Logic NAND) 1.3 感知机的实现1.3.1 简单实现 (基于阈…...
PHP 8.5 即将发布:管道操作符、强力调试
前不久,PHP宣布了即将在 2025 年 11 月 20 日 正式发布的 PHP 8.5!作为 PHP 语言的又一次重要迭代,PHP 8.5 承诺带来一系列旨在提升代码可读性、健壮性以及开发者效率的改进。而更令人兴奋的是,借助强大的本地开发环境 ServBay&am…...
