vue 输入框@人功能组件,支持复制粘贴

文章目录

  • 概要
  • 整体架构流程
  • 技术细节
  • 小结
  • 自定义输入框代码
  • 外部调用示例

概要

开发任务系统中,业务需求:需要在任务描述、评论等地方支持@人员功能,可以将任务外部人员添加至当前任务中。

功能:1、支持输入@展开下拉,可通过鼠标点击或键盘上下移动 + 回车选中人员;

           2、支持@后继续输入以搜索人员列表;

           3、输入框@内容支持复制粘贴;

           4、添加输入框最大输入限制;

整体架构流程


因时间紧迫,虽然看到了el-input支持Autocomplete属性,但是没空去研究它了,还是用div + contenteditable="true"开撸吧;

  1. 使用div(ref[customInput]),添加contenteditable="true"可编辑状态,得到一个自定义输入框;
  2. 使用el-popover,用于展示@人员列表;
  3. 监听customInput 的input事件,当输入为@时,保存当前输入框光标位置,打开el-popover
  4. 打开el-popover后,继续监听customInput 的input事件,使用diff.diffChars对比输入差异,拿到差异输入进行@人员列表的搜索;
  5. 选中人时,为了方便,我用的方案是:拿到人员数据,生成一个span标签(给span添加contenteditable="false",不然插入的人员块也能被编辑),将人员的信息绑定在span标签上,我绑定的为data-id(工号)、data-realname(姓名);span标签创建好后,根据步骤3保存的光标位置,将span插入至customInput中,同时后移光标至插入的元素后;
  6. 为了实现复制粘贴功能,需要对customInput的复制粘贴事件(copy、paste)进行自定义;
  7. 组件内写了格式化数据的方法,通过changeChosen将处理好的@人员数组数据传递给父组件;

如业务需求包含重新编辑功能(如表单提交后需要二次编辑或修改),需要告知后端需额外保存一个存html代码的字段,因为当前组件的输入框内容,因为@人员块的缘故,只能通过html保持原本的正确格式,否则就是纯文本,通俗易懂就是两个字段分别存储customInput的textContent和innerHTML字段;编辑时只需用到保存的innerHTML字段,其余的textContent字段以及处理好的@人员数组,对于我们编辑而言,是没有用的。

技术细节

  • 在监听input事件的同时,需要同时监听keydown,用来限制用户的输入;
  • 插入span以及处理自定义粘贴事件时,需要注意光标位置;
  • 还有el-popover的弹出位置,我这里是针对我们的业务需求进行的调整,有具体需要可以自己手动调整

小结

代码看起来很简单,但因为其中一些知识之前没有涉足过,一个小问题就耽误很久,三天的艰苦奋斗,中间每次觉得完美了,就会有莫名其妙的问题跳出来,暂时就这样了,目前用起来还行,就是用户体验方面需要稍微优化一下。(对了,还写了一个小程序的@组件,小程序是uniapp写的,有需要滴滴我),下边贴代码了,懒得整理了,复制粘贴拿走用吧。

如发现什么问题,或者有大哥给优化建议的,欢迎滴滴。

自定义输入框代码

