当前位置: 首页 > news >正文

讲讲项目里的仪表盘编辑器(二)

应用场景

        正常来说,编辑器应用场景应该包括:

  •         编辑器-预览
  •         编辑器
  •         最终运行时    

怎么去设计

        

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

编辑器-预览

        点击预览按钮,执行以下逻辑:

  /** @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博客icon-default.png?t=N7T8https://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);

        

        

相关文章:

讲讲项目里的仪表盘编辑器(二)

应用场景 正常来说&#xff0c;编辑器应用场景应该包括&#xff1a; 编辑器-预览 编辑器 最终运行时 怎么去设计 上一篇推文&#xff0c;我们已经大概了解了编辑器场景。接下来&#xff0c;我们来看预览时的设计 编辑器-预览 点击预览按钮&#xff0c;执行以…...

文心一言 VS 讯飞星火 VS chatgpt (102)-- 算法导论9.3 8题

八、用go语言&#xff0c;设 X[1…n]和 Y[1…n]为两个数组&#xff0c;每个都包含n个有序的元素。请设计一个 O(lgn)时间的算法来找出数组 X和Y中所有 2n 个元素的中位数。 文心一言&#xff1a; 要在 O(lgn) 时间内找出两个有序数组 X 和 Y 中所有元素的中位数&#xff0c;可…...

axios添加缓存请求,防止多次请求,单页面多个同一组件造成多次请求解决方案

在axios中&#xff0c;添加 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包装类与自动拆箱装箱

有的时候博客内容会有变动&#xff0c;首发博客是最新的&#xff0c;其他博客地址可能会未同步,认准https://blog.zysicyj.top 首发博客地址[1] 面试题手册[2] 系列文章地址[3] 1. 什么是 Java 包装类和自动拆箱装箱&#xff1f; Java 中的基本数据类型&#xff08;如 int、cha…...

基于SpringBoot网上超市的设计与实现【附万字文档(LW)和搭建文档】

主要功能 前台登录&#xff1a; 注册用户&#xff1a;用户名、密码、姓名、联系电话 用户&#xff1a; ①首页、商品信息推荐、商品资讯、查看更多 ②商品信息、商品详情、评论、点我收藏、添加购物车、立即购买 ③个人中心、余额、点我充值、更新信息、我的订单、我的地址、我…...

二、C++项目:仿muduo库实现并发服务器之时间轮的设计

文章目录 一、为什么要设计时间轮&#xff1f;&#xff08;一&#xff09;简单的秒级定时任务实现&#xff1a;&#xff08;二&#xff09;Linux提供给我们的定时器&#xff1a;1.原型2.例子 二、时间轮&#xff08;一&#xff09;思想&#xff08;一&#xff09;代码 一、为什…...

计算机竞赛 深度学习OCR中文识别 - opencv python

文章目录 0 前言1 课题背景2 实现效果3 文本区域检测网络-CTPN4 文本识别网络-CRNN5 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; **基于深度学习OCR中文识别系统 ** 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;…...

蓝桥等考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就行了 二、后端 搭建父子模块 因为这个涉及到前台以及后台管理所以使用父子模块 并且放置一个公共模块&#xff0c;放置公共的依赖以及公共的代码 2.1 搭建父子工程 这个可以使用直接一个个的maven模块&#xff…...

pandas--->CSV / JSON

csv CSV&#xff08;Comma-Separated Values&#xff0c;逗号分隔值&#xff0c;有时也称为字符分隔值&#xff0c;因为分隔字符也可以不是逗号&#xff09;&#xff0c;其文件以纯文本形式存储表格数据&#xff08;数字和文本&#xff09;。 CSV 是一种通用的、相对简单的文…...

LeetCode算法二叉树—116. 填充每个节点的下一个右侧节点指针

目录 116. 填充每个节点的下一个右侧节点指针 题解&#xff1a; 代码&#xff1a; 运行结果&#xff1a; 给定一个 完美二叉树 &#xff0c;其所有叶子节点都在同一层&#xff0c;每个父节点都有两个子节点。二叉树定义如下&#xff1a; struct Node {int val;Node *left;N…...

二、2023.9.28.C++基础endC++内存end.2

文章目录 17、说说new和malloc的区别&#xff0c;各自底层实现原理。18、 说说const和define的区别。19、 说说C中函数指针和指针函数的区别&#xff1f;20、 说说const int *a, int const *a, const int a, int *const a, const int *const a分别是什么&#xff0c;有什么特点…...

DevSecOps 将会嵌入 DevOps

通常人们在一个项目行将结束时才会考虑到安全&#xff0c;这么做会导致很多问题&#xff1b;将安全融入到DevOps的工作流中已产生了积极结果。 DevSecOps&#xff1a;安全正当时 一直以来&#xff0c;开发人员在构建软件时认为功能需求优先于安全。虽然安全编码实践起着重要作…...

不同管径地下管线的地质雷达响应特征分析

不同管径地下管线的地质雷达响应特征分析 前言 以混凝土管线为例&#xff0c;建立了不同管径的城市地下管线模型&#xff0c;进行二维地质雷达正演模拟&#xff0c;分析不同管径管线的地质雷达响应特征。 文章目录 不同管径地下管线的地质雷达响应特征分析前言1、管径50cm2、…...

【接口测试学习】白盒测试 接口测试 自动化测试

一、什么是白盒测试 白盒测试是一种测试策略&#xff0c;这种策略允许我们检查程序的内部结构&#xff0c;对程序的逻辑结构进行检查&#xff0c;从中获取测试数据。白盒测试的对象基本是源程序&#xff0c;所以它又称为结构测试或逻辑驱动测试&#xff0c;白盒测试方法一般分为…...

7.网络原理之TCP_IP(下)

文章目录 4.传输层重点协议4.1TCP协议4.1.1TCP协议段格式4.1.2TCP原理4.1.2.1确认应答机制 ACK&#xff08;安全机制&#xff09;4.1.2.2超时重传机制&#xff08;安全机制&#xff09;4.1.2.3连接管理机制&#xff08;安全机制&#xff09;4.1.2.4滑动窗口&#xff08;效率机制…...

Docker Dockerfile解析

Dockerfile是什么 Dockerfile是用来构建Docker镜像的文本文件&#xff0c;是由一条条构建镜像所需的指令和参数构成的脚本。 官网&#xff1a;Dockerfile reference | Docker Docs 构建三步骤&#xff1a; 编写Dockerfile文件docker build命令构建镜像docker run依镜像运行容…...

浏览器从输入URL到页面展示这个过程中都经历了什么

一. URL输入 URL是统一资源定位符&#xff0c;用于定位互联网上的资源&#xff0c;俗称网址。我们在地址栏输入网址后敲下回车&#xff0c;浏览器会对输入的信息进行以下判断&#xff1a; 1. 检查输入的内容是否是一个合法的URL连接 2. 如果合法的话&#xff0c;则会判断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变量名正…...

SpringMVC 学习(二)Hello SpringMVC

3. Hello SpringMVC (1) 新建 maven 模块 springmvc-02-hellomvc (2) 确认依赖的导入 (3) 配置 web.xml <!--web/WEB-INF/web.xml--> <?xml version"1.0" encoding"UTF-8"?> <web-app xmlns"http://xmlns.jcp.org/xml/ns/javaee…...

交换机之间配置手动|静态链路聚合

两台交换机&#xff0c;配置链路聚合&#xff1a; 1、禁止自动协商速率&#xff0c;配置固定速率 int G0/0/1 undo negotiation auto speed 100int G0/0/2 undo negotiation auto speed 100 2、配置eth-trunk int eth-trunk 1 mode manual | lacp-staticint G0/0/1 eth-trun…...

Shiro高级及SaaS-HRM的认证授权

Shiro在SpringBoot工程的应用 Apache Shiro是一个功能强大、灵活的&#xff0c;开源的安全框架。它可以干净利落地处理身份验证、授权、企业会话管理和加密。越来越多的企业使用Shiro作为项目的安全框架&#xff0c;保证项目的平稳运行。 在之前的讲解中只是单独的使用shiro&…...

eclipse svn插件安装

1.进入eclipse的help->Eclipse Marketplace,如下图所示&#xff1a; 2.输入“svn”,再按回车&#xff0c;如下图&#xff1a; 3.这我选择的是 Subversive,点击后面的“install”按钮&#xff0c;如下图 Eclipse 下连接 SVN 库有两种插件 —— Subclipse 与 Subversive &…...

C语言 cortex-A7核 UART总线 实验

一、C 1&#xff09;uart4.h #ifndef __UART4_H__ #define __UART4_H__ #include "stm32mp1xx_rcc.h" #include "stm32mp1xx_gpio.h" #include "stm32mp1xx_uart.h&quo…...

不同走向地下管线的地质雷达响应特征分析

不同走向地下管线的地质雷达响应特征分析 前言 以PVC管线为例&#xff0c;建立不同走向&#xff08;水平倾斜、垂直倾斜、水平相邻&#xff09;的三维管线地质模型&#xff0c;进行三维地质雷达数据模拟&#xff0c;分析不同走向地下管线的地质雷达响应特征。 文章目录 不同…...

Nginx负载均衡详解

一、负载均衡介绍 1、负载均衡的定义 单体服务器解决不了并发量大的请求&#xff0c;所以&#xff0c;我们可以横向增加服务器的数量&#xff08;集群&#xff09;&#xff0c;然后将请求分发到各个服务器上&#xff0c;将原先请求集中到单个服务器上的情况改为将请求分发到多…...

基于Spring Boot的宠物咖啡馆平台的设计与实现

目录 前言 一、技术栈 二、系统功能介绍 用户信息管理 看护师信息管理 宠物寄养管理 健康状况管理 点单 宠物体验 三、核心代码 1、登录模块 2、文件上传模块 3、代码封装 前言 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已…...

TYVJ P1026 犁田机器人

描述 Farmer John為了让自己从无穷无尽的犁田工作中解放出来&#xff0c;於是买了个新机器人帮助他犁田。这个机器人可以完成犁田的任务&#xff0c;可惜有一个小小的缺点&#xff1a;这个犁田机器人一次只能犁一个边的长度是整数的长方形的田地。 因為FJ的田地有树和其他障碍…...

软件测试面试经验分享,真实面试题

前言 本人普通本科计算机专业&#xff0c;做测试也有3年的时间了&#xff0c;讲下我的经历&#xff0c;我刚毕业就进了一个小自研薪资还不错&#xff0c;有10.5k&#xff08;个人觉得我很优秀&#xff09;&#xff0c;在里面呆了两年&#xff0c;积累了一些的经验和技能&#…...

兰州营销型网站建设/中国最新军事新闻直播

虚拟主机IIS6常用MINe类型作者&#xff1a;dthost |时间&#xff1a;2015-08-10 |7,646 次阅读什么是MIME类型&#xff1a;MIME类型是IIS的插件统称&#xff0c;实际上IIS默认支持很多插件&#xff0c;但是需要一个媒介去开启&#xff0c;所以有了MIME&#xff0c;不管是IIS6还…...

太原网站建设世纪优创/最好的网络营销软件

来都来了,怎么说也你也踩下我的说说是吧,求回复...

无锡专业做网站的公司/淘宝关键词工具

在前面的一篇文章 “Elasticsearch&#xff1a;创建属于自己的 Ingest processor” 中&#xff0c;我相信地介绍了如何使用一个模板来创建 Ingest 插件。在今天的文章中&#xff0c;我们使用另外一个方法来做同样的事。我们将使用 eclipse 来生成一个 maven 的项目。 前言 El…...

无锡网站建设维护/湖南网络推广排名

最近开发了一个数据解析程序&#xff0c;需要显示10W的设备数据&#xff0c;采用了DataGridView 虚拟模式&#xff0c;效率非常高&#xff0c;但是使用中也遇到了一个奇葩的问题&#xff0c;微软MSN上面好像没有说到这个情况&#xff0c;比如我有10多列&#xff0c;界面默认只能…...

大学生个人网站怎么做/网站备案查询

安装环境准备 1.安装 jdk 2.安装Maven 依赖关系如下&#xff1a; 因为没有现成的安装包&#xff0c;需要使用Maven对Github上的源码进行编译。所以安装的Jdk版本取决于你的Maven版本。至于Maven版本的选择就选择最新的。 以下是我安装的版本&#xff1a; 具体安装步骤…...

做网站banner是什么意思/百度指数官网入口

开发的项目多种多样&#xff0c;有时候程序员也会遇到瓶颈不知如何往下操作&#xff0c;今天爱站技术频道小编为大家编写了iOS开发之如何给View添加指定位置的边框线详解&#xff0c;希望对大家有所帮助&#xff01;示例代码封装一&#xff1a;直接封装成了一个方法/// 边框类型…...