<template><div class="custom-at-box"><div class="custom-textarea-box"><div:class="['custom-textarea custom-scroll',{ 'show-word-limit': showWordLimit && maxlength },{ 'custom-textarea-disabled': disabled },]":style="{ height: height }"ref="customInput":contenteditable="!disabled":placeholder="placeholder"@input="onInput($event)"@keydown="onKeyDownInput($event)"@paste="onPaste($event)"@copy="onCopy($event)"@click="showList(false)"></div><div class="custom-at-limit" v-if="showWordLimit && maxlength">{{ inputValueLen }}/{{ maxlength }}</div></div><div :key="`customInput${taskPanelIsInFullScreen}`"><el-popoverv-model="showPopover"trigger="click"class="custom-select-box"ref="popoverRef":append-to-body="taskPanelPopoverAppendToBody"@hide="hidePoppver":style="{ top: popoverOffset + 'px' }"><divclass="custom-select-content custom-scroll"ref="customSelectContent"><div class="custom-select-empty load" v-if="searchOperatorLoad"><i class="el-icon-loading"></i><span>加载中</span></div><divclass="custom-select-empty"v-else-if="searchOperatorList.length === 0">没有查询到该用户</div><divv-else:class="['custom-select-item',{ hoverItem: selectedIndex === index },]"v-for="(item, index) in searchOperatorList":key="item.employeeNo"@click="handleClickOperatorItem(item)"><div class="custom-select-item-content">{{ item.realname }}({{ item.employeeNo }})</div></div></div></el-popover></div></div>
</template>
<script>
const diff = require("diff");
import {queryEmployeeByParam,addRemark,
} from "@/api/recruitmentSystem/childUtils.js";
export default {props: {// 输入框placeholderplaceholder: {type: String,default: "请输入...",},// 是否显示输入字数统计showWordLimit: {type: Boolean,default: true,},// 是否禁用disabled: {type: Boolean,default: false,},// 最大输入长度maxlength: {type: [Number, String],default: "300",},// 输入框高度height: {type: String,default: "100px",},setRefresh: {type: Object,default: () => {},},// 输入框输入的内容value: {type: String,default: "",},},data() {return {// 已输入内容的长度inputValueLen: 0,top: "",left: "",message: "",startOffset: 0,// @搜索人domsearSpan: null,// 筛选人数据searchOperatorList: [],// 筛选人数据加载状态searchOperatorLoad: false,// @插入位置selectionIndex: 0,// 当前编辑的domdom: null,// 当前编辑dom的indexdomIndex: 0,// 当前编辑dom的childNodes的indexchildDomIndex: 0,// 编辑前dom内容beforeDomVal: "",// 筛选人选择框showPopover: false,// 筛选人选择框偏移量popoverOffset: 0,listInput: false,listInputValue: "",// 防抖timer: null,// 保存弹窗加载状态addDataLoad: false,// 鼠标选择人的索引selectedIndex: 0,};},mounted() {this.setNativeInputValue();},computed: {// 计算属性,用于同步父组件的数据model: {get() {return this.value;},set(newValue) {this.$emit("input", newValue);if (this.$refs.customInput) {this.$emit("inputText", this.$refs.customInput.textContent);}const nodeList = this.$refs.customInput.childNodes;let list = [];nodeList.forEach((e) => {if(e.childNodes) {e.childNodes.forEach(i => {if (i.className === "active-text") {list.push({jobNumber: i.getAttribute("data-id"),name: i.textContent.replace(/@/g, "").replace(/\s/g, ""),});}})}if (e.className === "active-text") {list.push({jobNumber: e.getAttribute("data-id"),name: e.textContent.replace(/@/g, "").replace(/\s/g, ""),});}});this.$emit("changeChosen", list);},},taskPanelIsInFullScreen() {return this.$store.getters.taskPanelIsInFullScreen;},taskPanelPopoverAppendToBody() {return this.$store.getters.taskPanelPopoverAppendToBody;},},methods: {// 设置输入框的值setNativeInputValue() {if (this.$refs.customInput) {if (this.value === this.$refs.customInput.innerHTML) return;this.$refs.customInput.innerHTML = this.value;this.inputValueLen = this.$refs.customInput.innerText.length;}},// 筛选人弹窗数据选择handleClickOperatorItem(item) {this.addData(JSON.parse(JSON.stringify(item)));this.$refs.customSelectContent.scrollTop = 0;this.selectedIndex = 0;this.showPopover = false;this.listInput = false;this.listInputValue = "";},// 艾特人弹窗关闭hidePoppver() {this.$refs.customSelectContent.scrollTop = 0;this.selectedIndex = 0;this.showPopover = false;this.listInput = false;this.listInputValue = "";},// 创建艾特需要插入的元素createAtDom(item) {// 先判断剩余输入长度是否能够完整插入元素const dom = document.createElement("span");dom.classList.add("active-text");// 这里的contenteditable属性设置为false,删除时可以整块删除dom.setAttribute("contenteditable", "false");// 将id存储在dom元素的标签上,便于后续数据处理dom.setAttribute("data-id", item.employeeNo);dom.innerHTML = `@${item.realname}&nbsp;`;return dom;},// 插入元素addData(item) {const spanElement = this.createAtDom(item);const maxlength = Number(this.maxlength) || 300;// 因为插入后需要删除之前输入的@,所以判断长度时需要减去这个1if (maxlength - this.inputValueLen < spanElement.innerText.length - 1) {this.$message("剩余字数不足");return;}this.$refs.customInput.focus();// 获取当前光标位置的范围const selection = window.getSelection();const range = selection.getRangeAt(0);// 找到要插入的节点const nodes = Array.from(this.$refs.customInput.childNodes);let insertNode = "";// 是否是子元素let domIsCustomInputChild = true;if (nodes[this.domIndex].nodeType === Node.TEXT_NODE) {insertNode = nodes[this.domIndex];} else {const childNodeList = nodes[this.domIndex].childNodes;insertNode = childNodeList[this.childDomIndex];domIsCustomInputChild = false;}// 如果前一个节点是空的文本节点,@用户无法删除// 添加判断条件:如果前一个节点是空的文本节点,则插入一个空的<span>节点const html = insertNode.textContent;// 左边的节点const textLeft = document.createTextNode(html.substring(0, this.selectionIndex - 1) + "");const emptySpan = document.createElement("span");// 如果找到了要插入的节点,则在其前面插入新节点if (insertNode) {if (!textLeft.textContent) {if (domIsCustomInputChild) {this.$refs.customInput.insertBefore(emptySpan, insertNode);} else {nodes[this.domIndex].insertBefore(emptySpan, insertNode);}}insertNode.parentNode.insertBefore(spanElement, insertNode.nextSibling);// 删除多余的@以及搜索条件const textContent = insertNode.textContent.slice(0,-(1 + this.listInputValue.length));if (!textContent && insertNode.nodeName === "#text") {insertNode.remove();} else {insertNode.textContent = textContent;}} else {// 如果未找到要插入的节点,则将新节点直接追加到末尾this.$refs.customInput.appendChild(spanElement);}// 将光标移动到 span 元素之后const nextNode = spanElement.nextSibling;range.setStart(nextNode || spanElement.parentNode,nextNode ? 0 : spanElement.parentNode.childNodes.length);range.setEnd(nextNode || spanElement.parentNode,nextNode ? 0 : spanElement.parentNode.childNodes.length);selection.removeAllRanges();selection.addRange(range);this.model = this.$refs.customInput.innerHTML;this.inputValueLen = this.$refs.customInput.innerText.length;this.showList(false);},// 检查是否发生了全选操作isSelectAll() {const selection = window.getSelection();return selection.toString() === this.$refs.customInput.innerText;},// 获取输入框是否选中文字isSelect() {try {const selection = window.getSelection();return selection.toString().length;} catch (error) {return 0;}},// 输入事件onKeyDownInput(event) {// 获取当前输入框的长度let currentLength = this.$refs.customInput.innerText.length;// 获取最大输入长度限制let maxLength = Number(this.maxlength) || 300;// 如果按下的键是非控制键并且当前长度已经达到了最大长度限制if (currentLength >= maxLength) {// 获取按键的 keyCodevar keyCode = event.keyCode || event.which;// 检查是否按下了 Ctrl 键var ctrlKey = event.ctrlKey || event.metaKey; // metaKey 用于 macOS 上的 Command 键// 允许的按键:Backspace(8)、Delete(46)、方向键和 var allowedKeys = [8, 46, 37, 38, 39, 40];// 允许的按键 Ctrl+A、Ctrl+C、Ctrl+Vlet allowedCtrlKey = [65, 67, 86]// 检查按键是否在允许列表中并且没有执行选中操作if (!allowedKeys.includes(keyCode) && !this.isSelect()) {if((allowedCtrlKey.includes(keyCode) && ctrlKey)) {return;}// 阻止默认行为event.preventDefault();return false;}}if (this.showPopover) {let listElement = this.$refs.customSelectContent;let itemHeight = listElement.children[0].clientHeight;if (event.key === "ArrowDown") {// 防止光标移动event.preventDefault();// 移动选中索引if (this.selectedIndex === this.searchOperatorList.length - 1) {this.selectedIndex = 0; // 跳转到第一项listElement.scrollTop = 0; // 滚动到列表顶部} else {this.selectedIndex++;let itemBottom = (this.selectedIndex + 1) * itemHeight;let scrollBottom = listElement.scrollTop + listElement.clientHeight;if (itemBottom > scrollBottom) {listElement.scrollTop += itemHeight;}}} else if (event.key === "ArrowUp") {event.preventDefault();if (this.selectedIndex === 0) {this.selectedIndex = this.searchOperatorList.length - 1; // 跳转到最后一项listElement.scrollTop = listElement.scrollHeight; // 滚动到列表底部} else {this.selectedIndex--;let itemTop = this.selectedIndex * itemHeight;if (itemTop < listElement.scrollTop) {listElement.scrollTop -= itemHeight;}}} else if (event.key === "Enter") {event.preventDefault();if (!this.searchOperatorLoad) {this.handleClickOperatorItem(this.searchOperatorList[this.selectedIndex]);}}} else if (event.key === "Backspace" && this.isSelectAll()) {// 如果执行了全选操作并删除,清空输入框内容this.$refs.customInput.innerText = "";this.model = this.$refs.customInput.innerHTML;this.inputValueLen = 0;}},// 监听输入事件onInput(e) {this.inputValueLen = this.$refs.customInput.innerText.length;if (["<div><br></div>", "<br>", "<span></span><br>"].includes(this.$refs.customInput.innerHTML)) {this.$refs.customInput.innerHTML = "";this.inputValueLen = 0;} else if (e.data === "@") {// 保存焦点位置this.saveIndex();this.showList();this.listInput = true;} else if (this.showPopover) {const diffResult = diff.diffChars(this.beforeDomVal,this.dom.textContent);let result = "";// 遍历差异信息数组for (let i = 0; i < diffResult.length; i++) {const change = diffResult[i];// 如果当前差异是添加或修改类型,则将其添加到结果字符串中if (change.added) {result += change.value;} else if (change.removed && change.value === "@") {this.showList(false);this.listInputValue = "";}}if (this.timer) {clearTimeout(this.timer);}this.listInputValue = result;this.timer = setTimeout(() => {this.remoteMethod();}, 300);}this.model = this.$refs.customInput.innerHTML;},onPaste(event) {event.preventDefault();// 获取剪贴板中的 HTML 和文本内容const html = (event.clipboardData || window.clipboardData).getData("text/html");const text = (event.clipboardData || window.clipboardData).getData("text/plain");// 设置最大输入限制const maxLength = Number(this.maxlength) || 300;// 此时加个条件  看鼠标选中的文本长度,剩余可输入长度加上选中文本长度const selection1 = window.getSelection();const range1 = selection1.getRangeAt(0);const clonedSelection = range1.cloneContents();let selectTextLen = 0if(clonedSelection.textContent && clonedSelection.textContent.length) {selectTextLen = clonedSelection.textContent.length;}// 剩余可输入长度const remainingLength = maxLength - this.inputValueLen + selectTextLen;// 过滤掉不可见字符const cleanText = text.replace(/\s/g, "");// 创建一个临时 div 用于处理粘贴的 HTML 内容const tempDiv = document.createElement("div");tempDiv.innerHTML = html;// 过滤掉不需要的内容,例如注释和换行符const fragment = document.createDocumentFragment();let totalLength = 0;if (cleanText) {if (remainingLength >= cleanText.length) {fragment.appendChild(document.createTextNode(cleanText));} else {const truncatedText = cleanText.substr(0, remainingLength);fragment.appendChild(document.createTextNode(truncatedText));}}else {Array.from(tempDiv.childNodes).forEach((node) => {const regex = /<span class="active-text" contenteditable="false" data-id="(\d+)">@([^<]+)<\/span>/g;// 过滤注释和空白节点if (node.nodeType !== 8 &&!(node.nodeType === 3 && !/\S/.test(node.textContent))) {const childText = node.textContent || "";const childLength = childText.length;const childHtml = node.outerHTML || node.innerHTML;// 如果剩余空间足够,插入节点if ((regex.exec(childHtml) !== null) && totalLength + childLength <= remainingLength) {fragment.appendChild(node.cloneNode(true));totalLength += childLength;} else if (remainingLength - totalLength > 0) {// 如果还有剩余长度,不插入节点,插入文本内容const lastNodeLength = remainingLength - totalLength;const truncatedText = childText.substr(0, lastNodeLength);fragment.appendChild(document.createTextNode(truncatedText));totalLength += truncatedText.length;} else {// 如果添加当前节点的内容会超出剩余可插入长度,则结束循环return;}}});}// 插入处理后的内容到光标位置const selection = window.getSelection();const range = selection.getRangeAt(0);range.deleteContents();range.insertNode(fragment);// 更新输入框内容和长度this.model = this.$refs.customInput.innerHTML;this.inputValueLen = this.$refs.customInput.innerText.length;// 设置光标位置为插入内容的后面一位const newRange = document.createRange();newRange.setStart(range.endContainer, range.endOffset);newRange.collapse(true);selection.removeAllRanges();selection.addRange(newRange);},// 修改默认复制事件onCopy(e) {e.preventDefault();const selection = window.getSelection();const range = selection.getRangeAt(0);const clonedSelection = range.cloneContents();// 检查复制的内容是否包含符合条件的元素const hasActiveText =clonedSelection.querySelector('.active-text[contenteditable="false"][data-id]') !== null;const clipboardData = e.clipboardData || window.clipboardData;if(hasActiveText) {const div = document.createElement("div");div.appendChild(clonedSelection);const selectedHtml = div.innerHTML;clipboardData.setData("text/html", selectedHtml);}else {clipboardData.setData("text/plain", clonedSelection.textContent || "");}},// 保存焦点位置async saveIndex() {const selection = getSelection();this.selectionIndex = selection.anchorOffset;const nodeList = this.$refs.customInput.childNodes;const range = selection.getRangeAt(0);// 保存当前编辑的dom节点for (const [index, value] of nodeList.entries()) {// 这里第二个参数要配置成true,没配置有其他的一些小bug// (range.startContainer.contains(value) && range.endContainer.contains(value))  是为了处理兼容性问题if (selection.containsNode(value, true) ||(range.startContainer.contains(value) &&range.endContainer.contains(value))) {if (value.nodeType === Node.TEXT_NODE) {this.dom = value;this.beforeDomVal = value.textContent;this.domIndex = index;const selection = window.getSelection();const range = selection.getRangeAt(0);this.startOffset = range.startOffset - 1;} else {const childNodeList = value.childNodes;for (const [childIndex, childValue] of childNodeList.entries()) {if (selection.containsNode(childValue, true)) {this.dom = value;this.beforeDomVal = value.textContent;this.domIndex = index;this.childDomIndex = childIndex;const selection = window.getSelection();const range = selection.getRangeAt(0);this.startOffset = range.startOffset - 1;}}}}}},// 筛选人弹窗showList(bool = true) {this.showPopover = bool;if (bool) {const offset =this.getCursorDistanceFromDivBottom(this.$refs.customInput) || -1;if (offset < 0) {this.popoverOffset = 0;} else {this.popoverOffset = -(offset - 1);}}if (!bool) {this.listInputValue = "";this.remoteMethod();}},// 获取光标位置getCursorDistanceFromDivBottom(editableDiv) {// 获取选区const selection = window.getSelection();// 获取选区的范围const range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null;if (range) {// 创建一个临时元素来标记范围的结束位置const markerElement = document.createElement("span");// 插入临时标记元素range.insertNode(markerElement);markerElement.appendChild(document.createTextNode("\u200B")); // 零宽空格// 获取标记元素的位置信息const markerOffsetTop = markerElement.offsetTop;const markerHeight = markerElement.offsetHeight;// 计算光标距离div底部的距离const cursorDistanceFromBottom =editableDiv.offsetHeight - (markerOffsetTop + markerHeight);// 滚动条距顶部的高度const scrollTop = editableDiv.scrollTop || 0;// 移除临时标记元素markerElement.parentNode.removeChild(markerElement);// 返回光标距离底部的距离return cursorDistanceFromBottom + scrollTop;}// 如果没有选区,则返回-1或者其他错误值return -1;},// 搜索筛选人async remoteMethod() {let query = this.listInputValue;this.searchOperatorLoad = true;let params = {keyword: query,pageNo: 1,pageSize: 500,};await queryEmployeeByParam(params).then((res) => {this.searchOperatorList = res.list.filter((i) => i.employeeStatusId === 1).map((e) => {e.value = e.employeeNo + "_" + e.realname;return e;});}).catch(() => {});this.searchOperatorLoad = false;},handleNameShift(item) {const name = item.realname || "";if (!name) return "--";if (name.length > 1) {return name.slice(0, 1);} else {return name;}},// 按钮div点击 聚焦textareahandleBtnBoxClick() {this.$refs.customInput.focus();},// 获取@人的姓名getInnerText() {const customInput = this.$refs.customInput;if (!customInput) return;return customInput.innerText;},// 获取@人的工号getJobId() {const nodeList = this.$refs.customInput.childNodes;let list = [];nodeList.forEach((e) => {if (e.className === "active-text") {list.push(e.getAttribute("data-id"));}});return list;},clearInput() {this.$refs.customInput.innerText = "";this.$refs.customInput.innerHTML = "";this.inputValueLen = 0;this.$emit("input", "");this.$emit("inputText", "");this.$emit("changeChosen", []);},},
};
</script>
<style lang="scss" scoped>
.custom-textarea-btn {position: absolute;bottom: 1px;right: 4px;left: 4px;text-align: right;// background: #fff;padding-bottom: 3px;.el-button {font-size: 12px;padding: 4px 10px;}
}
.custom-textarea-box {position: relative;
}
.custom-at-limit {position: absolute;right: 12px;bottom: 4px;font-size: 12px;color: #999;line-height: 12px;
}
::v-deep.custom-textarea {width: 100%;min-height: 50px;max-height: 200px;border: 1px solid #dcdfe6;border-radius: 4px;background-color: #ffffff;padding: 5px 15px;color: #606266;overflow-y: auto;line-height: 20px;font-size: 14px;transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);position: relative;word-break: break-all;&.show-word-limit {padding-bottom: 16px;}&.custom-textarea-disabled {cursor: not-allowed;background-color: #f5f7fa;border-color: #e4e7ed;color: #c0c4cc;}&:focus {border-color: #f98600 !important;}&:empty::before {content: attr(placeholder);font-size: 14px;color: #c0c4cc;}.active-text {color: #909399;// padding: 2px 6px;// background: #f4f4f5;margin-right: 4px;// border-radius: 4px;// font-size: 12px;}// &:focus::before {//   content: "";// }
}::v-deep.custom-select-box {position: relative;.el-popover {padding: 0;top: 0;box-shadow: 0 4px 8px 0 rgba(89, 88, 88, 0.8);}.custom-select-content {width: 259px;padding: 8px;max-height: 260px;overflow-y: auto;}.custom-select-item {// font-size: 14px;// padding: 0 20px;// position: relative;// height: 34px;// line-height: 34px;// box-sizing: border-box;display: flex;padding: 8px 12px;border-bottom: 1px solid #ebebeb;align-items: center;color: #606266;cursor: pointer;&:last-child {border-bottom: none;}.avatar-box {flex-shrink: 0;.custom-select-item-avatar {width: 24px;height: 24px;background-color: #ffb803;border-radius: 50%;text-align: center;line-height: 24px;color: #ffffff;}}.custom-select-item-content {flex: 1;padding-left: 12px;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;}&:hover {background-color: #f5f7fa;}&.hoverItem {background-color: #dbdbdb;}}.custom-select-empty {padding: 10px 0;text-align: center;color: #999;font-size: 14px;&.load {display: flex;align-items: center;justify-content: center;}}.custom-scroll {overflow: auto;&::-webkit-scrollbar {width: 8px;height: 8px;}&::-webkit-scrollbar-thumb {border-radius: 8px;background-color: #b4b9bf;}}
}
</style>

外部调用示例

<template><div><CustomInputv-model="customInputHTML"placeholder="这是一个支持@的输入框组件"height="unset"@inputText="handleChangeInputText"@changeChosen="handleChangeChosen"/></div>
</template>
<script>
import CustomInput from "@/components/CustomInput/index.vue";
export default {components: { CustomInput },data() {return {// 输入框的html代码customInputHTML: "",// 输入框的文本,可让后端使用,如不使用,也没啥用customInputText: "",// 输入框返回的@人员数据customInputMentions: []}},methods: {// 获取输入框返回的文本handleChangeInputText(val) {this.customInputText = val;},// 获取输入框返回的文本handleChangeChosen(val) {this.customInputMentions = val;},}
}
</script>

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

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

相关文章

Linux:进程通信(二)信号的保存

目录 一、信号的处理是否是立即处理的&#xff1f; 二、信号如何保存 1、阻塞、未决、递达 2、信号集 3、信号集操作函数 4、sigprocmask函数 5、sigpending 函数 上篇文章我们讲解了信号的产生&#xff1a;Linux&#xff1a;进程信号&#xff08;一&#xff09;信号的产…

ResponseHttp

文章目录 HTTP响应详解使用抓包查看响应报文协议内容 Response对象Response继承体系Response设置响应数据功能介绍Response请求重定向概述实现方式重定向特点 请求重定向和请求转发比较路径问题Response响应字符数据步骤实现 Response响应字节数据步骤实现 HTTP响应详解 使用抓…

适用于 Windows 的免费恢复软件:前 7 个解决方案对比

Windows计算机上的数据恢复可能是一项简单或艰巨的任务&#xff0c;具体取决于您使用的软件。 软件的质量及其功能将决定将恢复多少数据、文件的完整性、扫描存储的深度以及整个过程在时间和恢复成功率方面的整体效率。 如果您想了解一些适用于 Windows的最佳免费取消删除软件…

Adobe Illustrator 2024 for Mac:矢量图形设计软件

Adobe Illustrator 2024 for Mac是一款专为Mac用户设计的行业标准矢量图形设计软件。该软件以其卓越的性能和丰富的功能&#xff0c;为设计师和艺术家们提供了一个全新的创意空间。 作为一款矢量图形软件&#xff0c;Adobe Illustrator 2024 for Mac支持创建高质量的矢量图形&a…

如何利用工作流自定义一个AI智能体

选择平台 目前已经有不少大模型平台都提供自定义智能体的功能&#xff0c;比如 百度的文心 https://agents.baidu.com/ 阿里的百炼平台 https://bailian.console.aliyun.com/。 今天再来介绍一个平台扣子&#xff08;https://www.coze.cn/&#xff09;&#xff0c;扣子是…

翔云优配官网美股市场分析问界回应M7事故四大疑问

问界再次新M7 Plus山西高速事故。 4月26日下午,山西运城曾有一辆问界新M7 Plus车辆(以下简称“事故车辆”)在高速行驶时和一辆高速洒水车追尾,碰撞后车辆起火,造成三人遇难,该事故引发了广泛关注。 翔云优配以其专业的服务和较低的管理费用在市场中受到不少关注。该平台提供了…

效率跨越式提升的工农业对机器人专业的需求

需求 需要用人的地方一定会逐步收缩。 原来需要人的地方也会逐步被机器人取代。 机器人这个专业最强的悖论就是可以部分取代人。 此处&#xff1a;用人的地方是指“工农业”&#xff0c;包括工业和农业。 机器人工程行业算制造业吗 机器人工程终身学习和工作计划 趋势 工匠…

安卓动态加载view

目录 前言一、addview1.1 addView 的重载方法1.2 在 LinearLayout 中的使用1.2.1 addView(View child)方法的分析&#xff1a;1.2.2 addView(View child, int index)方法的分析&#xff1a;1.2.3 小结 1.3 在 RelativeLayout 中的使用 二、addContentview2.1 测试 12.2 测试 22…

华为OD机试【最大N个数与最小N个数的和】(java)(100分)

1、题目描述 给定一个数组&#xff0c;编写一个函数来计算它的最大N个数与最小N个数的和&#xff0c;需要对数组进行去重。 说明&#xff1a; ● 数组中数字范围[0, 1000] ● 最大N个数与最小N个数不能有重叠&#xff0c;如有重叠&#xff0c;输入非法返回-1 ● 输入非法返回-…

【解决方案】Can‘t exec “locale”: No such file or directory

【解决方案】Cant exec “locale”: No such file or directory 还可能出现的错误&#xff1a; 1. 报错原因&#xff1a; 缺少ldconfig 2. 解决方案&#xff1a; sudo apt-get download libc-bin dpkg -x libc-bin*.deb unpackdir/ sudo cp unpackdir/sbin/ldconfig /sbin/ s…

明星中药企业系列洞察(一)丨官宣提价后股价涨幅近15%,百年老字号佛慈制药如何焕发力量?

近日&#xff0c;佛慈制药发布公告称&#xff0c;鉴于原材料以及生产成本上涨等原因&#xff0c;公司对主营中成药产品的出厂价进行调整&#xff0c;平均提价幅度为9%。提价消息释出后&#xff0c;资本市场给出了态度&#xff1a;佛慈制药股价连续两天累计上涨近15%。佛慈制药近…

Docker安装部署一本通:从Linux到Windows,全面覆盖!(网络资源精选)

文章目录 📖 介绍 📖🏡 说明 🏡⚓️ 相关链接 ⚓️📖 介绍 📖 随着容器技术的飞速发展,Docker已成为现代软件开发和运维不可或缺的工具。然而,不同平台下的Docker安装部署方式各异,这常常让初学者感到困惑。本文将为您详细梳理各平台下Docker的安装部署方法,帮…

组件通信-(父子组件通信)

目录 一、什么是组件通信 二、组件关系的分类 三、组件通信解决方案 四、父传子 五、子传父 一、什么是组件通信 组件通信&#xff0c;就是指组件与组件之间的数据传递。组件的数据是独立的&#xff0c;无法直接访问其他组件的数据。如果想使用其他组件的数据&#xff0c;…

cron表达式xxljob

Cron格式说明 | | | | | | | | | | | | | — 年 (range: 1970-2099) | | | | | ------- 周 (range: 1&#xff5e;7&#xff09; | | | | ----------- 月(range: 1&#xff5e;12) | | | --------------- 日(range: 1&#xff5e;31) | | ------------------- 时(range: 0…

软件FMEA的时机:架构设计、详设阶段——FMEA软件

免费试用FMEA软件-免费版-SunFMEA 软件FMEA&#xff08;故障模式与影响分析&#xff09;是一种预防性的质量工具&#xff0c;旨在识别软件中可能存在的故障模式&#xff0c;并分析其对系统性能、安全性和可靠性的影响。在软件开发生命周期中&#xff0c;选择适当的时机进行FME…

工业光源环形系列一高亮条形光源特点

产品特点 ◆可以根据检测需求随意调整照射角度&#xff1b; ◆可以根据检测需求选择光源颜色&#xff1b; ◆多个条形光源可以自由组合&#xff1b; ◆使用大功率贴片灯珠&#xff0c;亮度高&#xff1b; ◆灯珠上面增加透镜&#xff0c;照射距离更远

【Unity动画系统】动画层级(Animation Layer)讲解与使用

如何使用Unity的Animation Layer和Avater Mask把多个动画组合使用 想让玩家持枪行走&#xff0c;但是手里只有行走和持枪站立的动作。 Unity中最方便的解决办法就是使用动画层级animation layer以及替身蒙版avatar mask。 创建一个动画层级 Weight表示权重&#xff0c;0的话则…

使用 OpenNJet 分分钟完成打地鼠小游戏部署

文章目录 OpenNJet应用引擎什么是应用引擎什么是OpenNJet独特优势技术架构 OpenNJet安装RPM安装 部署打地鼠小游戏配置OpenNJet部署打地鼠小游戏启动 NJet访问打地鼠小游戏 总结 今天阿Q打算使用OpenNJet应用引擎来部署一下我们的打地鼠小游戏。在开始部署之前&#xff0c;我们…

教你快速记录每日待办事项,并提醒自己按时完成不忘记

在忙碌的日常生活中&#xff0c;我们经常会面临待办事项繁杂、时间紧迫的困扰。为了更高效地管理时间和任务&#xff0c;我们需要一个能够快速记录并准时提醒我们完成待办事项的工具。此时&#xff0c;敬业签这类的待办软件就成为了很多人的首选工具。 敬业签是一款功能强大的…